wandres.dev
MULTIPLATAFORMA · KMP y Compose Multiplatform

Compose Multiplatform: la misma interfaz en iOS y escritorio

Compose Multiplatform lleva el modelo declarativo de Compose fuera de Android dibujando la interfaz sobre un lienzo propio en lugar de traducirla a componentes del sistema, y esa decisión arquitectónica explica a la vez su fidelidad visual perfecta entre plataformas y todas sus costuras. Esta lección describe qué parte del ecosistema viaja intacta y cuál se queda en el camino, examina con detalle las junturas que aparecen en iOS —texto, accesibilidad, gestos del sistema, tamaño del binario y arranque—, sitúa el estado real de madurez en 2026 y defiende el patrón híbrido de composición incrustada como la estrategia de adopción con mejor relación entre riesgo y beneficio.

⏱ 22 min

Compose Multiplatform es la parte del ecosistema que despierta más entusiasmo y más desconfianza, y ambas reacciones tienen fundamento porque nacen del mismo hecho técnico. A diferencia de otras soluciones multiplataforma que traducen tus componentes a componentes nativos del sistema, Compose no traduce nada: se lleva consigo su propio motor de dibujo, reserva un lienzo en la pantalla y pinta cada píxel de la interfaz por su cuenta. La consecuencia inmediata es que lo que ves en Android es exactamente lo que ves en iOS, hasta el radio del último borde, sin las divergencias sutiles que atormentan a quien traduce. La consecuencia mediata, y la que hay que entender antes de comprometerse, es que todo aquello que el sistema operativo hacía por ti —seleccionar texto, leerlo en voz alta, reaccionar al gesto de retroceso, mostrar el teclado en el sitio correcto— deja de venir gratis y pasa a ser algo que alguien tiene que reimplementar dentro del lienzo.

🎯 Al terminar esta lección sabrás
  • Explicar el modelo de dibujo sobre lienzo propio y deducir de él las ventajas y las costuras.
  • Distinguir qué capas del ecosistema Compose viajan a iOS y cuáles siguen siendo de Android.
  • Evaluar con criterio las junturas conocidas: texto, accesibilidad, gestos, arranque y tamaño.
  • Adoptar el patrón híbrido de composición incrustada dentro de una aplicación nativa.

Un lienzo propio, no una traducción

En Android, Compose ya dibujaba sobre un lienzo dentro de una vista anfitriona; llevarlo a otras plataformas consistió en sustituir esa vista anfitriona y en apoyar el dibujo sobre un motor gráfico portable. En iOS el resultado es un controlador de vista que aloja una superficie, y dentro de ella el árbol de composición completo. En escritorio es una ventana del sistema con la misma superficie dentro. El árbol de composición, la recomposición, los modificadores, el sistema de medición y colocación, el estado y las animaciones son idénticos en todos los destinos porque son código Kotlin común que no consulta a nadie.

flowchart TD
A[Codigo de UI en commonMain] --> B[Runtime de Compose]
B --> C[Motor de dibujo portable]
C --> D[Superficie en Android]
C --> E[Superficie dentro de un UIViewController]
C --> F[Ventana de escritorio]
E -.->|hay que reconstruirlo dentro| G[Texto, accesibilidad, gestos del sistema]
D -.->|lo aporta el sistema| G

Lo que viaja intacto es más de lo que se supone: el runtime, la biblioteca de interfaz, Material 3, las animaciones, los gestos de pointerInput, la navegación en su versión multiplataforma y las herramientas de previsualización. Lo que no viaja es todo lo que estaba anclado a Android: los recursos a través de la clase generada, el ciclo de vida de las actividades, los permisos, las vistas incrustadas mediante interoperabilidad, las bibliotecas de terceros que solo publican artefactos para Android, y una parte del catálogo de componentes adaptativos que asume convenciones de la plataforma. La sustitución de recursos por el sistema multiplataforma es mecánica y aburrida, pero afecta a cada cadena y cada imagen del proyecto y no conviene descubrirla el último día.

