Saltar al contenido
K
Apéndice: más allá de la appConcepto60 min de lectura · 12 min de máquina Intermedio

Compose Multiplatform

Al terminar: Crear un módulo de interfaz compartida con Compose Multiplatform, verlo en Android y compilarlo para iOS, y decidir cuándo compartir también la interfaz.

Hasta aquí, Marcador ha ido por un camino: lógica compartida (:torneo), interfaz nativa en cada plataforma (Compose en Android, SwiftUI en iOS). Compose Multiplatform es el otro camino: escribir también la interfaz una sola vez. Desde hace poco es una opción mucho más seria de lo que era.

En esta lección lo pruebas con tu proyecto. Crearás un módulo nuevo, :pantalla, con una tabla de posiciones escrita una sola vez que usa los datos de :torneo: la verás funcionar en el emulador de Android, y la compilarás como framework para iOS. Por el camino instalarás, paso a paso, el plugin de Compose Multiplatform, sus bibliotecas y su sistema de recursos. Al final tendrás datos reales para decidir cuándo compartir también la interfaz y cuándo no.

Necesitas :torneo ya migrado a Kotlin Multiplatform, de Migrar un proyecto Android existente y Migrar el disco con expect/actual. Los pasos de iOS necesitan un Mac con Xcode; el resto vale en cualquier sistema. Los ficheros se crean con el método de Concurrencia y memoria en Kotlin/Native: con el selector del panel en la vista Android, clic derecho sobre el módulo → New → File y la ruta completa (o mkdir -p y touch en el Terminal).

Paso 1 · Qué es exactamente

Dos cosas que conviene tener claras desde el principio:

  • Jetpack Compose es de Google y es solo Android. Compose Multiplatform es de JetBrains y añade el resto de plataformas.
  • No es un envoltorio de componentes nativos. No traduce un Button al botón nativo de iOS (UIButton): dibuja su propia interfaz sobre un lienzo, con Skia, una biblioteca de gráficos 2D (la misma que usa Chrome). Lo que ves en iOS es Compose pintando, no UIKit.

Paso 2 · El dato que cambia la conversación

Durante años la respuesta honesta era «en iOS todavía no». Ya no.

La versión estable de hoy es la 1.12.1, la que vas a instalar. Cualquier texto que leas sobre «Compose en iOS es experimental» está desactualizado; comprueba la fecha antes de decidir nada con él.

Paso 3 · Crea el módulo :pantalla

El archivo. Un módulo es una carpeta con su propio build.gradle.kts. Hoy tienes :app (la app de Android) y :torneo (el dominio). Crearás :pantalla: la interfaz compartida. Dependerá de :torneo, y :app dependerá de ella.

Primero, la carpeta y su fichero de configuración. Abre el Terminal de Android Studio (pestaña abajo), en la carpeta de tu proyecto, y escribe:

mkdir pantalla
touch pantalla/build.gradle.kts

Ahora dile a Gradle que existe. En Gradle Scripts → settings.gradle.kts (Project Settings), añade una línea al final, junto a los otros include:

settings.gradle.kts: módulo nuevo
// Un módulo nuevo se declara aquí; sin esta línea Gradle ignora la carpeta.
include(":pantalla")

Pulsa Sync Now en la barra amarilla. Sync lee otra vez la configuración de Gradle; debe terminar sin rojo, y en Gradle Scripts aparece ahora build.gradle.kts (Module :pantalla), vacío. Ábrelo.

El concepto. Este módulo, como :torneo, tiene varios destinos (targets): Android, el iPhone y el simulador de iOS. Ya los viste en Migrar un proyecto Android existente; aquí se repiten con otro nombre de framework (un framework es el paquete de código que Xcode sabe enlazar). Escribe en build.gradle.kts (Module :pantalla):

build.gradle.kts (Module :pantalla): el esqueleto KMP
plugins {
    // Kotlin Multiplatform: un módulo, varios destinos (ya lo usa :torneo).
    alias(libs.plugins.kotlin.multiplatform)
    // El plugin de Android para bibliotecas KMP (el mismo que :torneo).
    alias(libs.plugins.android.kmp.library)
}
 
