El desarrollo de aplicaciones multiplataforma ha evolucionado enormemente en los últimos años, y Kotlin Multiplatform junto con Compose Multiplatform están liderando esta transformación. Estas tecnologías permiten crear aplicaciones modernas, eficientes y nativas para múltiples plataformas desde un solo código base. Este artículo explora sus ventajas, compatibilidad y cómo comenzar.
¿Qué es Kotlin Multiplatform, Compose Multiplatform y por qué deberías usarlos? 🚀

Kotlin Multiplatform (KMP) es una tecnología que permite compartir lógica entre diferentes plataformas, como Android, iOS, escritorio y web, utilizando Kotlin como lenguaje principal. Por su parte, Compose Multiplatform extiende la funcionalidad de Jetpack Compose, el popular framework de interfaz de usuario para Android, al ámbito multiplataforma.
Diferencias clave con Jetpack Compose tradicional
Aunque Compose Multiplatform se basa en Jetpack Compose, hay diferencias importantes:
- Compatibilidad multiplataforma: Compose Multiplatform no se limita a Android. Soporta también iOS, escritorio y web.
- Temas y tokens: Los temas de colores y tipografía requieren definiciones específicas en Compose Multiplatform para garantizar la coherencia en todas las plataformas.
- Componentes adaptados: Algunos componentes, como Checkbox, tienen parámetros específicos según la plataforma.
Beneficios de Kotlin Multiplatform en el desarrollo UI
- Reutilización de código: Comparte lógica y componentes de UI entre plataformas.
- Flexibilidad nativa: Permite personalizar componentes para cada plataforma según sea necesario.
- Ecosistema Kotlin: Amplio soporte y herramientas como Kotlin/Native y Kotlin Multiplatform Mobile (KMM).
Ventajas de usar Compose Multiplatform 🧑💻
-
Desarrollo eficiente:
- Escribe una vez, ejecuta en múltiples plataformas.
- Comparte hasta el 80% del código entre Android, iOS y escritorio.
-
Experiencia de usuario consistente:
- Ofrece rendimiento y diseño nativos en cada plataforma.
- Soporte para temas personalizados y diseño responsivo.
-
Ecosistema Kotlin:
- Integra perfectamente con otras tecnologías de Kotlin.
- Documentación sólida y una comunidad activa.
Compatibilidad de Compose con diferentes plataformas 🦾

Desarrollo móvil: Android e iOS
Compose Multiplatform simplifica la creación de aplicaciones móviles para Android e iOS. Al reutilizar la lógica y los componentes visuales, puedes garantizar consistencia en el diseño y comportamiento en ambas plataformas, reduciendo significativamente el tiempo de desarrollo.
Desarrollo web y escritorio
Compose Multiplatform también es compatible con aplicaciones web y de escritorio:
- Aplicaciones web: Aunque el soporte para web está en evolución, Compose Multiplatform ofrece una solución viable para crear interfaces web modernas.
- Aplicaciones de escritorio: Con JetBrains Compose, las herramientas de escritorio pueden beneficiarse de interfaces modernas y fluidas.
Comparación con otros frameworks multiplataforma 📤
-
Compose vs Flutter:
- Flutter destaca por su rendimiento en UI personalizada, pero Compose Multiplatform permite una integración más profunda con componentes nativos.
-
Compose vs React Native:
- React Native utiliza JavaScript y es popular por su facilidad de uso, pero Compose Multiplatform ofrece mayor eficiencia y compatibilidad con el ecosistema nativo.
Ejemplos prácticos y casos de uso ☑️
Creación de una aplicación móvil multiplataforma
Con Kotlin Multiplatform y Compose Multiplatform, puedes configurar rápidamente un proyecto que comparta la lógica y los componentes de UI entre Android, iOS y escritorio. El flujo de trabajo se simplifica al dividir el proyecto en módulos comunes y específicos de cada plataforma. Para este ejemplo particular tomaremos composables y lógica previamente desarrollados del curso v . Aquí vas a ver como con unos sencillos pasos podemos reutilizar Compose para otras plataformas
Instalación y requisitos previos
- Android Studio: Es la IDE que utilizaremos al cual instalaremos el plugin de Kotlin Multiplatform
- Xcode: Es el IDE para desarrollo de Apps en el ecosistema Apple( Opcional, Si quieres usar la aplicación en un emulador iOS)
- Brew instalado en MacOS para la instalación de paquetes
- Conocimientos básicos de Kotlin y Jetpack Compose.
Paso 1: Instala el plugin Kotlin Multiplatform

