Saltar al contenido
K
Entorno y primer proyectoConcepto27 min Introductorio

Qué es KMP y qué no

Al terminar: Distinguir KMP de Compose Multiplatform y decidir cuánto código vas a compartir.

Antes de crear el proyecto de la Pokédex conviene saber qué estás eligiendo. Ya escribiste Marcador dos veces, una en Kotlin y otra en Swift, y viste cuánto código se duplicó; KMP es la respuesta a esa duplicación. Al terminar esta lección sabrás distinguir cuatro nombres que casi todo el mundo usa como si fueran el mismo, y tendrás un criterio para decidir dónde corta tu app la línea entre lo compartido y lo nativo.

La app que vas a construir es una Pokédex: una lista de Pokémon, el detalle de cada uno y una lista de favoritos. Los datos los pide a PokeAPI, un servicio web público y gratuito con la información de los Pokémon. Con ella verás cada idea aplicada a algo concreto.

Ese corte es la única decisión de fondo del curso. Todo lo demás —las librerías, los puentes a Swift, la forma de los ViewModels— sale de haberlo puesto en un sitio y no en otro.

Cuatro nombres que no son sinónimos

NombreQué es
Kotlin Multiplatform (KMP)La tecnología de compartir código Kotlin entre plataformas. Es lo que estás aprendiendo.
Kotlin/NativeEl compilador que convierte Kotlin en un binario nativo. Es lo que produce el framework de iOS (el paquete de código ya compilado que Xcode añade a la app).
Kotlin/JVMEl compilador que convierte Kotlin en bytecode para la máquina virtual de Java (JVM). Es el que usa Android.
Compose Multiplatform (CMP)Una librería de JetBrains para hacer interfaces, construida encima de KMP. Opcional.

Compilar es traducir tu código a instrucciones que una máquina ejecuta. Un binario nativo son instrucciones que entiende directamente el chip del iPhone; el bytecode son instrucciones para una máquina virtual, un programa que hace de chip, y es lo que ejecuta Android.

La confusión más común es la última. KMP no incluye UI. Compartir la interfaz es una decisión aparte, y la Pokédex decidió no hacerlo: comparte la lógica y escribe la interfaz dos veces, con Jetpack Compose en Android y con SwiftUI en iOS.

Qué se comparte, exactamente

Piensa en tu app como una pila. KMP te deja elegir dónde cortar.

ComposeAndroid
SwiftUIiOS
PresentaciónViewModel y estado
Dominioreglas de negocio
Datosrepositorios y persistencia
Red, JSON y modelos
La Pokédex corta entre la interfaz y la presentación: de los ViewModels hacia abajo, todo es código común.

(JSON es el formato de texto en el que los servidores suelen mandar los datos; los modelos son las clases que representan esos datos dentro de la app.)

Cuanto más arriba cortes, más compartes y más te acercas a «una sola app». Cuanto más abajo, más nativo es el resultado y más código duplicas.

Para hablar de «lo compartido» y «lo nativo» hace falta una palabra que oirás en todas las lecciones: source set («conjunto de fuentes»). Es una carpeta de código con un contrato: dice qué plataformas la compilan y, por tanto, qué APIs puede usar. Tu proyecto tendrá estas:

  • commonMain: el código que compilan todas las plataformas. Solo puede usar Kotlin común y librerías que también sean multiplataforma, porque tiene que funcionar en todas.
  • androidMain e iosMain: el código de una sola plataforma, con acceso a lo suyo (el SDK de Android, las APIs de Apple). Y jvmMain, para la JVM de tu Mac, donde corren los tests más rápidos.
  • commonTest: los tests comunes. Se ejecutan una vez por cada plataforma.

En la lección Anatomía del proyecto las verás por dentro; por ahora basta con la idea de «una carpeta, un contrato».

Fíjate en lo que no dice esa cita. No dice «comparte solo la capa de datos», ni «nunca compartas ViewModels». Google no impone un reparto. Si alguien te dice que «Google recomienda compartir solo X», pídele la fuente: no existe.

Los niveles de soporte de Kotlin/Native

Un target (destino) es cada plataforma para la que Kotlin compila tu código: Android, la JVM, el iPhone, el simulador de iPhone en un Mac con chip de Apple (los M1 y posteriores; se llama Apple Silicon)… Kotlin clasifica sus targets nativos (los que compila a código de máquina, sin máquina virtual) en tres tiers (niveles) según cuánto se garantizan. (¿Qué Mac tienes? Menú Apple → Acerca de este Mac: si dice «Chip» y un M, es Apple Silicon; si dice «Procesador» e Intel, es un Mac con Intel.) Importa porque no todos los destinos son igual de seguros, y porque uno de ellos decide algo del proyecto ya en la lección El módulo compartido y la prueba de humo.

Dos cosas de esas citas sorprenden la primera vez.