kotlin {
    // Compila con JDK 17 aunque tu máquina tenga otro.
    jvmToolchain(17)
 
    // Destino Android: lo consumirá :app.
    android {
        // Identificador del módulo; no es un applicationId (eso solo lo tiene una app).
        namespace = "com.sebastiannarvaez.marcador.pantalla"
        // Los mismos números que en :torneo (mira su build.gradle.kts): son literales, no salen del catálogo.
        compileSdk = 37
        minSdk = 24
    }
 
    // Destinos de iOS: iPhone y simulador. Cada uno produce un framework.
    listOf(iosArm64(), iosSimulatorArm64()).forEach { ios ->
        ios.binaries.framework {
            // El nombre con el que Swift lo importaría: import Pantalla.
            baseName = "Pantalla"
            // Estático: el código del framework se copia dentro de la app al enlazarla (el paso que lo une todo en un ejecutable).
            isStatic = true
        }
    }
 
    sourceSets {
        commonMain.dependencies {
            // Dependencia entre módulos: :pantalla usa los tipos de :torneo (FilaDePosicion, Ejemplo…).
            implementation(project(":torneo"))
        }
    }
}

Pulsa Sync Now. Debe terminar sin rojo. En este momento el módulo no tiene ningún código, y no hace falta: compila vacío.

Paso 4 · Instala el plugin de Compose Multiplatform

Qué es y por qué ahora. Para dibujar interfaz hacen falta dos plugins, y los dos son nuevos en este módulo:

  • org.jetbrains.compose: el plugin de Compose Multiplatform. Aporta las tareas que preparan los recursos (imágenes, textos) y el soporte de cada plataforma.
  • org.jetbrains.kotlin.plugin.compose: el plugin del compilador de Compose, el que entiende @Composable y transforma esas funciones. Es el mismo que ya usa :app (libs.plugins.kotlin.compose): ya existe en tu catálogo, no hay que declararlo de nuevo.

Ya conoces la receta de plugins de Los ficheros de Gradle de tu proyecto. Aplícala solo al que falta:

1. La versión y el plugin en el catálogo. Abre Gradle Scripts → libs.versions.toml (Version Catalog "libs"). Cada sección en su sitio. En [versions], al final de la sección:

catálogo: [versions], línea nueva
composeMultiplatform = "1.12.1"   # nombre en camelCase; la estable de hoy (sin -alpha ni -beta)

En [plugins], al final:

catálogo: [plugins], línea nueva
# id: el identificador del plugin; version.ref apunta a la línea de [versions]; en el código, libs.plugins.jetbrains.compose
jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }

2. Declararlo en la raíz. En Gradle Scripts → build.gradle.kts (Project: Marcador), dentro de plugins { … }, con apply false (declara que existe sin aplicarlo a ningún módulo todavía):

build.gradle.kts (Project: Marcador): línea nueva
    // alias: el plugin del catálogo; apply false: Gradle lo conoce, pero no lo aplica a la raíz
    alias(libs.plugins.jetbrains.compose) apply false

3. Aplicarlo en el módulo. En build.gradle.kts (Module :pantalla), dentro de plugins { … }, tras la línea libs.plugins.android.kmp.library:

build.gradle.kts (Module :pantalla): dos líneas nuevas
    // El plugin de Compose Multiplatform (recursos, tareas).
    alias(libs.plugins.jetbrains.compose)
    // El plugin del compilador de Compose: el mismo alias que ya usa :app.
    alias(libs.plugins.kotlin.compose)

4. Sync Now. Debe terminar sin rojo.

Paso 5 · Instala las bibliotecas de Compose

Qué son. Compose se reparte en bibliotecas pequeñas. Para una pantalla de texto te hacen falta tres: runtime (el motor: @Composable, estado), foundation (Column, Modifier…) y material3 (Text, MaterialTheme: los componentes con estilo). Van en commonMain porque la interfaz es común.

Las versiones y las bibliotecas, en el catálogo. runtime y foundation comparten versión con el plugin (la 1.12.1); material3 tiene la suya (la 1.9.0). En [versions]:

catálogo: [versions], línea nueva
composeMaterial3 = "1.9.0"   # material3 lleva su propio número, distinto del de Compose Multiplatform

En [libraries], al final de la sección:

catálogo: [libraries], líneas nuevas
# group y name son las coordenadas; runtime y foundation usan la versión del plugin, material3 la suya
jetbrains-compose-runtime = { group = "org.jetbrains.compose.runtime", name = "runtime", version.ref = "composeMultiplatform" }
jetbrains-compose-foundation = { group = "org.jetbrains.compose.foundation", name = "foundation", version.ref = "composeMultiplatform" }
jetbrains-compose-material3 = { group = "org.jetbrains.compose.material3", name = "material3", version.ref = "composeMaterial3" }