// commonMain: identico a lo que ya escribes en Android
@Composable
fun PantallaSuscripcion(estado: EstadoVisible, onRenovar: () -> Unit) {
    Column(Modifier.fillMaxSize().padding(24.dp)) {
        Text(text = estado.titulo, style = MaterialTheme.typography.headlineSmall)
        Spacer(Modifier.height(12.dp))
        Button(onClick = onRenovar) { Text("Renovar") }
    }
}

// iosMain: el punto de entrada que iOS entiende
fun MainViewController(): UIViewController = ComposeUIViewController {
    AppTheme { PantallaSuscripcion(estado, onRenovar = { /* ... */ }) }
}

Las costuras que hay que conocer antes de decidir

Ninguna de las costuras conocidas es un defecto de implementación pendiente de un parche: todas se derivan de dibujar en lugar de traducir, y por tanto se mitigan pero no desaparecen.

El texto es la primera. Seleccionar, copiar, arrastrar el cursor con la lupa, invocar el menú contextual del sistema y respetar el ajuste de tamaño de fuente del usuario son comportamientos que en iOS pertenecen al componente nativo de texto, y dentro del lienzo hay que reproducirlos. La reproducción ha mejorado de forma notable, pero un usuario de iOS detecta la diferencia en el gesto largo sobre un campo de entrada antes de saber decir por qué.

La accesibilidad es la segunda y la más seria. El árbol de semántica de Compose se puentea hacia el árbol de accesibilidad del sistema, de manera que el lector de pantalla recibe algo con lo que trabajar, pero la correspondencia no es total y el orden de recorrido, las acciones personalizadas y las rotaciones del rotor requieren atención explícita. Una aplicación con obligaciones legales de accesibilidad debe auditar esto en el dispositivo antes de comprometerse, no después.

⚠️
El gesto de retroceso por borde y el teclado son los dos fallos de aceptación más frecuentes

El deslizamiento desde el borde izquierdo para retroceder es en iOS una convención tan interiorizada que su ausencia se percibe como aplicación rota, y dentro de un lienzo hay que conectarlo a mano con la navegación del gestor nativo. El comportamiento del teclado —el desplazamiento del contenido para que el campo enfocado quede visible, el respeto a la zona segura, la barra de accesorios— es la otra fuente sistemática de informes. Ambos son resolubles y ambos se olvidan en la planificación, y juntos explican buena parte de las historias de proyectos que llegaron al noventa por ciento en tres semanas y tardaron cuatro meses en el diez restante.

El tamaño del binario y el tiempo de arranque completan la lista. Incorporar un motor de dibujo y el runtime de Kotlin/Native añade varios megabytes al paquete y un coste de inicialización de la primera composición que en dispositivos antiguos es perceptible. Para una aplicación grande es ruido; para una aplicación pequeña cuyo argumento es la ligereza puede ser un veto.

Queda una costura de naturaleza distinta que no aparece en las listas técnicas y que sin embargo decide muchos proyectos: el desajuste de expectativas dentro del propio equipo. Una pantalla compartida es, por definición, una pantalla que ningún diseñador puede pedir que se comporte según las convenciones de una sola plataforma sin romper la compartición. Cuando el equipo de diseño no ha participado en la decisión, cada revisión de producto se convierte en una negociación sobre si tal detalle se implementa dos veces o se renuncia a él, y esas negociaciones consumen más tiempo que el código que ahorraron.

Recursos, temas y el trabajo diario

Fuera de las costuras conceptuales, la adopción se juega en asuntos prosaicos que ocupan las primeras semanas. El más extenso es el de los recursos: la clase generada de Android desaparece y en su lugar hay un sistema multiplataforma que genera accesores tipados a partir de una carpeta común, con soporte para cadenas, plurales, imágenes y fuentes, y con cualificadores por idioma y por densidad.

// commonMain: los recursos se resuelven en tiempo de composicion
@Composable
fun Cabecera() {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Image(painterResource(Res.drawable.logo), contentDescription = null)
        Spacer(Modifier.width(8.dp))
        Text(stringResource(Res.string.titulo_suscripcion))
    }
}