La primera: Android y la JVM no aparecen. No es un olvido. La página es de Kotlin/Native, y ni Android ni la JVM pasan por ese compilador: los compila Kotlin/JVM, el compilador original de Kotlin, que no necesita tiers. (Lo que sí aparece en el Tier 3, los «targets nativos de Android», es otra cosa: código nativo de bajo nivel para Android, como el de las librerías escritas en C, un lenguaje antiguo y muy cercano a la máquina. No es la app Android de tu Pokédex, que va por Kotlin/JVM.)

La segunda: el simulador de un Mac con Intel es Tier 3, el mismo nivel que Windows (mingwX64) o que el código nativo de bajo nivel para Android. El iPhone y el simulador de Apple Silicon son Tier 1. Por eso la lección El módulo compartido y la prueba de humo quita iosX64() del proyecto en cuanto lo genera el asistente, y no vuelve a aparecer.

La estructura que recomienda JetBrains

JetBrains publica una estructura de proyecto recomendada, y bifurca justo en la decisión que nos ocupa.

Los puntos de entrada (entry points) son los sitios por donde arranca cada app: MainActivity en Android y la estructura marcada con @main en iOS.

Tu situaciónEstructura recomendada
Todas las apps comparten UIUn único módulo shared
Alguna app tiene UI nativa (SwiftUI puro)sharedLogic + sharedUI separados

sharedLogic es la lógica de negocio sin dependencias de Compose. sharedUI es el código Compose, solo para las plataformas que comparten interfaz.

La Pokédex tiene UI nativa en las dos plataformas, así que sharedUI sencillamente no aplica: solo existe el equivalente a sharedLogic, llamado shared por ser el único.

Cómo se ve en la Pokédex

Ya en la primera versión del proyecto, antes de que haya una sola pantalla de verdad, está el patrón que se repetirá a lo largo de todo el curso: el contrato en común, las implementaciones por plataforma.

así será Platform.kt en tu proyecto (no lo escribas aún)
package com.sebastiannarvaez.pokedex
 
/**
 * El nombre y la version del sistema donde corre este codigo.
 *
 * Es una interfaz y no una `expect class` a proposito: JetBrains desaconseja
 * las clases `expect` donde basta una interfaz, porque una interfaz se puede
 * falsear en los tests y una `expect class` no.
 */
interface Platform {
    val name: String
}
 
/** Cada plataforma dice quien es. Lo unico que hace falta declarar dos veces. */
expect fun currentPlatform(): Platform

Fíjate en la palabra expect: expect promete, actual cumple. Esa última línea promete «existirá una función currentPlatform() que devuelve una Platform», sin decir cómo; cada plataforma la cumple con su actual. (Por qué se usa así, y no una clase expect, lo cuenta la lección expect/actual frente a interfaces.)

Dos palabras del comentario de arriba: «falsear» es sustituir la pieza real por una de pruebas, y una expect class es la otra forma de declarar una promesa (en vez de una función), que verás en la lección citada. Y «declarar dos veces» quiere decir la promesa en commonMain y su cumplimiento en cada plataforma.

Este fichero no sabe qué es Android ni qué es iOS. Quien lo sabe es su pareja en iosMain, pocas líneas. Los comentarios de cada línea están encima:

así será Platform.ios.kt en tu proyecto (con comentarios, no lo escribas aún)
package com.sebastiannarvaez.pokedex
 
// La clase de iOS que sabe qué sistema es. Es de UIKit, las clases de Apple
// sobre las que se apoya SwiftUI.
import platform.UIKit.UIDevice
 
// Cumple el contrato `Platform` de commonMain.
private class IosPlatform : Platform {
    // Nombre del sistema + su versión: «iOS» y «26.2», por ejemplo.
    override val name: String =
        UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}
 
// La promesa de commonMain (`expect`), cumplida para iOS.
actual fun currentPlatform(): Platform = IosPlatform()

UIDevice es una clase de UIKit, y la estás llamando desde Kotlin sin escribir una línea de Swift ni de Objective-C. Eso es Kotlin/Native. (systemName() lleva paréntesis porque es una función; systemVersion no, porque es una propiedad.)

Esa función la usa una clase Greeting que escribirás en El módulo compartido y la prueba de humo: su método greet() llama a currentPlatform() y devuelve «Hola desde » seguido del nombre de la plataforma. Si el mismo saludo se imprime en cada destino, esto es lo que sale (los números cambian con tu simulador, tu emulador y tu Java):

Hola desde iOS 26.2        (simulador de iPhone)
Hola desde Android 35      (emulador con Android 15)
Hola desde JVM 17.0.12     (los tests en tu Mac)

Lo que hace rentable el reparto no es escribir menos: es probar una vez. Este test vive en commonTest y se ejecuta tres veces, una por plataforma, y comprueba lo que devuelve greet(). Es el fichero entero:

así será GreetingTest.kt en tu proyecto (no lo escribas aún)
package com.sebastiannarvaez.pokedex
 
import kotlin.test.Test
import kotlin.test.assertTrue
 
class GreetingTest {
 