En el módulo. En build.gradle.kts (Module :pantalla), dentro de commonMain.dependencies { … }, tras la línea de :torneo:

build.gradle.kts (Module :pantalla): líneas nuevas
            // implementation: son para este módulo; :app no necesita verlas.
            implementation(libs.jetbrains.compose.runtime)
            implementation(libs.jetbrains.compose.foundation)
            implementation(libs.jetbrains.compose.material3)

Sync Now. Sin rojo.

Paso 6 · Escribe la pantalla, una sola vez

El archivo. Crea TablaCompartida.kt en el código común del módulo. En la vista Android, clic derecho sobre el nodo pantalla → New → File, y escribe la ruta completa:

src/commonMain/kotlin/com/sebastiannarvaez/marcador/pantalla/TablaCompartida.kt

Android Studio crea las carpetas que falten y abre el fichero vacío.

El concepto. Una @Composable escrita en commonMain es una función como cualquier otra: pinta lo mismo en todas las plataformas. Esta recibe las filas de la tabla de :torneo (FilaDePosicion, que ya conoces) y las dibuja con Column y Text:

TablaCompartida.kt
package com.sebastiannarvaez.marcador.pantalla
 
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.sebastiannarvaez.marcador.torneo.FilaDePosicion
 
// Una pantalla escrita UNA vez: el mismo código se ejecuta en Android y en iOS.
@Composable
fun TablaCompartida(filas: List<FilaDePosicion>) {
    // Column apila a sus hijos de arriba abajo; padding deja 16 dp de margen.
    Column(Modifier.padding(16.dp)) {
        Text("Tabla de posiciones", style = MaterialTheme.typography.titleMedium)
        // Una línea por equipo: puesto, nombre, puntos y goles a favor-en contra.
        filas.forEachIndexed { indice, fila ->
            Text("${indice + 1}. ${fila.equipo.nombre}  ${fila.puntos} pts  (${fila.golesAFavor}-${fila.golesEnContra})")
        }
    }
}

Los import son los mismos androidx.compose… que en Android: Compose Multiplatform mantiene los nombres de paquete de Jetpack Compose, solo cambia de dónde vienen las bibliotecas.

Comprueba que compila para Android: en el Terminal, ./gradlew :pantalla:compileAndroidMain termina en BUILD SUCCESSFUL (la primera vez descarga las bibliotecas).

Paso 7 · Úsala desde :app

La dependencia. :app tiene que ver :pantalla, igual que ya ve :torneo. En Gradle Scripts → build.gradle.kts (Module :app), dentro de dependencies { … }, justo debajo de implementation(project(":torneo")):

build.gradle.kts (Module :app): línea nueva
    // :app usa el módulo :pantalla, igual que ya usa :torneo
    implementation(project(":pantalla"))

Sync Now. Sin rojo.

La pantalla. Abre app → kotlin+java → com.sebastiannarvaez.marcador → MainActivity. En la función PantallaDePruebas (la pantalla de pruebas que separaste en Compose: Scaffold, slots y tema), debajo de la línea Text("Marcador"), añade la llamada; arriba del fichero, el import:

MainActivity.kt: import nuevo (junto a los demás)
import com.sebastiannarvaez.marcador.pantalla.TablaCompartida
MainActivity.kt: línea nueva en PantallaDePruebas, tras el texto Marcador
                    // La tabla compartida: la dibuja el código de :pantalla.
                    TablaCompartida(tabla)

tabla ya existe ahí: es la lista de filas que PantallaDePruebas recibe como parámetro.

Ejecuta la app (botón Run con app y tu emulador elegidos), abre la pantalla de pruebas (el botón «Pruebas de Android» de la pantalla del marcador) y debajo de «Marcador» verás, con los dos partidos de ejemplo:

Tabla de posiciones
1. Rayo FC  3 pts  (2-1)
2. Águilas  3 pts  (2-1)
3. Lobos  0 pts  (1-2)
4. Toros  0 pts  (1-2)

Esa tabla la dibuja código que vive en commonMain, no en :app.

Paso 8 · Textos que viajan con la pantalla: los recursos

Hasta ahora el título es un literal dentro del código. En Android los textos viven en res/values/strings.xml, pero :pantalla no es una app de Android: Compose Multiplatform trae su propio sistema de recursos, que sirve igual en todas las plataformas.

Instala la biblioteca de recursos. En el catálogo, [libraries]:

catálogo: [libraries], línea nueva
# la biblioteca de recursos de Compose Multiplatform; comparte versión con el plugin
jetbrains-compose-components-resources = { group = "org.jetbrains.compose.components", name = "components-resources", version.ref = "composeMultiplatform" }

Y en build.gradle.kts (Module :pantalla), en commonMain.dependencies:

build.gradle.kts (Module :pantalla): línea nueva
            // en commonMain: los recursos sirven igual para Android y para iOS
            implementation(libs.jetbrains.compose.components.resources)

Sync Now. Sin rojo.

El archivo. Los recursos viven en una carpeta especial, composeResources, dentro del source set. Clic derecho sobre el nodo pantalla → New → File, ruta:

src/commonMain/composeResources/values/strings.xml
strings.xml: el título, una sola vez para todas las plataformas
<resources>
    <!-- name es el identificador con el que el código encuentra este texto. -->
    <string name="titulo_tabla">Tabla de posiciones</string>
</resources>

El concepto. El plugin lee esa carpeta al compilar y genera una clase Res con un campo por recurso (Res.string.titulo_tabla). Por defecto la deja en un paquete derivado del nombre del proyecto, difícil de adivinar; mejor fijarlo. Añade al final de build.gradle.kts (Module :pantalla), fuera del bloque kotlin { … }:

build.gradle.kts (Module :pantalla): bloque nuevo al final
// El paquete donde el plugin deja la clase Res (y los accesos a cada recurso).
compose.resources {
    packageOfResClass = "com.sebastiannarvaez.marcador.pantalla.recursos"
}

Úsalo. En TablaCompartida.kt, añade estos tres import con los demás y cambia la línea del título:

TablaCompartida.kt: imports nuevos
import com.sebastiannarvaez.marcador.pantalla.recursos.Res           // la clase que genera el plugin
import com.sebastiannarvaez.marcador.pantalla.recursos.titulo_tabla  // el acceso a ese texto concreto
import org.jetbrains.compose.resources.stringResource                // lee el texto de un recurso
TablaCompartida.kt: la línea del título, antes y después
// Antes:
Text("Tabla de posiciones", style = MaterialTheme.typography.titleMedium)
// Después: el texto sale de composeResources/values/strings.xml.
Text(stringResource(Res.string.titulo_tabla), style = MaterialTheme.typography.titleMedium)

Sync Now y compila: ./gradlew :pantalla:compileAndroidMain termina bien. Pero ejecuta la app (Run con app y tu emulador) y abre la pantalla de pruebas: aquí viene un fallo que no es de compilación.

La app compila, pero se cierra al abrir la pantalla y Logcat dice:

org.jetbrains.compose.resources.MissingResourceException: Missing resource with path: composeResources/com.sebastiannarvaez.marcador.pantalla.recursos/values/strings.commonMain.cvr

El módulo se declaró como biblioteca de Android con el plugin de KMP, y ese plugin no empaqueta recursos por defecto: el recurso existe en el módulo pero no viaja dentro de la app. Se activa con un paso más.

Activa los recursos de Android en el módulo. En build.gradle.kts (Module :pantalla), dentro del bloque android { … }, tras la línea de minSdk:

build.gradle.kts (Module :pantalla): dentro de android { … }, líneas nuevas
        // Sin esto el plugin KMP de Android no incluye los recursos de Compose en la app.
        androidResources {
            enable = true
        }

Sync Now (sin rojo) y vuelve a ejecutar: el título aparece.

Paso 9 · iOS: la misma pantalla en un UIViewController

El archivo. Para iOS hay un paso más, porque iOS no entiende una @Composable: entiende un UIViewController.

Esa función solo existe en iOS, así que va en iosMain, el source set que comparten el iPhone y el simulador (la jerarquía de Anatomía del proyecto). Mismo método: clic derecho sobre el nodo pantalla → New → File, ruta:

src/iosMain/kotlin/com/sebastiannarvaez/marcador/pantalla/MainViewController.kt
MainViewController.kt: el punto de entrada de iOS
package com.sebastiannarvaez.marcador.pantalla
 
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.window.ComposeUIViewController
import com.sebastiannarvaez.marcador.torneo.Ejemplo
import platform.UIKit.UIViewController
 