La conversión es mecánica pero total: afecta a cada cadena traducida, a cada icono y a cada dimensión del proyecto, y no admite una migración parcial elegante porque conviven mal los dos sistemas. Por eso es el trabajo que conviene planificar con nombre propio en el calendario en lugar de dejarlo como detalle de implementación.

El tema es la segunda pieza cotidiana. Material 3 viaja a todos los destinos, incluidos el esquema de color, la tipografía y las formas, pero el color dinámico extraído del fondo de pantalla es una capacidad de Android que en iOS no tiene equivalente ni sentido. La solución habitual consiste en declarar un tema propio en el común, con sus valores explícitos, y permitir que Android lo sobrescriba con el color dinámico cuando el usuario lo tenga activado.

ℹ️
La previsualización sigue funcionando, y es más importante de lo que parece

Las herramientas de previsualización operan sobre el destino de escritorio y sobre el de Android, de manera que el ciclo de trabajo diario para una pantalla compartida es el mismo que ya conoces: escribir, ver el resultado sin compilar la aplicación entera, iterar. Ese detalle, que suena a comodidad, es en realidad uno de los argumentos económicos más sólidos a favor de la interfaz compartida, porque el bucle de retroalimentación corto es lo que hace barata la construcción de interfaces y es exactamente lo que se pierde al mantener dos implementaciones que solo se pueden comparar ejecutándolas en dos dispositivos.

Madurez en 2026 y la estrategia híbrida

El estado razonable de las cosas a mediados de 2026 es que el destino de escritorio y el de Android son plenamente productivos, que iOS es estable y se usa en producción por empresas serias, y que la web sobre WebAssembly es la frontera activa donde todavía conviene medir antes de prometer. La pregunta útil ya no es si funciona, sino cuánta interfaz merece la pena unificar en un producto concreto.

Merece la pena separar dos afirmaciones que suelen ir juntas y que tienen valores de verdad distintos. Que la tecnología esté madura significa que hace lo que dice y que sus fallos son conocidos y documentados; no significa que la distancia con una interfaz nativa haya desaparecido, porque esa distancia no es un defecto que se corrija sino una consecuencia del método. Un equipo que adopta interfaz compartida esperando paridad total con lo nativo va a sentirse engañado por una tecnología que nunca prometió eso; un equipo que la adopta sabiendo que compra uniformidad y velocidad a cambio de una fracción de familiaridad va a estar razonablemente satisfecho durante años.

📝
El destino de escritorio es el que más gente subestima

Casi toda la conversación gira en torno a iOS, y sin embargo el retorno más limpio suele estar en escritorio: herramientas internas, paneles de operaciones, utilidades para el propio equipo. Ahí no hay convenciones del sistema tan interiorizadas por el usuario, no hay revisión de tienda, no hay costuras de accesibilidad móvil, y una pantalla ya escrita para Android se convierte en una aplicación de escritorio funcional en cuestión de horas. Muchos equipos descubren que su primera adopción exitosa de Compose Multiplatform no fue el producto, sino la herramienta interna que nadie quería mantener.

La respuesta que mejor envejece casi nunca es todo o nada. Compose Multiplatform se incrusta como un controlador de vista más dentro de una aplicación iOS nativa, lo que permite que la navegación, el arranque, los permisos y las pantallas de mayor exigencia sigan siendo nativos mientras las pantallas densas en lógica y pobres en interacción se comparten. Ese patrón reduce el riesgo a una pantalla, permite medir con usuarios reales antes de ampliar y deja abierta la retirada, que es la propiedad que ninguna decisión de arquitectura debería perder.

// iosApp: la pantalla compartida es un controlador mas dentro de la navegacion nativa
struct PantallaCompartida: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        MainViewControllerKt.MainViewController()
    }
    func updateUIViewController(_ vc: UIViewController, context: Context) { }
}

El criterio para elegir qué pantalla se comparte primero no debería ser la más fácil, sino la que mejor información produzca. Una pantalla de ajustes es fácil y no enseña nada; un listado con desplazamiento, entrada de texto y estados de carga enseña casi todo lo que hay que saber sobre rendimiento, teclado, accesibilidad y sensación al tacto, y lo enseña en dos semanas en lugar de en dos trimestres.

