Cuando empecé a diseñar la interfaz de Caladero, la app de pesca en la que trabajo actualmente, tuve que decidir entre Jetpack Compose y el sistema tradicional de layouts XML con vistas. No es una decisión trivial ni puramente técnica: afecta a cuánto tardas en construir cada pantalla, qué documentación y ejemplos vas a encontrar, y qué tan fácil es incorporar a otra persona al proyecto más adelante. Cuento aquí los criterios que usé, no para decir "Compose siempre gana", sino para explicar cómo razoné la decisión.
Qué es cada enfoque, en pocas palabras
XML con vistas es el sistema clásico de Android: defines la interfaz en archivos XML declarativos y la manipulas desde código con referencias a cada vista (findViewById o view binding). Es el sistema que ha sostenido Android desde su primera versión y sigue siendo perfectamente válido.
Jetpack Compose es el toolkit declarativo moderno de Google: la interfaz se describe con funciones Kotlin (@Composable) que reaccionan automáticamente a cambios de estado, sin necesidad de manipular vistas manualmente. Es el enfoque que Google recomienda actualmente para proyectos nuevos.
Por qué elegí Compose para un proyecto nuevo
Para un proyecto que empiezo desde cero hoy, Compose tiene ventajas concretas que pesaron en mi decisión:
Menos código repetitivo. Con vistas XML, cada cambio de estado en la interfaz obliga a escribir código imperativo que actualiza manualmente cada vista afectada. Con Compose, la función composable se vuelve a ejecutar automáticamente cuando cambia el estado del que depende, sin que yo tenga que escribir esa sincronización a mano.
Vista previa sin ejecutar la app. Las anotaciones @Preview permiten ver cómo queda una pantalla sin necesidad de compilar e instalar la app completa en un emulador, lo que acelera bastante el ciclo de iterar sobre el diseño de una pantalla concreta.
Dirección clara de Google. La documentación oficial, las librerías nuevas de Jetpack y buena parte de los ejemplos recientes se centran en Compose. Elegir XML hoy significa nadar cada vez más a contracorriente de dónde va el ecosistema.
Un ejemplo práctico: la pantalla de lista de puntos guardados
En Caladero tengo una pantalla que muestra la lista de puntos de pesca guardados por el usuario. Con Compose, el núcleo de esa pantalla se reduce aproximadamente a esto:
@Composable
fun PuntosScreen(viewModel: PuntosViewModel) {
val estado by viewModel.uiState.collectAsState()
LazyColumn {
items(estado.puntos, key = { it.id }) { punto ->
PuntoItem(
punto = punto,
onClick = { viewModel.seleccionarPunto(punto.id) }
)
}
}
}
Cuando estado.puntos cambia, por ejemplo porque el usuario añade un punto nuevo, la lista se actualiza sola. No tengo que llamar manualmente a notifyItemInserted como haría con un RecyclerView clásico en XML, ni gestionar un adaptador aparte. Esa reducción de código repetitivo es la que más noto en el día a día, sobre todo en pantallas con estados que cambian con frecuencia.
Cuándo todavía tendría sentido usar XML
No creo que XML sea una mala elección en todos los casos, y sería deshonesto presentarlo así:
- Proyectos existentes grandes con vistas XML. Migrar una app entera a Compose de golpe tiene un coste alto y un riesgo real de introducir regresiones. En esos casos tiene más sentido una migración gradual, pantalla a pantalla, o directamente no migrar si la app ya es estable y no está en desarrollo activo.
- Equipos con mucha experiencia acumulada en XML. Si un equipo lleva años dominando el sistema de vistas y las pantallas nuevas son pocas, el coste de aprendizaje de Compose puede no compensar a corto plazo, aunque a medio plazo probablemente sí.
- Dependencias de librerías de terceros no migradas. Algunas librerías de UI muy específicas todavía se apoyan en el sistema de vistas clásico y requieren capas de interoperabilidad (
AndroidView) para usarse dentro de Compose, lo que añade complejidad en casos concretos.
Errores y límites que he encontrado con Compose
No todo ha sido sencillo. El error más frecuente que cometí al empezar fue no entender bien cuándo se recompone una función y colocar cálculos costosos directamente en el cuerpo del composable, lo que provocaba que se repitieran innecesariamente en cada recomposición. La solución fue aprender a usar remember y derivedStateOf para evitar recalcular cosas que no dependen de los valores que cambian.
Otro límite real: la curva de aprendizaje de la gestión de estado en Compose (State, remember, rememberSaveable, elevación de estado hacia el ViewModel) es más pronunciada de lo que sugieren los tutoriales introductorios. Al principio es fácil terminar con estado duplicado entre el composable y el ViewModel, lo que genera bugs sutiles de sincronización. Me tomó varias iteraciones entender que el estado de la interfaz debería vivir, casi siempre, en el ViewModel, y que el composable debería limitarse a leerlo y a notificar eventos, sin guardar su propia copia salvo para estado puramente visual y efímero (como si un menú desplegable está abierto o cerrado).
También hay que tener en cuenta el rendimiento en listas muy largas o animaciones complejas, donde Compose ha mejorado mucho pero todavía requiere entender conceptos como estabilidad de parámetros (@Stable, @Immutable) para evitar recomposiciones innecesarias, algo que con vistas XML no existía como problema porque el modelo de actualización era completamente manual.
Conclusión
Para un proyecto Android nuevo, Compose es hoy la opción que recomendaría por defecto, y es la que uso en mis propios proyectos, incluido Caladero. Pero no lo elegiría solo porque sea "lo nuevo": lo elegí porque reduce código repetitivo real, acelera la iteración visual y sigue la dirección donde va la documentación y el ecosistema oficial de Android. Si ya tienes una app grande y estable en XML, la pregunta no es "Compose sí o no" en abstracto, sino si el coste de migrar compensa el beneficio en tu caso concreto, y muchas veces la respuesta honesta es que no, al menos no todavía.
