Tema 1: Introducción a Jetpack Compose
Objetivos de este tema
- Conocer Jetpack Compose y las diferencias con el sistema tradicional basado en vistas (XML).
- Identificar las ventajas de utilizar Jetpack Compose para el desarrollo de interfaces.
- Explorar y comprender la relación entre Kotlin y Jetpack Compose.
- Establecer la estructura básica de un proyecto Compose.
- Distinguir el flujo de trabajo del compilador y el runtime en la composición de interfaces.
- Construir interfaces básicas utilizando funciones Composables y layouts principales.
- Entender el proceso de recomposición y cómo se gestiona el estado en Compose.
1.1. Fundamentos de Compose
1.1.1. ¿Qué es Jetpack Compose?
Jetpack Compose es el framework moderno de Android que permite construir interfaces de usuario de forma declarativa. Diseñado para escribir UI de forma más intuitiva, menos propenso a errores y totalmente integrable con el lenguaje de programación Kotlin.
Se basa en tres ideas clave:
- UI declarativa: la idea es describir qué se quiere mostrar, en lugar de dibujarlo con XML.
- Reactividad: se actualiza automáticamente cuando los datos cambian.
- Menos código: no se utiliza XML ni
findViewById
.
1.1.2. Diferencias entre el sistema basado en vistas y Jetpack Compose
Sistema basado en vistas (XML) | Jetpack Compose |
---|---|
XML separado de lógica | Código unificado en Kotlin |
findViewById necesario o ViewBinding |
No requiere vinculación manual |
Inflado de vistas | Composición directa en runtime |
Mucho código | Sintaxis más concisa |
Acoplamiento más rígido | Modularidad y reutilización nativa |
1.1.3. Ventajas de Jetpack Compose
- Declarativo: se define la UI como función del estado, esto significa que la interfaz de usuario se crea a partir del estado de los datos actuales, si estos cambian la UI se actualiza.
- Menos código: elimina gran parte del boilerplate.
- Testing más sencillo: las funciones Composables pueden probarse directamente.
- Integración total con Kotlin.
- Mejora el rendimiento en muchos casos gracias a la recomposición eficiente.
- Animaciones fáciles de implementar.
- Migración progresiva: puede utilizarse junto con el sistema de vistas tradicional.
Ejemplo 1.1. UI reactiva con estado
Este ejemplo muestra un saludo reactivo. Al pulsar el botón, el estado (nombre) cambia y Compose actualiza automáticamente la interfaz, mostrando “Hola, Patricia” sin que haya que modificar directamente el Text
.
1import android.os.Bundle
2import androidx.activity.ComponentActivity
3import androidx.activity.compose.setContent
4import androidx.activity.enableEdgeToEdge
5import androidx.compose.foundation.layout.Arrangement
6import androidx.compose.foundation.layout.Column
7import androidx.compose.foundation.layout.Spacer
8import androidx.compose.foundation.layout.fillMaxSize
9import androidx.compose.foundation.layout.height
10import androidx.compose.foundation.layout.padding
11import androidx.compose.material3.Button
12import androidx.compose.material3.MaterialTheme
13import androidx.compose.material3.Text
14import androidx.compose.runtime.Composable
15import androidx.compose.runtime.getValue
16import androidx.compose.runtime.mutableStateOf
17import androidx.compose.runtime.remember
18import androidx.compose.runtime.setValue
19import androidx.compose.ui.Alignment
20import androidx.compose.ui.Modifier
21import androidx.compose.ui.unit.dp
22
23class MainActivity : ComponentActivity() {
24 override fun onCreate(savedInstanceState: Bundle?) {
25 super.onCreate(savedInstanceState)
26 enableEdgeToEdge()
27 setContent {
28 MaterialTheme {
29 SaludoInteractivo()
30 }
31 }
32 }
33}
34
35@Composable
36fun SaludoInteractivo() {
37 var nombre by remember { mutableStateOf("Javier") }
38 Column(
39 modifier = Modifier
40 .fillMaxSize()
41 .padding(16.dp),
42 verticalArrangement = Arrangement.Center, // Centra verticalmente
43 horizontalAlignment = Alignment.CenterHorizontally // Centra horizontalmente
44 ) {
45 Text(
46 text = "Hola, $nombre", style = MaterialTheme.typography.headlineMedium
47 )
48
49 Spacer(modifier = Modifier.height(8.dp))
50
51 Button(onClick = { nombre = "Patricia" }) {
52 Text(text = "Cambiar nombre")
53 }
54 }
55}
1.2. Arquitectura de Compose
1.2.1. Componentes clave: Runtime, UI, Compiler
Jetpack Compose se divide en tres módulos que trabajan conjuntamente y con tareas claramente definidas:
- Compose Compiler: plugin del compilador de Kotlin que transforma las funciones etiquetadas como
@Composable
en código optimizado y eficiente para ser ejecutado sobre la plataforma Android. Además de inyectar la lógica interna necesaria para hacer que la UI sea reactiva y eficiente. - Compose Runtime: el motor de ejecución que mantiene el estado, gestiona las recomposiciones y decide qué UI debe actualizarse. Es capaz de saber qué partes de la UI deben volver a generarse.
- Compose UI: contiene los elementos visuales como Text, Button, Row, Column, etc. Aquí está el trabajo directo del desarrollador para construir interfaces. A nivel interno trabaja con la librería gráfica de Android.
Estos tres módulos trabajan de forma desacoplada, lo que permite actualizar o extender cada parte por separado.
Modularidad y escalabilidad
Gracias a esta arquitectura desacoplada, Jetpack Compose es:
- Extensible: puedes crear tus propios elementos UI (
@Composable
personalizados) o incluso reemplazar partes del runtime si lo necesitas. - Testable: puedes probar el runtime o la UI por separado.
- Ligero y escalable: puedes incluir solo los módulos necesarios.
Ejemplo 1.2. Modularidad
El siguiente ejemplo permite añadir una TopAppBar
estableciendo el título de la aplicación a través del recurso String.
1import androidx.compose.material3.TopAppBar
2
3...
4
5@Composable
6fun MyAppTopAppBar(topAppBarText: String) {
7 TopAppBar(
8 title = {
9 Text(
10 text = topAppBarText,
11 textAlign = TextAlign.Left,
12 modifier = Modifier
13 .fillMaxSize()
14 .wrapContentSize(Alignment.CenterStart),
15 )
16 }
17 )
18}
Para mostrarla añade la llamada al método saludoInteractivo()
.
1@Composable
2fun SaludoInteractivo() {
3 var nombre by remember { mutableStateOf("Javier") }
4
5 myAppTopAppBar(stringResource(R.string.app_name))
6
7 Column(
8 ...
9 )
10 ...
11}
Debes tener en cuenta que esta no será la mejor manera de mostrar una TopAppBar
, pero de momento puede servir. Más adelante se verá el componente Scaffold
.
1.3. Trabajo del compilador (IR y transformaciones)
1.3.1. Funcionamiento del compilador en Compose
Cuando se escribe una función etiquetada con @Composable
, el compilador no la ejecuta tal cual. En lugar de eso, el plugin de Compose para Kotlin modifica esa función y añade el código adicional necesario para gestionar:
- El control de recomposición
- El seguimiento del estado
- La eficiencia en la actualización de la UI
1.3.2. Composable Compiler Plugin
- Se integra en el proceso de compilación de Kotlin.
- Convierte funciones
@Composable
en llamadas más complejas que pueden ser monitorizadas por el runtime. - Inyecta parámetros invisibles como el Composer, que rastrea si una función necesita recomponerse o no.
Oberseva el siguiente método:
1@Composable
2fun Saludo(nombre: String) {
3 Text("Hola, $nombre")
4}
El resultado del compilador será algo parecido al siguiente método:
1fun Saludo(nombre: String, composer: Composer, changed: Int) {
2 if (composer.shouldRecompose(changed)) {
3 Text("Hola, $nombre")
4 }
5}
Este código resultado no se verá, pero es el encargado del funcionamiento óptimo y eficiente para Compose.
1.3.3. Transformacón en el Intermediate Representation (IR)
El compilador de Compose trabaja en la fase de IR (Intermediate Representation) de Kotlin, dónde se realizarán transformaciones como:
- Inyección de lógica para recomposición condicional.
- División de Composables en múltiples fases si contienen múltiples niveles de recomposición.
- Manejo de claves y grupos para optimizar la reconstrucción de UI.
1.4. Conexión con el tiempo de ejecución
1.4.1. Cómo se interpretan y ejecutan los Composables
Cuando un método etiquetado como @Composable
se llama desde setContent
o desde otro Composable, no se dibujará directamente en pantalla, sino que entra en juego el runtime de Compose:
- Interpretará la estructura del árbol Composable.
- Evaluará si debe recomponer por cambios de estado.
- Utilizará el sistema de slots para decidir qué partes de la UI deberán redibujarse.
1.4.2. Sistema de slots y control de recomposición
El sistema de slots (Slot Table) es la estructura interna encargada de:
- Representar cada nodo del árbol de la UI (Text, Button, Column…).
- Registrar qué parte del árbol corresponde a qué parte del estado.
- Guardar el orden y la identidad de cada elemento para poder hacer una recomposición eficiente.
Podría verse como un índice dinámico del árbol de UI.
La recomposición se produce cuando un valor observado cambia (por ejemplo, una variable del tipo remember { mutableStateOf(...) }
), Compose marca los Composables afectados como sucios (dirty). En la siguiente fase del frame, solo esos Composables se vuelven a ejecutar.
El encargado de esto es el runtime, sin que el desarrollador tenga que intervenir manualmente.
¿Y cómo sabe Compose qué recomponer para ser eficiente?
- Cada método Composable recibe información sobre su “posición” en el árbol.
- El runtime asigna un grupo de recomposición a cada llamada Composable.
- Si el estado relevante cambia, solo ese grupo se vuelve a ejecutar.
Esto hace que Compose sea más eficiente que sistemas anteriores.
Ejemplo 1.4. Recomposición
1@Composable
2fun Contador() {
3 var valor by remember { mutableStateOf(0) }
4
5 Column {
6 Text("Valor: $valor")
7 Button(onClick = { valor++ }) {
8 Text("Incrementar")
9 }
10 }
11}
¿Qué ocurre en este ejemplo cuando se pulsa el botón?
- Únicamente se recompondrá
Text("Valor: $valor")
. - El botón permanece intacto.
- Todo esto es decidido por el runtime con ayuda de la Slot Table.
1.5. Introducción a la UI de Compose
1.5.1. Estructura básica de un @Composable
En Jetpack Compose, la UI se construye a partir de los métodos marcados con la anotación @Composable
. Deberás tener en cuenta que estos métodos:
- No devuelven nada (
Unit
). - Describen cómo se mostrará la interfaz.
- Se pueden ser anidar y reutilizar.
Un ejemplo básico:
1@Composable
2fun Saludo(nombre: String) {
3 Text(text = "Hola, $nombre")
4}
Para mostrar la composición de esta etiqueta (Text
), deberá llamarse desde dentro de setContent{}
en una Activity.
1setContent {
2 Saludo("Jetpack Compose")
3}
1.5.2. Layouts básicos en Compose
Jetpack Compose ofrece varios contenedores flexibles para la organización de los elementos de UI. Los tres layouts básicos son:
Colum
Este layout organiza los elementos verticalmente, uno debajo de otro.
1@Composable
2fun EjemploColumn() {
3 Column(modifier = Modifier.padding(16.dp)) {
4 Text("Primera línea")
5 Text("Segunda línea")
6 }
7}
Row
Este organiza los elementos horizontalmente, de izquierda a derecha.
1@Composable
2fun EjemploRow() {
3 Row(modifier = Modifier.padding(16.dp)) {
4 Text("Izquierda")
5 Spacer(modifier = Modifier.width(8.dp))
6 Text("Derecha")
7 }
8}
Box
Este layout superpone los elementos, estando siempre encima de todos el último.
1@Composable
2fun EjemploBox() {
3 Box(modifier = Modifier.size(100.dp)) {
4 Text("Fondo", modifier = Modifier.align(Alignment.Center))
5 Box(modifier = Modifier
6 .size(40.dp)
7 .background(Color.Red)
8 .align(Alignment.BottomEnd))
9 }
10}
Puedes probarlo en Android Studio y añadir justo antes de la etiqueta @Composable
la etiqueta @Preview(showBackground = true)
para ver una previsualización sin necesidad de ejecutar la aplicación.
1.6. Composición y recomposición
1.6.1. ¿Qué es la recomposición?
La recomposición de Jetpack Compose es el proceso por el cual el sistema vuelve a ejecutar funciones @Composable
con el fin de actualizar la interfaz de usuario (UI) como respuesta a cambios en el estado. Esto permite que la UI muestre siempre el estado actual de la aplicación.
Por ejemplo, si una variable de estado cambia, Compose identificará las partes de la UI que dependen de ese estado y volverá a ejecutar solo los métodos @Composable
para refrescar la pantalla.
1.6.2. Triggers de recomposición
La recomposición en Jetpack Compose se activa cuando:
- Se producen cambios en variables de estado: Al modificar una variable creada con
mutableStateOf
, Compose detecta el cambio y recompone los métodos que la utilizan. - Hay nuevos valores en parámetros de funciones
@Composable
: Al llamar a un método@Composable
con diferentes argumentos, se considera que su entrada ha cambiado y se recompone. - Cambios en claves de listas: Al modificar la clave de un elemento en una lista, Compose puede recomponer ese elemento específico.
Destacar que Compose optimiza este proceso, recomponiendo únicamente las partes necesarias de la UI. Por ejemplo, si una lista de elementos cambia en orden pero no en contenido, Compose puede evitar recomponer los elementos que no han cambiado.
1.7. Estado y diferenciación inteligente
1.7.1. Estado observable
En Jetpack Compose, el estado representa cualquier dato que puede cambiar y debe reflejarse en la UI. Cuando un estado cambia, Compose vuelve a ejecutar los métodos @Composable
que dependen de ese estado para actualizar la UI según corresponda.
Para gestionar el estado de una manera eficiente, Compose facilita varias APIs:
- remember: Almacena un valor en memoria durante la composición, útil para conservar el estado entre recomposiciones.
1val contador = remember { mutableStateOf(0) }
- mutableStateOf: Crea un objeto mutable que Compose observa, cuando cambia su valor se produce la recomposición.
1var texto by remember { mutableStateOf("") }
Declaración | Reactivo | Recomposición | Adecuado para estado UI |
---|---|---|---|
by remember { mutableStateOf(false) } |
✅ Sí | ✅ Automática | ✅ Sí |
remember { false } |
❌ No | ❌ No automática | ❌ No |
- derivedStateOf: Permite derivar un nuevo estado a partir de otros estados. Solo se actualizará cuando el resultado derivado cambie, lo que evita recomposiciones innecesarias.
1val esTextoLargo by remember {
2 derivedStateOf { texto.length > 10 }
3}
El uso de estas APIs pueden ayudar a optimizar la recomposición y evitar así, recomposiciones innecesarias.
Consejo, evitar operaciones complejas en métodos
@Composable
, los métodos@Composable
deben ser rápidos y sin efectos secundarios. Las operaciones intensivas deben realizarse fuera de estas funciones y sus resultados deben pasarse como parámetros.
1.7.2. Diferenciación y control inteligente
Compose optimiza las recomposiciones mediante un sistema de diferenciación inteligente. Lo que significa que solo las partes de la UI que dependen de un estado que ha cambiado se vuelven a componer.
Para aprovechar esta diferenciación:
-
Minimiza el alcance del estado: Define los estados en los niveles más bajos posible del árbol de Composables, limitando así las recomposiciones.
-
Evita operaciones costosas en composables: Realiza cálculos intensivos fuera de las funciones
@Composable
y pasa los resultados como parámetros. -
Usa
remember
yderivedStateOf
adecuadamente: Estas funciones ayudan a conservar valores y evitar recomposiciones innecesarias.
Ejemplo 1.7. Recomposición
1@Composable
2fun EjemploEstado() {
3 var texto by remember { mutableStateOf("") }
4 val esTextoLargo by remember {
5 derivedStateOf { texto.length > 10 }
6 }
7
8 Column(modifier = Modifier.padding(16.dp)) {
9 TextField(
10 value = texto,
11 onValueChange = { texto = it },
12 label = { Text("Ingrese texto") }
13 )
14 if (esTextoLargo) {
15 Text("El texto es largo")
16 }
17 }
18}
En este código de ejemplo, la variable esTextoLargo
se actualiza únicamente cuando la longitud de texto
cambie y supere la longitud establecida, evitando las recomposiciones innecesarias.
1.8. Naturaleza y propiedades de las funciones componibles
1.8.1. Reglas de los Composables
Las funciones @Composable
son la parte principal de Jetpack Compose. Estas funciones deberán cumplir ciertas reglas que garanticen una UI eficiente y predecible:
- Anotación obligatoria: Toda función que construya UI deberá estar anotada como
@Composable
. - No deben devolver valores: Como norma general, este tipo de funciones no devolverán ningún valor, deben describir como se mostrará la UI.
- Llamadas a otras composables: Pueden llamar a otras funciones
@Composable
para construir interfaces más complejas. - Sin efectos secundarios: Deben ser “puras”, es decir, no deben modificar el estado global ni realizar operaciones que afecten fuera del alcance del método.
Estas reglas básicas garantizan que Compose pueda realizar una gestión eficientemente de la recomposición y mantener una UI coherente.
1.8.2. Efectos secundarios y pureza
En el paradigma declarativo de Compose, que las funciones @Composable
sean puras es muy importante. Esto significa que, dadas las mismas entradas, siempre deben producir la misma salida sin causar efectos secundarios.
Si fuese necesario realizar operaciones que requieran efectos secundarios, Compose proporciona APIs específicas:
- LaunchedEffect: Ejecuta una operación de suspensión cuando una clave específica cambia.
1LaunchedEffect(key1 = clave) {
2 // Operación de suspensión
3}
-
rememberUpdatedState: Permite acceder al valor más reciente de una variable dentro de un efecto.
-
DisposableEffect: Realiza una operación cuando el Composable entra en la composición y limpia cuando sale.
Estas herramientas permiten manejar efectos secundarios de una manera controlada, manteniendo la integridad del sistema de composición.
1.8.3. Buenas prácticas
Para escribir funciones @Composable
de manera eficiente:
-
Mantén la pureza: No modifiques estados globales o realices operaciones que modifiquen elementos fuera del alcance de la función.
-
Descomposición en funciones pequeñas: Facilita la lectura y reutilización del código.
-
Evita recomposiciones innecesarias: Utiliza
remember
yderivedStateOf
para memorizar valores y evitar recomposiciones innecesarias. -
Utiliza las APIs adecuadas para manejar efectos secundarios: Como
LaunchedEffect
oDisposableEffect
.
Seguir estas prácticas garantiza una UI eficiente y un código fácil de mantener.
1.9. Estrategias de migración desde Views
1.9.1. Interoperabilidad entre Views y Compose
Jetpack Compose se diseño para coexistir con el sistema tradicional de vistas, basado en Views (XML), esto permite una migración progresiva y controlada.
1.9.2. AndroidView: Incluir Views en Compose
Es posible reutilizar componentes existentes basados en Views dentro de una interfaz construida con Compose, para ello se utiliza el Composable AndroidView
.
1@Composable
2fun VistaPersonalizada() {
3 AndroidView(
4 factory = { context ->
5 TextView(context).apply {
6 text = "Texto desde una View tradicional"
7 }
8 }
9 )
10}
Puede resultar útil cuando se necesita incorporar widgets personalizados, o bibliotecas que no tienen un equivalente en Compose.
1.9.3. ComposeView: Incluir Compose en layouts de Views
Por otra lado, existe la posibilidad de insertar contenido de Compose en una jerarquía de Views existente, para lo que se utilizará ComposeView.
Vista XML:
1<androidx.compose.ui.platform.ComposeView
2 android:id="@+id/compose_view"
3 android:layout_width="match_parent"
4 android:layout_height="wrap_content" />
Desde Kotlin:
1val composeView = findViewById<ComposeView>(R.id.compose_view)
2composeView.setContent {
3 Text("Contenido de Compose dentro de una View")
4}
De esta manera, es posible introducir nuevas funcionalidades haciendo uso de Compose sin reescribir código de las pantallas existentes.
1.9.4. Migración progresiva
Si te ves en la situación de ralizar una migración a Jetpack Compose, se recomienda realizarla de manera progresiva, permitiendo que Compose y Views coexistan en el mismo proyecto hasta que la aplicación esté completamente migrada.
- Construir nuevas pantallas con Compose: Desarrolla las nuevas funcionalidades directamente con Compose, aprovechando sus beneficios desde el inicio.
- Identificar componentes reutilizables: Crea bibliotecas de componentes UI comunes en Compose, fomentando así la reutilización y manteniendo una fuente única de verdad.
- Reemplazar pantallas existentes gradualmente: Migra las pantallas existentes una a una, comenzando por las más sencillas, o aquellas que requieran cambios, asegurando una transición controlada.
Este enfoque mantiene la estabilidad de la aplicación mientras se produce el cambio a la nueva tecnología.
Consideraciones adicionales
- Compatibilidad con temas y estilos: Asegúrarte que los temas definidos en Views son compatibles, o adaptados a Compose para mantener una apariencia coherente.
- Gestión del ciclo de vida: Ten en cuenta el ciclo de vida de los componentes al integrar Compose y Views, especialmente en actividades y fragments.
- Pruebas y depuración: Actualiza las pruebas existentes y considera nuevas estrategias de testing para componentes en Compose.
1.10. Ciclo de vida de una aplicación móvil
1.10.1. Ciclo de vida de una Activity
Las aplicaciones móviles en Android están sujetas a un ciclo de vida gestionado por el sistema operativo:
onCreate()
-> Se inicializan componentes y UI.onStart()
-> La Activity es visible, pero no interactúa con el usuario todavía.onResume()
-> La Activity ya está en primer plano y permite interacciones con el usuario.onPause()
-> Pierde el foco, momento en el que se puede guardar datos o pausar tareas.onStop()
-> La Activity ya no es visible y se liberan recursos pesados.onDestroy()
-> Se destruye la Activity y se limpian los recursos finales.
Conocer el ciclo de vida es vital para manejar recursos, permisos y situaciones como rotaciones, cambios de configuración o interrupciones.
1.10.2. Ciclo de vida de un Composable
El runtime de Compose dispone su propio ciclo de vida, este se compone de tres fases fundamentales:
- Enter the Composition: punto de inicio, es cuando la función @
Composable
se ejecuta por primera vez. - Recomposition: vuelve a ejecutarse si el estado cambia, actualizando solo aquello que es necesario.
- Leave the Composition: se elimina del árbol UI y se liberan los recursos asociados.
1.10.3. Relación entre ciclos
Aunque separados, estos ciclos interactúan entre sí en aplicaciones Compose:
- Las recomposiciones ocurren dentro del contexto de la Activity que controla la Composition.
- Si se destruye la Activity, se abandona la Composition y todos los efectos son cancelados(
DisposableEffect
,LaunchedEffect
, etc.). - Para reaccionar a eventos del ciclo de vida de la Activity dentro de Compose, se puede observar
Lifecycle
usando APIs comoLifecycleEventEffect
olifecycle.currentStateAsState()
del módulolifecycle-runtime-compose
.
Ejemplo práctico
1@Composable
2fun CiclosDeVida() {
3 // Se utiliza lifecycleOwner para observar el ciclo de vida de la actividad o fragmento.
4 val lifecycleOwner = LocalLifecycleOwner.current
5 // Se obtiene el estado actual del ciclo de vida como un estado Compose.
6 val estado = lifecycleOwner.lifecycle.currentStateAsState()
7
8 // Se muestra el estado actual del ciclo de vida.
9 Log.d("CiclosDeVida", "Estado del ciclo de vida: ${estado.value}")
10 Text(
11 text = "Estado del ciclo de vida: ${estado.value}",
12 modifier = Modifier.padding(16.dp)
13 )
14}
Una posible salida por el Logcat sería:
2025...-18617 CiclosDeVida es.javiercarrasco.examplet01b D Estado del ciclo de vida: RESUMED
2025...-18617 CiclosDeVida es.javiercarrasco.examplet01b D Estado del ciclo de vida: STARTED
2025...-18617 CiclosDeVida es.javiercarrasco.examplet01b D Estado del ciclo de vida: CREATED
2025...-18617 CiclosDeVida es.javiercarrasco.examplet01b D Estado del ciclo de vida: RESUMED
Fuentes
- Documentación Jetpack Compose
- Cómo pensar en Compose
- Crea la arquitectura de tu IU de Compose
- Complemento de Gradle para el compilador de Compose
- Compose Runtime
- Compose Layouts
- Ciclo de vida de los elementos componibles
- El estado y Jetpack Compose
- Cómo pensar en Compose
- Estrategia de migración
- Cómo usar objetos View en Compose
- Cómo integrar Lifecycle con Compose