Las pantallas que casi siempre resultan buenas candidatas son las que están densamente pobladas de reglas y pobremente pobladas de gestos: formularios de alta con validación compleja, resúmenes de estado, historiales, flujos de configuración de producto. Las que casi nunca lo son incluyen la pantalla de inicio, cualquier cosa con cámara o mapa incrustados, los reproductores multimedia con controles del sistema y todo lo que dependa de widgets, atajos o integraciones fuera de la aplicación.

🎨

Dibuja, no traduce

Fidelidad visual absoluta entre plataformas a cambio de reconstruir lo que daba el sistema.

🗣️

Accesibilidad primero

El puente hacia el árbol nativo existe, pero se audita en dispositivo antes de comprometerse.

👆

Gestos y teclado

Retroceso por borde y desplazamiento al enfocar son el noventa por ciento de los informes.

🧩

Incrusta, no sustituyas

Una pantalla compartida dentro de una aplicación nativa mantiene abierta la puerta de salida.

Dibujar la interfaz es asumir un contrato con el sistema operativo que el sistema no ha firmado

Cuando una aplicación traduce sus componentes a componentes nativos, hereda automáticamente todo lo que el fabricante decida hacer con ellos en los próximos diez años: cada mejora de accesibilidad, cada nuevo gesto, cada ajuste de la tipografía dinámica, cada modo de lectura, cada integración con un dispositivo de asistencia llega sin que nadie haga nada. Cuando una aplicación dibuja su propia interfaz, esa herencia se corta de raíz y se sustituye por una obligación permanente de imitación. Es fundamental ver que no se trata de una deuda que se salda una vez, sino de una suscripción: el sistema operativo se mueve cada septiembre, y cada movimiento amplía la distancia entre lo que los usuarios esperan de una aplicación de esa plataforma y lo que tu lienzo sabe hacer. Los equipos que adoptan interfaz compartida con éxito son los que entienden esto desde el principio y presupuestan una vigilancia continua, exactamente igual que presupuestan la actualización anual del nivel de API objetivo; los que fracasan son los que trataron la adopción como una migración con fecha de fin. Y hay un segundo contrato, más silencioso, que se firma con los propios usuarios. Una interfaz idéntica en dos sistemas es un objetivo de ingeniería, no un objetivo de producto: nadie usa las dos versiones a la vez, y por tanto ningún usuario percibe jamás la consistencia entre plataformas que tanto esfuerzo costó. Lo que sí percibe cada usuario, con una precisión que sorprende, es la distancia entre tu aplicación y las demás aplicaciones de su propio teléfono, porque esas sí las usa todos los días. La uniformidad entre plataformas produce, medida desde el único sitio desde el que alguien la mide, una pérdida neta de familiaridad. Eso no invalida la decisión —hay productos con identidad visual tan fuerte que la uniformidad es un activo, y hay pantallas tan neutras que nadie nota nada—, pero obliga a formularla con honestidad: se comparte interfaz porque el ahorro en construcción y mantenimiento supera la fricción que se traslada al usuario, y ese balance hay que rehacerlo pantalla por pantalla y no proclamarlo una vez para todo el producto.

⚔️ Incrusta una pantalla y busca las junturas a propósito
  1. Extrae una pantalla real de tu aplicación a commonMain e incrústala en una aplicación iOS nativa mediante un controlador de vista.
  2. Sustituye todos sus recursos por el sistema multiplataforma y anota cuánto tiempo te lleva la parte mecánica.
  3. Activa el lector de pantalla del sistema, recorre la pantalla entera y documenta cada elemento mal anunciado o mal ordenado.
  4. Prueba el deslizamiento desde el borde para retroceder, la selección de texto y el foco en un campo con teclado abierto; corrige lo que puedas y anota lo que no.
  5. Mide el tamaño del paquete y el tiempo hasta el primer fotograma antes y después, en un dispositivo de gama baja, y decide si el balance justifica la segunda pantalla.