Paso 2: Instalar kdoctor a través de brew. Al finalizar veras algo como esto:

Paso 3: Ejecuta kdoctor

Nota: Debemos resolver conflictos. Cada maquina es independiente de las cosas que sucedan y no hace parte del alcance del articulo explicar como resolverlos.

Paso 4: Kotlin Multiplatform Wizard
Debemos generar el proyecto multiplataforma a través de Kotlin Multiplatform Wizard y seleccionar las plataformas que queremos incluir: Android, iOS y Desktop. Selecciona descargar y extrae el .zip descargado a una carpeta.

Paso 6: Abrir proyecto.
Una vez abres el proyecto con Android Studio ubicando la carpeta(puede tomar unos minutos en estar listo). Vas a ver un readme que explica el propósito de cada carpeta.
This is a Kotlin Multiplatform project targeting Android, iOS, Desktop.
* `/composeApp` is for code that will be shared across your Compose Multiplatform applications.
It contains several subfolders:
- `commonMain` is for code that’s common for all targets.
- Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name.
For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app,
`iosMain` would be the right folder for such calls.
* `/iosApp` contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform,
you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project.
En este caso nos concentraremos en la carpeta commonMain donde colocaremos toda la lógica compartida entre UI y lógica de negocio. Si quisiéramos colocar algo especifico, por ejemplo en iOS deberíamos incluirlo en la carpeta iosMain.
Nota: Importante colocar la forma de visualizar el proyecto en modo Project y no Android para navegar de forma sencilla.

En este momento vamos a migrar los archivos del proyecto del curso Curso de Jetpack Compose en Android y los acomodaremos en esta estructura de carpetas.

