Decidir: cuándo KMP compensa y cuándo salen más baratas dos apps nativas
La pregunta final de cualquier evaluación de Kotlin Multiplatform no es técnica sino económica, y se responde mal casi siempre porque se compara el coste de escribir con el coste de mantener. Esta lección construye la aritmética completa: el ahorro real por lógica no duplicada y por divergencia evitada, frente al coste de configuración, del puente con Swift, de la coordinación entre equipos y de la dependencia en herramientas de terceros. Presenta las señales que empujan en cada dirección, el papel decisivo de la composición del equipo, el criterio de adopción incremental por una funcionalidad acotada, y el análisis del coste de salida que convierte la decisión en reversible y por tanto en asumible.
Las cuatro lecciones anteriores describen lo que Kotlin Multiplatform permite hacer; esta responde a la única pregunta que importa después: si conviene hacerlo. Y conviene abordarla sabiendo que la respuesta honesta depende de variables que no son técnicas. La calidad de la herramienta está fuera de discusión y el reparto de capas es claro, de modo que el debate no se juega ahí: se juega en cuánta lógica tiene realmente tu producto, en si las dos aplicaciones se parecen o llevan años divergiendo, en si existe una persona capaz de sostener el puente con Swift cuando algo falle, y en si tus dos equipos pueden coordinarse sobre un repositorio común sin que la coordinación cueste más que la duplicación que elimina. Un análisis que ignore esas variables producirá una recomendación técnicamente impecable y organizativamente inviable.
- Construir la aritmética completa de la decisión, incluyendo los costes que no aparecen en las estimaciones iniciales.
- Identificar las señales de producto y de equipo que empujan hacia el código compartido y las que empujan hacia dos aplicaciones nativas.
- Diseñar una adopción incremental por una funcionalidad acotada que produzca evidencia antes que compromiso.
- Evaluar el coste de salida y convertir la decisión en reversible para poder tomarla con la información disponible.
La aritmética real
Antes de sumar nada conviene fijar la unidad de medida, porque casi todas las discusiones sobre este tema se libran comparando magnitudes distintas. No se trata de contar líneas ni ficheros: se trata de contar decisiones que hay que tomar dos veces y cambios que hay que aplicar dos veces. Una pantalla con doscientas líneas de composición y ninguna decisión es barata de duplicar; un motor de reglas de cincuenta líneas con doce casos límite es carísimo, porque cada uno de esos casos tuvo que descubrirse, discutirse, implementarse y probarse, y duplicarlo significa repetir las cuatro cosas.
El error de cálculo más común consiste en comparar el coste de escribir dos veces con el coste de escribir una, cuando lo que domina el ciclo de vida de un producto es el coste de mantener. Escribir la misma regla dos veces cuesta el doble una vez; mantenerla dos veces cuesta el doble cada semana durante años, y además introduce una probabilidad no despreciable de que las dos copias dejen de decir lo mismo sin que nadie se entere. Cualquier estimación que omita ese segundo término subestima el ahorro.
En la columna de los costes hay que ser igual de riguroso. Está la configuración inicial del proyecto, que hoy es cuestión de días y no de semanas. Está el puente con Swift, que es el coste recurrente principal y que se paga en cada tipo nuevo que cruza la frontera. Está el tiempo de compilación de los destinos nativos, que es notablemente peor que el de la máquina virtual. Está la dependencia en un ecosistema de bibliotecas más pequeño, que obliga a envolver a mano lo que no exista. Está la coordinación entre dos equipos que ahora comparten un artefacto. Y está, casi siempre olvidado, el coste de formación: alguien tiene que entender la jerarquía de conjuntos de fuentes, el puente y las herramientas, y ese alguien debe ser más de una persona para que el proyecto no dependa de un individuo.
Hay además un coste que no es de ingeniería y que conviene poner por escrito antes de que aparezca solo: el de la percepción. Para el equipo de Android, compartir código es escribir Kotlin como siempre en una carpeta distinta. Para el de iOS, es aceptar que una parte de su aplicación se escribe en un lenguaje que no eligió, en un repositorio que quizá no controla, y depurar a través de un puente que no domina. Esa asimetría es real, no es un prejuicio, y un plan de adopción que no la reconozca explícitamente encontrará resistencia que luego se interpretará como falta de colaboración cuando en realidad es una respuesta racional a un reparto desigual del coste.
Un umbral empírico razonablemente robusto: si la lógica compartible —dominio, datos, contenedores— representa menos de un tercio del esfuerzo de tu producto, el enfoque difícilmente compensa, porque el coste fijo del puente se reparte entre muy poca ganancia. Si supera el sesenta por ciento, es difícil justificar la duplicación. En la banda intermedia la decisión la determinan factores de equipo, no de código.
Nótese que varios de esos costes decrecen con el tiempo mientras otros no. La configuración se paga una vez; la formación, una vez por persona; el puente, en cambio, se paga con cada tipo nuevo que cruza, de modo que su coste es proporcional al tamaño de la superficie pública y no al tamaño del proyecto. Esa distinción tiene una consecuencia práctica directa: el mejor ahorro disponible no está en compartir más, está en exponer menos.
Merece un apunte el horizonte temporal, porque cambia el signo del resultado. El coste del enfoque es casi todo inicial —configuración, puente, formación, primeras fricciones— mientras que el beneficio es casi todo recurrente y crece con el número de cambios que el producto reciba. En un proyecto de seis meses el enfoque casi nunca sale a cuenta, sencillamente porque no llega a amortizar su coste fijo. En uno con vocación de durar cinco años, la comparación se invierte y lo que pesa es cuántas veces habrá que tocar cada regla de negocio a lo largo de esa vida.
Las señales, en las dos direcciones
Conviene enumerar las señales sin ponderarlas todavía, porque su peso depende del producto, y luego contarlas honestamente en ambas columnas antes de decidir.
A favor: lógica pesada
Reglas de negocio densas, cálculos financieros, sincronización con resolución de conflictos, trabajo sin conexión, algoritmos que deben dar el mismo resultado en los dos sistemas.
A favor: paridad exigida
El producto promete la misma funcionalidad en las dos plataformas y sufre cada vez que una se adelanta a la otra. La divergencia es aquí un defecto, no una libertad.
En contra: producto de interfaz
Aplicaciones donde la lógica es delgada y el valor está en la presentación, la animación y la integración profunda con el sistema. Hay poco que compartir y mucho que adaptar.
En contra: equipos separados
Dos equipos que no comparten repositorio, ni revisiones, ni calendario de entregas. El código compartido exige una coordinación que no existe y que nadie ha presupuestado.
Conviene resistir la tentación de convertir esta lista en una puntuación. Las señales no pesan lo mismo ni son independientes entre sí: una plantilla dividida en dos equipos sin comunicación anula por sí sola cualquier ventaja que aporten las tres restantes, mientras que una lógica de negocio verdaderamente densa puede justificar el enfoque aunque el resto de indicadores sean tibios. Las tablas de puntuación tienen la virtud de parecer objetivas y el defecto de esconder qué factor está decidiendo realmente.
Hay dos señales adicionales que rara vez se citan y que en la práctica deciden más que las anteriores. La primera es si existe en el equipo de iOS al menos una persona dispuesta a leer Kotlin: sin ella, cada fallo en la frontera se convierte en una petición a otro equipo y el enfoque se percibe como una imposición. La segunda es la madurez del producto: adoptar el enfoque en un producto que aún busca su forma es barato, porque hay poco que migrar; adoptarlo en uno con seis años de historia en dos bases de código maduras significa una migración larga con beneficio diferido.
La forma sensata de decidir no es un documento comparativo sino un experimento acotado. Elige una funcionalidad con lógica real y poca superficie de interfaz —autenticación, un motor de precios, la sincronización de una lista—, impleméntala en el módulo compartido y consúmela desde las dos aplicaciones existentes. En cuatro semanas tendrás datos reales sobre el puente, sobre el tiempo de compilación y, sobre todo, sobre cómo reacciona el equipo de iOS. Esa evidencia vale más que cualquier estimación.
Lo que hace viable ese experimento es que la adopción no exige reestructurar nada: el módulo compartido se añade como una dependencia más a las dos aplicaciones que ya existen, sin tocar su arquitectura ni su forma de construirse. Ese detalle es el que convierte la evaluación en barata y por tanto en realizable.
// El modulo compartido entra como una dependencia mas
// build.gradle.kts de la app de Android
dependencies {
implementation(project(":compartido"))
}
// Y como un framework para el proyecto de Xcode
kotlin {
listOf(iosArm64(), iosSimulatorArm64()).forEach { destino ->
destino.binaries.framework {
baseName = "Compartido"
isStatic = true
}
}
}
El experimento solo sirve si se decide de antemano qué lo declara fallido, porque de lo contrario cualquier resultado se interpretará según la preferencia previa de quien lo lea. Tres métricas bastan y conviene fijarlas por escrito antes de escribir la primera línea: el tiempo que tarda el equipo de iOS en integrar un cambio del módulo compartido sin ayuda del equipo de Android, el incremento del tiempo de construcción, y el número de defectos atribuibles a la frontera. Si las tres se mantienen dentro de lo pactado, hay evidencia para extender el enfoque; si no, hay evidencia para pararlo, que es igual de valioso y bastante más barato que descubrirlo dos años después.
flowchart TB Q1[La logica compartible supera un tercio del esfuerzo] -->|no| N[Dos apps nativas] Q1 -->|si| Q2[Hay alguien en iOS dispuesto a leer Kotlin] Q2 -->|no| N Q2 -->|si| Q3[Los equipos pueden coordinar entregas] Q3 -->|no| N Q3 -->|si| E[Experimento acotado de cuatro semanas] E --> D[Decidir con las tres metricas] style E fill:#a6e3a1,color:#11111b style N fill:#f38ba8,color:#11111b style D fill:#89b4fa,color:#11111b
Tres escenarios y su veredicto
Los criterios abstractos se entienden mejor aplicados, así que conviene recorrer tres casos que aparecen una y otra vez y que se resuelven de forma distinta pese a parecerse en el enunciado.
Conviene leerlos buscando el propio, pero también buscando en qué se diferencia el propio de cada uno, porque las diferencias suelen ser más informativas que los parecidos.
El primero es una aplicación de banca o de seguros con siete años de vida, reglas de cálculo densas, requisitos regulatorios idénticos en ambas plataformas y un historial documentado de incidencias por divergencia entre las dos implementaciones. Aquí el veredicto es claro y el único debate razonable es sobre el orden de la migración: se empieza por el motor de cálculo, que es donde más duele la divergencia y donde menos superficie cruza la frontera.
El segundo es una aplicación social o de contenido, con lógica delgada, mucho trabajo de animación, integración profunda con las funciones del sistema y equipos que compiten por adelantarse en el uso de las novedades de cada plataforma. Aquí el enfoque casi nunca compensa: lo poco que hay que compartir cabe en un cliente de red, y el coste del puente se llevaría por delante el ahorro. Dos aplicaciones nativas son más baratas y además más rápidas de evolucionar.
Falta un cuarto escenario que rara vez se menciona y que es más común de lo que parece: la empresa que adopta el enfoque no para ahorrar sino para forzar una arquitectura que llevaba años intentando imponer sin éxito. Funciona, porque el compilador consigue lo que ningún documento de normas había conseguido. Pero conviene ser consciente de que en ese caso el objetivo real no es compartir código, y por tanto las métricas de éxito tampoco deberían ser las de compartir código.
El tercero es el más frecuente y el más difícil: una empresa de tamaño medio con una aplicación de producto, lógica moderada, dos equipos pequeños y una hoja de ruta compartida. Aquí ninguna cuenta decide, porque los números caen en la banda intermedia, y la variable determinante pasa a ser la relación entre los dos equipos. Si trabajan juntos, el enfoque los hará más rápidos; si no se hablan, les creará un punto de fricción semanal. Ese diagnóstico no lo produce un análisis de código, y por eso este caso se decide con el experimento y no con una hoja de cálculo.
El coste de salida
Casi todo el miedo que rodea a esta decisión procede de una intuición razonable: que adoptar una tecnología transversal es entrar en un camino sin retorno. Merece la pena examinar esa intuición en vez de aceptarla, porque en este caso concreto no se sostiene.
Ninguna decisión de arquitectura debería tomarse sin conocer su coste de reversión, y aquí es donde el enfoque sale sorprendentemente bien parado. Si dentro de un año concluyes que no compensa, lo que has escrito es Kotlin ordinario: sigue siendo válido para Android sin tocar una línea, y lo que hay que rehacer es la implementación de iOS, que es exactamente la que habrías escrito de todos modos si nunca hubieras compartido nada. No hay formato propietario, ni lenguaje intermedio, ni tiempo de ejecución del que dependas para siempre.
Conviene aun así cuantificar la salida en vez de darla por barata sin mirar. Lo que hay que rehacer es la implementación de iOS de todo lo que estuviera compartido, más el trabajo de desmontar el puente y devolver el módulo a un proyecto exclusivo de Android. Lo que no hay que rehacer es el diseño: las decisiones sobre la forma del estado, sobre los límites del dominio y sobre el reparto de responsabilidades siguen siendo válidas y sirven de especificación para la reescritura. Migrar con una especificación funcionando delante es un ejercicio radicalmente distinto de migrar a ciegas.
Esa asimetría cambia el carácter de la decisión. Una elección irreversible exige certeza antes de actuar; una reversible exige solo una hipótesis razonable y un plan para comprobarla. Comparado con las alternativas históricas de código compartido en móvil, donde abandonar el enfoque implicaba reescribir la aplicación entera en los dos sistemas, aquí el escenario de fracaso deja intacta la mitad del trabajo. Eso no es un argumento a favor de adoptarlo sin pensar: es un argumento a favor de decidirlo con un experimento en vez de con un comité.
Hay, eso sí, una forma de convertir la decisión en irreversible sin darse cuenta, y consiste en dejar que el módulo compartido crezca hacia arriba. Mientras contenga dominio, datos y contenedores, la salida es la que se ha descrito. Si además acaba conteniendo navegación, presentación, componentes y toda la interfaz de iOS, el coste de salida deja de ser una reescritura acotada y pasa a ser la reescritura entera. La reversibilidad, como casi todo en arquitectura, no es una propiedad que se tenga: es una propiedad que se mantiene o se pierde con cada decisión que amplía la frontera.
No al final, cuando ya duela: el primer día, cuando todavía es barato pensarlo con frialdad. Un párrafo basta: qué habría que reescribir, quién lo haría, cuánto tiempo llevaría y qué señal concreta activaría esa decisión. Escribirlo tiene dos efectos: convierte una discusión emocional futura en un procedimiento acordado, y obliga al equipo a vigilar que el módulo compartido no crezca más allá de lo que el plan de salida contemplaba.
Casi todas las evaluaciones de este tipo se escriben como si la respuesta estuviera en las propiedades de la tecnología, y por eso casi todas fallan. Compartir código no es primariamente una decisión técnica: es una decisión sobre acoplamiento organizativo disfrazada de decisión de arquitectura. En el momento en que dos aplicaciones dependen del mismo contenedor, los dos equipos que las mantienen dejan de ser independientes: comparten ritmo de entrega, comparten la superficie de un fallo, comparten el coste de cualquier cambio en la forma del estado y comparten la conversación necesaria para cambiarla. Si esa dependencia refleja una realidad que ya existía —un solo producto, una sola hoja de ruta, un solo responsable— el código compartido no crea acoplamiento, se limita a hacer visible y verificable el que ya había, y entonces se percibe como un alivio. Si, en cambio, los dos equipos operaban con autonomía real, con prioridades y calendarios distintos, el código compartido introduce un acoplamiento nuevo que la organización no había aceptado, y ninguna cantidad de elegancia técnica compensará la fricción diaria que produce. De ahí que la misma decisión sea acertada en una empresa y desastrosa en otra con el mismo producto y el mismo tamaño: lo que difiere no es la aplicación, es la estructura de quien la mantiene. La versión madura de esta pregunta no es entonces cuánto código puedo compartir, sino cuánta coordinación estoy dispuesto a sostener durante los próximos tres años, y ese número no lo estima un arquitecto mirando un repositorio: lo estima quien conoce cómo se hablan de verdad los equipos cuando algo se rompe un viernes por la tarde.
- Mide, en tu producto real, qué porcentaje del esfuerzo corresponde a lógica compartible y qué porcentaje a presentación, y contrástalo con la regla del umbral.
- Cuenta las señales de las dos columnas para tu caso concreto y escribe cuál pesa más y por qué, sin empatar.
- Diseña el experimento acotado: elige la funcionalidad, define el plazo y fija de antemano las tres métricas que decidirán el resultado.
- Entrevista a alguien del equipo de iOS y anota literalmente su objeción principal; evalúa si es técnica, organizativa o de percepción.
- Escribe el plan de salida completo: qué habría que rehacer, cuánto costaría y qué hito del proyecto marcaría el punto sin retorno.