// Devuelve un UIViewController de UIKit que dibuja Compose por dentro.
// (platform.UIKit es UIKit visto desde Kotlin: Kotlin/Native trae las cabeceras de los frameworks de Apple.)
fun MainViewController(): UIViewController = ComposeUIViewController {
    MaterialTheme {
        // Los mismos dos partidos de ejemplo que en Android.
        TablaCompartida(Ejemplo.torneoConPartidos().tablaDePosiciones())
    }
}

Compílalo (en un Mac con Xcode; la primera vez tarda unos minutos):

./gradlew :pantalla:linkDebugFrameworkIosSimulatorArm64

Termina en BUILD SUCCESSFUL y deja el framework en pantalla/build/bin/iosSimulatorArm64/debugFramework/Pantalla.framework. Dentro, el fichero Headers/Pantalla.h contiene la línea que verá Swift:

+ (UIViewController *)MainViewController __attribute__((swift_name("MainViewController()")));

Fíjate en el nombre de la clase, MainViewControllerKt: Kotlin agrupa las funciones sueltas de un fichero en una clase llamada <Fichero>Kt. Desde Swift se llamaría MainViewControllerKt.MainViewController(), y se envuelve para SwiftUI así:

envolver ese UIViewController para SwiftUI
import Pantalla
import SwiftUI
 
// Un envoltorio: convierte un UIViewController de UIKit en una vista de SwiftUI
struct ComposeView: UIViewControllerRepresentable {
    // SwiftUI lo llama una vez para crear el controlador
    func makeUIViewController(context: Context) -> UIViewController {
        MainViewControllerKt.MainViewController()   // la función de Kotlin, dentro de la clase <Fichero>Kt
    }
    // SwiftUI lo llama al cambiar el estado; aquí no hay nada que actualizar
    func updateUIViewController(_ vc: UIViewController, context: Context) {}
}

Enchufar ese framework a un proyecto de Xcode es una operación aparte, la de Integración con iOS (la fase de compilación de Gradle que ya hiciste en la Pokédex); la app de iOS de Marcador no la hace, porque usa su propio paquete Swift. Aquí te quedas con el framework compilado, que prueba que la pantalla compila para iOS.

Y al revés: UIKit dentro de Compose

Esto es lo que hace viable la adopción parcial: no tienes que elegir para toda la app. Puedes tener pantallas en Compose y pantallas en SwiftUI, y meter un componente nativo dentro de una pantalla de Compose.

Comprueba. Al final de la lección, desde el Terminal, estos comandos terminan en BUILD SUCCESSFUL: ./gradlew :pantalla:compileAndroidMain, ./gradlew :app:assembleGratisDebug y, en un Mac, ./gradlew :pantalla:linkDebugFrameworkIosSimulatorArm64; y ./gradlew :torneo:jvmTest sigue con los mismos tests que tenías. En el emulador, «Tabla de posiciones» y las cuatro filas.

Paso 10 · Qué cambiaría en Marcador si la interfaz fuera compartida

Con lo que acabas de construir, la pregunta deja de ser abstracta. Marcador tiene hoy la interfaz escrita dos veces: unas 2 500 líneas de Kotlin en :app (con Compose, notificaciones, permisos, Room y DataStore) y la app de iOS entera en SwiftUI.

Lo que se fundiría en uno. Las pantallas que son estado, listas y formularios: la lista de partidos, los goleadores, el marcador en directo. Hoy cada una existe como Pantalla… en Compose y como …View en SwiftUI con el mismo trabajo. Ahí está el grueso del ahorro.

Lo que se simplificaría de golpe, en un proyecto que sí comparte Kotlin con iOS (la Pokédex, no Marcador: la app de iOS de Marcador usa su propio paquete Swift y no consume Kotlin, así que aquí no hay puente que quitar). En la Pokédex existe una capa de puente con Swift: envoltorios observables, cajas para los tipos que cruzan. Al consumir un StateFlow desde Compose no haría falta ninguno. Es la ganancia menos comentada y probablemente la mayor: no es que escribas menos interfaz, es que desaparece el código que solo existía para cruzar la frontera.

Lo que no se movería. Las notificaciones, los permisos, el disco y los ajustes siguen siendo de cada sistema. La documentación lo dice sin rodeos:

Y lo que habría que revisar aunque no se note. Marcador usa Room, DataStore y Navigation 3 en :app. Cada una tendría que comprobarse contra los destinos que publica, que es exactamente el trabajo de mirar el fichero .module de una biblioteca. Nada de esto es imposible; todo esto es tiempo.

Paso 11 · La tabla honesta