Archivos capa data
//FakeTaskLocalDataSource.kt
object FakeTaskLocalDataSource: TaskLocalDataSource {
private val _tasksFlow = MutableStateFlow<List<Task>>(emptyList())
init {
_tasksFlow.value = completedTask + pendingTask
}
override val tasksFlow: Flow<List<Task>>
get() = _tasksFlow
override suspend fun addTask(task: Task) {
val tasks = _tasksFlow.value.toMutableList()
tasks.add(task)
delay(1000L)
_tasksFlow.value = tasks
}
override suspend fun updateTask(updatedTask: Task) {
val tasks = _tasksFlow.value.toMutableList()
val taskIndex = tasks.indexOfFirst { it.id == updatedTask.id }
if (taskIndex != -1) {
tasks[taskIndex] = updatedTask
delay(1000L)
_tasksFlow.value = tasks
}
}
override suspend fun removeTask(task: Task) {
val tasks = _tasksFlow.value.toMutableList()
tasks.remove(task)
delay(1000L)
_tasksFlow.value = tasks
}
override suspend fun deleteAllTasks() {
delay(1000L)
_tasksFlow.value = emptyList()
}
override suspend fun getTaskById(taskId: String): Task? {
delay(1000L)
return _tasksFlow.value.find { it.id == taskId }
}
}
Archivos capa domain
enum class Category {
WORK,
PERSONAL,
SHOPPING,
OTHER;
}
...
data class Task(
val id: String,
val title:String,
val description:String?,
val isCompleted:Boolean = false,
val category: Category? = null
)
interface TaskLocalDataSource{
val tasksFlow: Flow<List<Task>>
suspend fun addTask(task: Task)
suspend fun updateTask(updatedTask: Task)
suspend fun removeTask(task: Task)
suspend fun deleteAllTasks()
suspend fun getTaskById(taskId: String): Task?
}
val completedTask = mutableListOf<Task>()
.apply {
repeat(20){
add(
Task(
id = it.toString(),
title = "Task $it",
description = "Description $it",
category = WORK,
isCompleted = false
)
)
}
}
val pendingTask = mutableListOf<Task>()
.apply {
repeat(20){
add(
Task(
id = (it+30).toString(),
title = "Task $it",
description = "Description $it",
category = OTHER,
isCompleted = true
)
)
}
}
Archivos capa presentacion
@Composable
fun SectionTitle(
modifier: Modifier = Modifier,
title: String
) {
Box {
Text(
text = title,
style = MaterialTheme.typography.h6,
color = MaterialTheme.colors.onSurface,
modifier = modifier
.padding(8.dp)
)
}
}
...
@Composable
fun SummaryInfo(
modifier: Modifier = Modifier,
date: String = "March 9, 2024",
tasksSummary: String = "5 incomplete, 5 completed"
) {
Column (
modifier = modifier
.padding(16.dp)
){
Text(
text = date,
style = MaterialTheme.typography.h4,
color = MaterialTheme.colors.onBackground,
fontWeight = FontWeight.Bold
)
Text(
text = tasksSummary,
style = MaterialTheme.typography.h4,
color = MaterialTheme.colors.onSurface,
)
}
}
....
@Composable
fun TaskItem(
modifier: Modifier = Modifier,
onClickItem:(String) -> Unit,
onDeleteItem:(String) -> Unit,
onToggleCompletion:(Task) -> Unit,
task: Task,
) {
Row (
modifier = modifier
.clickable {
onClickItem(task.id)
}
.background(
color = MaterialTheme.colors.surface,
)
.padding(horizontal = 8.dp)
,
verticalAlignment = Alignment.CenterVertically
){
Checkbox(
checked = task.isCompleted,
onCheckedChange = {
onToggleCompletion(
task
)
},
)
Column (
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(
4.dp
),
modifier = Modifier.padding(
8.dp
).weight(
1f
)
){
Text(
text = task.title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.subtitle1.copy(
textDecoration = if(task.isCompleted) TextDecoration.LineThrough else TextDecoration.None
),
color = MaterialTheme.colors.primary
)
if(!task.isCompleted){
task.description?.let {
Text(
text = it,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onBackground
)
}
task.category?.let {
Text(
text = it.toString(),
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onBackground
)
}
}
}
Box {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete Task",
tint = MaterialTheme.colors.surface,
modifier = Modifier
.padding(8.dp)
.clickable {
onDeleteItem(task.id)
}
)
}
}
}
.......
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HomeScreen(
modifier: Modifier = Modifier,
) {
var isMenuExtended by remember { mutableStateOf(false) }
var state by remember { mutableStateOf(HomeDataState()) }
val coroutineScope = rememberCoroutineScope()
val fakeTaskLocalDataSource = FakeTaskLocalDataSource
LaunchedEffect(true){
fakeTaskLocalDataSource.tasksFlow.collect{
val completedTask = it.filter { task -> task.isCompleted }
val pendingTask = it.filter { task -> !task.isCompleted }
state = HomeDataState(
date = "March 9, 2024",
summary = "${completedTask.size} completed, ${pendingTask.size} pending",
completedTask = completedTask,
pendingTask = pendingTask
)
}
}
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = {
Text(
text = "Todo App",
color = MaterialTheme.colors.surface,
fontWeight = FontWeight.Bold
)
},
actions = {
Box (
modifier= Modifier.padding(8.dp).clickable {
isMenuExtended = true
}
){
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = "Add Task",
tint = MaterialTheme.colors.onSurface,
)
DropdownMenu(
expanded = isMenuExtended,
modifier = Modifier.background(
color = MaterialTheme.colors.surface
),
onDismissRequest = { isMenuExtended = false }
) {
DropdownMenuItem(
content ={
Text(
text = "delete all",
color = MaterialTheme.colors.onSurface
)
},
onClick = {
}
)
}
}
}
)
},
content = { paddingValues ->
LazyColumn (
modifier = Modifier.padding( paddingValues = paddingValues )
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(
8.dp
)
){
item {
SummaryInfo(
date = state.date,
tasksSummary = state.summary
)
}
stickyHeader{
SectionTitle(
modifier = Modifier
.fillParentMaxWidth()
.background(
color = MaterialTheme.colors.surface
),
title = "Completed tasks"
)
}
items(
items = state.completedTask,
key = { task -> task.id }
){ task ->
TaskItem(
modifier = Modifier
.clip(
RoundedCornerShape(8.dp)
)
.animateItem(),
task = task,
onClickItem = {
},
onDeleteItem = {
coroutineScope.launch {
fakeTaskLocalDataSource.removeTask(task)
}
},
onToggleCompletion = {
coroutineScope.launch {
fakeTaskLocalDataSource.updateTask(it.copy(isCompleted = !it.isCompleted))
}
}
)
}
stickyHeader{
SectionTitle(
modifier = Modifier.background(
color = MaterialTheme.colors.surface
).fillParentMaxWidth(),
title = "Pending tasks"
)
}
items(
items = state.pendingTask,
key = { task -> task.id }
){ task ->
TaskItem(
modifier = Modifier
.clip(
RoundedCornerShape(8.dp)
)
.animateItem(),
task = task,
onClickItem = { },
onDeleteItem = {
coroutineScope.launch {
fakeTaskLocalDataSource.removeTask(task)
}
},
onToggleCompletion = {
coroutineScope.launch {
fakeTaskLocalDataSource.updateTask(it.copy(isCompleted = !it.isCompleted))
}
}
)
}
}
},
floatingActionButton = {
FloatingActionButton(
onClick = { }
) {
Icon(imageVector = Icons.Default.Add, contentDescription = "Add Task")
}
}
)
}
data class HomeDataState(
val date:String = "",
val summary:String = "",
val completedTask:List<Task> = emptyList(),
val pendingTask:List<Task> = emptyList(),
)
Lo único que debemos hacer ahora es colocar nuestro composable principal en el archivo App.kt para que los demás plataformas puedan invocarlo de forma global al proyecto.
@Composable
@Preview
fun App() {
MaterialTheme {
HomeScreen()
}
}
Las diferencias principales de cambios que se tienen que hacer en comparación con el proyecto del curso de Android residieron simplemente en la clase de MaterialTheme, tuvimos que buscar tokens que se adaptaran de una mejor forma y cambios en componentes como el checkbox para traer la pantalla principal o HomeScreen.
Ya podemos instalar el proyecto en cada una de las plataformas.

Android

iOS

Desktop

Mejores prácticas para proyectos multiplataforma
- Organiza el proyecto en capas (domain, data y presentación).
- Comparte la mayor cantidad de lógica posible en el módulo común.
- Personaliza componentes específicos cuando sea necesario.
Futuro de Compose Multiplatform 📊
Compose Multiplatform está redefiniendo el desarrollo de aplicaciones multiplataforma al combinar la eficiencia del código compartido con el rendimiento de componentes nativos. Su flexibilidad y compatibilidad hacen que sea una opción ideal tanto para desarrolladores nuevos como experimentados.
Si buscas una solución moderna para crear aplicaciones multiplataforma, Compose Multiplatform es una apuesta segura así que te invito a aprender más de este tema en nuestro Curso de Jetpack Compose en Android
¿Listo para comenzar? ¡Nos vemos en el curso!
Curso de Jetpack Compose en Android