    @Test
    fun elSaludoNombraLaPlataforma() {
        // El mismo test corre en la JVM, en los tests de Android (en tu Mac) y en el
        // simulador de iOS, y en cada uno el nombre es distinto. Por eso se comprueba la forma, no el texto.
        val saludo = Greeting().greet()
        assertTrue(saludo.startsWith("Hola desde "), "saludo inesperado: $saludo")
        assertTrue(saludo.length > "Hola desde ".length, "falta el nombre de la plataforma")
    }
}

Cuándo KMP merece la pena

Merece la pena cuando tienes lógica que hoy está duplicada y se desincroniza. El caso típico: una regla de negocio que alguien arregla en Android y se olvida de arreglar en iOS.

Ya lo viviste con Marcador, en Escribiste la misma lógica dos veces: la misma clasificación escrita en Kotlin y en Swift. En la Pokédex el ejemplo más claro llega en la lección El Pokémon del día en el núcleo, con el Pokémon del día. La regla es una función pura de la fecha (dada la misma fecha, da siempre el mismo resultado): no hay sorteo, no hay servidor y no se guarda nada, así que dos dispositivos con la misma fecha enseñan el mismo Pokémon. Eso solo se sostiene si las dos plataformas ejecutan exactamente la misma aritmética. Escrita dos veces —una en Kotlin y otra en Swift— basta con que una redondee distinto para que tu amigo y tú veáis Pokémon distintos el mismo martes.

Escrita una vez, con tests que corren en las tres plataformas, el problema no puede existir.

No merece la pena cuando:

  • Tu app es casi toda interfaz y tiene poca lógica propia.
  • Quieres que se vea idéntica en ambas plataformas. Eso es CMP, no KMP, y además en iOS lo que suele querer el usuario es que se vea como iOS.
  • Tu equipo de iOS no escribe Kotlin y no quiere hacerlo. La tecnología funciona; la resistencia organizativa es un problema distinto y más difícil.

Un caso: una app de banca

Para afinar el criterio, piensa en una app de banca que quiere pasarse a KMP. Tiene: cálculo de comisiones, validación del número de cuenta, listas de movimientos, autenticación biométrica, gráficas de gasto y notificaciones push. ¿Qué compartirías?

Claramente compartido:

  • Cálculo de comisiones y validación del número de cuenta. Reglas de negocio puras. Que Android e iOS calculen comisiones distintas no es un fallo de pantalla: es un problema grave, porque el banco cobraría distinto según el móvil.
  • El modelo de los movimientos y su agrupación por fecha o categoría. Lógica, no dibujo.

Claramente nativo:

  • Entrar con la huella o con la cara. Face ID (en iPhone) y BiometricPrompt (en Android) no se parecen en nada, ni en cómo se programan ni en lo que ve el usuario. Lo que se comparte es la decisión («hace falta autenticar»), no el mecanismo.
  • Las gráficas. Son dibujo puro, y cada plataforma tiene librerías mejores que cualquier cosa que compartas.

El interesante, las notificaciones push (los avisos que el servidor manda al móvil). Parece infraestructura y por tanto no compartible, y en parte lo es: registrar el móvil para recibirlas y recibirlas son cosas de cada plataforma. Pero qué hacer con la notificación —a qué pantalla lleva, qué datos refresca, si se agrupa con otra— es lógica de aplicación y sí se comparte.

Ese caso resume la regla de toda la lección: el criterio no es «¿esto es técnico o es interfaz?», sino «¿esto podría dar respuestas distintas en las dos plataformas y sería un problema?». Si la respuesta es sí, compártelo.

La decisión de la Pokédex

Con todo lo anterior sobre la mesa, la decisión del proyecto fue: compartir la lógica, escribir la UI dos veces.

Los motivos, en orden de peso:

  1. El usuario de iPhone espera una app que se sienta iPhone. Las listas, los gestos y la navegación nativa lo dan gratis.
  2. Lo valioso del proyecto —el cliente de PokeAPI, la paginación (pedir la lista por trozos, no entera), el modelo de dominio, la caché de favoritos (los favoritos guardados en el móvil)— es precisamente lo que no tiene UI.
  3. Escribir dos veces una cuadrícula de tarjetas es barato. Escribir dos veces una paginación con reintentos y casos borde (los casos raros que casi nunca pasan), no.

La lección de Compose Multiplatform cuenta la alternativa con sus datos actuales —en iOS ya es estable— para que compares y decidas con tu propio criterio.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.¿Cuál es la diferencia entre Kotlin Multiplatform y Compose Multiplatform?

  2. 2.¿Qué dice la documentación oficial de Google sobre cuánto código debes compartir?

  3. 3.La página oficial de targets de Kotlin/Native pone iosArm64 e iosSimulatorArm64 en Tier 1. ¿Dónde está iosX64, el simulador de los Mac con Intel?

  4. 4.Según la estructura recomendada por JetBrains en 2026, si una de tus apps tiene UI nativa (SwiftUI puro), ¿cómo conviene organizar los módulos compartidos?

  5. 5.¿Cuál de estos NO es un buen motivo para elegir KMP?

Faltan 5 de 5