Se ganaSe pierde
Cantidad de códigoUna interfaz en vez de dosNada
La capa de puente (en un proyecto que comparte Kotlin con iOS)Desaparece el código que solo cruzaba la fronteraNada: es el argumento más fuerte
Aspecto en iOSConsistencia perfecta entre plataformasEl aspecto lo pones tú: el sistema ya no te lo regala
Gestos y física—El rebote del scroll, el teclado y el deslizar-para-volver hay que comprobarlos uno a uno
Accesibilidad—VoiceOver sobre un lienzo propio no es lo mismo que sobre UIKit
Actualizaciones de iOS—Un componente nativo se moderniza solo al salir una versión; el tuyo, no
Tamaño del binario—Se añade el motor de renderizado
A quién puedes contratarUn equipo de Kotlin cubre las dos plataformasNadie del equipo escribe SwiftUI, y eso también es una dependencia
Aprendizaje de la frontera—No aprendes nada de interop, porque deja de haber interop

Paso 12 · Por qué la Pokédex no lo usa

Paso 13 · Cuándo sí y cuándo no

Encaja bien cuando el equipo es pequeño y sabe Kotlin, la interfaz es sobre todo formularios, listas y paneles, la consistencia entre plataformas es una ventaja para el producto, y la velocidad importa más que el último detalle de aspecto nativo.

Encaja mal cuando hay un equipo de iOS con criterio propio sobre cómo debe sentirse la app, la interfaz depende mucho de componentes del sistema, la accesibilidad es un requisito duro, o el producto se juega su reputación en parecer nativo.

Reto · La pantalla piloto

Tu equipo quiere probar Compose Multiplatform sin comprometerse. ¿Qué pantalla de Marcador eliges de primera, y qué mides para decidir?

Ver solución

La pantalla que eliges debe cumplir tres condiciones a la vez:

  • Que tenga entrada de texto y una lista, porque el teclado y el desplazamiento son justo donde más se nota la diferencia. Una pantalla de solo texto no prueba nada.
  • Que no sea la principal, para que un problema no arruine la impresión del producto.
  • Que ya exista en las dos plataformas, para poder comparar contra algo real en vez de contra una idea.

En Marcador la candidata es ajustes (o una pantalla de registro de un partido con su formulario): tiene controles de formulario, una lista de opciones y ningún gesto característico, y no es la primera pantalla que alguien ve. La que no elegirías es la lista de partidos en directo, aunque sea la más vistosa: arrastra datos que cambian, navegación y notificaciones, y si algo sale mal no sabrás cuál de las tres cosas fue.

Qué mides, en este orden:

  1. La prueba a ciegas: enseñársela en un dispositivo a alguien que use iPhone a diario, sin decirle qué mira. Es el único dato que importa de verdad.
  2. Accesibilidad con VoiceOver activado, recorriendo la pantalla entera. Es lo que más suele fallar y lo que menos se prueba.
  3. El tamaño de la app antes y después, medido, no estimado.
  4. El tiempo de compilación, porque afecta a todos los días de trabajo del equipo.

Y la trampa a evitar: no elijas la pantalla más fácil. Si el piloto es un «Hola mundo», la conclusión será «funciona perfecto» y no habrás aprendido nada sobre el caso que te va a doler.

Comprueba lo que entendiste

7 preguntas · necesitas 5 para completar la lección

  1. 1.¿Cuál es el nivel de estabilidad de Compose Multiplatform en iOS hoy?

  2. 2.¿Cómo dibuja Compose Multiplatform un Button en iOS?

  3. 3.Integras Compose Multiplatform en una app de iOS y se cierra al arrancar en un dispositivo real. ¿Qué te falta probablemente?

  4. 4.Un proyecto que comparte Kotlin con iOS, como la Pokédex, adopta Compose Multiplatform para toda su interfaz. ¿Cuál sería la ganancia menos comentada y probablemente la mayor?

  5. 5.Quieres probar Compose Multiplatform con una pantalla piloto de Marcador. ¿Cuál eliges y por qué?

  6. 6.Aplicas el plugin org.jetbrains.compose a :pantalla y Sync falla al configurar el módulo diciendo que debes aplicar org.jetbrains.kotlin.plugin.compose. ¿Qué haces?

  7. 7.La app compila, pero al abrir la pantalla de :pantalla que usa stringResource se cierra con MissingResourceException. El módulo usa el plugin KMP de Android. ¿Qué falta?

Faltan 7 de 7