Tema 1: Introducción a Jetpack Compose

Diapositivas

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:

  1. 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.
  2. 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.
  3. 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 @Composableen 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. Transformació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.

Imagen punto 1.3 Imagen punto 1.3

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:

  1. Interpretará la estructura del árbol Composable.
  2. Evaluará si debe recomponer por cambios de estado.
  3. 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.

Imagen punto 1.4 Imagen punto 1.4

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.

Imagen punto 1.5 Imagen punto 1.5

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 bandera = remember { false }
  • 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

Evita 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.

Ejemplo práctico

Ejemplo práctico 1 Estado observable

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 y derivedStateOf adecuadamente: Estas funciones ayudan a conservar valores y evitar recomposiciones innecesarias.

Ejemplo 1.7. Recomposición

 1@Composable
 2fun EjemploEstado(paddingValues: PaddingValues) {
 3    var texto by remember { mutableStateOf("") }
 4    val esTextoLargo by remember {
 5        derivedStateOf { texto.length > 10 }
 6    }
 7
 8    Column(modifier = Modifier.padding(paddingValues)) {
 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.

Imagen punto 1.8 Imagen punto 1.8

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 y derivedStateOf para memorizar valores y evitar recomposiciones innecesarias.

  • Utiliza las APIs adecuadas para manejar efectos secundarios: Como LaunchedEffect o DisposableEffect.

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.

  1. Construir nuevas pantallas con Compose: Desarrolla las nuevas funcionalidades directamente con Compose, aprovechando sus beneficios desde el inicio.
  2. Identificar componentes reutilizables: Crea bibliotecas de componentes UI comunes en Compose, fomentando así la reutilización y manteniendo una fuente única de verdad.
  3. 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:

  1. Enter the Composition: punto de inicio, es cuando la función @Composable se ejecuta por primera vez.
  2. Recomposition: vuelve a ejecutarse si el estado cambia, actualizando solo aquello que es necesario.
  3. Leave the Composition: se elimina del árbol UI y se liberan los recursos asociados.

Imagen punto 1.10 Imagen punto 1.10

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 como LifecycleEventEffect o lifecycle.currentStateAsState() del módulo lifecycle-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


Autor/a: Javier Carrasco Última modificación: 06/10/2025

Subsecciones de Tema 1: Introducción a Jetpack Compose

Instalación y configuración de Android Studio

Instalación de Android Studio

La documentación de Google para el desarrollo de aplicaciones Android es muy completa y de fácil consulta. Se comenzará por centrar la atención en la instalación de Android Studio1, desde este enlace, desde donde podrás descargar la última versión.

Las aplicaciones nativas para Android se desarrollan utilizando como lenguaje de programación Java, o Kotlin. Por tanto, será necesario tener instalado en el SO el software necesario para la ejecución de Java. Se hace referencia a la máquina virtual de Java (JVM) y su entorno de ejecución de Java (JRE). Puedes descargarlo desde la página de recursos técnicos de Oracle.

Una vez instalado Java en el ordenador, el siguiente paso será descargar Android Studio para el sistema operativo con el que se vaya a trabajar.

Imagen sección T1-01 1 Imagen sección T1-01 1

Una vez hayas descargado el instalador, ya puedes dar comienzo a la instalación del IDE en el sistema operativo. Como podrás ver a continuación, se trata de una instalación clásica guiada a través del asistente de instalación. Se han omitido algunos pasos del asistente, como el tipo de instalación, que suele seleccionarse standard, o la elección del tema, oscuro o claro.

Imagen sección T1-01 2 Imagen sección T1-01 2 Imagen sección T1-01 3 Imagen sección T1-01 3
Imagen sección T1-01 4 Imagen sección T1-01 4 Imagen sección T1-01 5 Imagen sección T1-01 5
Imagen sección T1-01 6 Imagen sección T1-01 6 Imagen sección T1-01 7 Imagen sección T1-01 7
Nota

Las imágenes mostradas en este apartado pueden no coincidir con las que se muestran en el proceso de instalación, ya que Google actualiza con frecuencia el asistente de instalación de Android Studio. Si tienes alguna duda durante el proceso de instalación, puedes consultar la documentación oficial.

Una vez finalizada la instalación, ya puedes ejecutar el IDE. En primer lugar podrás ver en la parte izquierda las opciones Projects, para crear y gestionar los proyectos que se vayan generando. La opción Customize, esta permite acceder de manera rápida a opciones visuales y de accesibilidad, pero también encontrarás el acceso a todas las opciones de configuración del IDE.

Imagen sección T1-01 8 Imagen sección T1-01 8

La opción Plugins, desde la cual pueden añadirse y eliminarse plugins al entorno de desarrollo.

Imagen sección T1-01 9 Imagen sección T1-01 9

Y por último Learn Android Studio, donde encontrarás enlaces a documentación, ejemplos y consejos sobre el IDE.

Imagen sección T1-01 10 Imagen sección T1-01 10

Android SDK

Ahora, se mostrará como añadir SDKs (Software Development Kit) de Android según se necesite. Tras la instalación, vendrá instalada la última versión disponible, pero si investigas un poco, verás que la última versión, no es siempre la más utilizada en el caso de Android. Desde Projects, selecciona la opción SDK Manager. Si la pantalla no aparece como la imagen siguiente, si no tienes proyectos creados, la encontrarás justo en el centro, en la opción More Actions.

Imagen sección T1-01 11 Imagen sección T1-01 11

De momento no es necesario añadir ningún API al IDE, se utilizará la última que contiene todo lo necesario para poder trabajar.

Imagen sección T1-01 12 Imagen sección T1-01 12

Si se necesitase añadir una API anterior, bastará con seleccionar la API deseada y pulsar el botón Apply, tras lo que se mostrará un mensaje informando de la descarga e instalación que va a producirse, deberás pulsar el botón OK para comenzar.

Imagen sección T1-01 13 Imagen sección T1-01 13

Seguidamente se deberá aceptar el contrato de licencia y pulsar el botón Next para comenzar con el proceso. A partir de aquí, Android Studio comenzará su trabajo, descargará e instalará los componentes seleccionados.

Imagen sección T1-01 14 Imagen sección T1-01 14 Imagen sección T1-01 15 Imagen sección T1-01 15

Una vez terminada la instalación, pulsa el botón Finish, seguidamente verás que aparecen marcadas las versiones instaladas. Ya podrías continuar preparando el entorno de trabajo.


Autor/a: Javier Carrasco Última modificación: 22/09/2025

Dispositivos Virtuales para pruebas

Antes de comenzar a desarrollar, se terminará de configurar el entorno de desarrollo. Las pruebas, como bien sabrás, son una parte muy importante durante el desarrollo, es necesario saber si lo que se está haciendo funciona o no.

Se centrará la atención en el emulador AVD que incorpora Android Studio y en los dispositivos físicos. Existen otras alternativas en caso de no poder utilizar ninguno de estos dos medios, como las siguientes:

Como último recurso para realizar pruebas, se puede generar un archivo de instalación de Android, fichero del tipo .apk, y enviarlo al dispositivo, esto se mostrará más adelante.

Emulador AVD

Como ya se ha comentado, AVD (Android Virtual Device) es emulador que viene por defecto con Android Studio, este permitirá simular características físicas de dispositivos reales, ya bien sean teléfonos, tablets, incluso dispositivos Wear OS, Android TV, etc.

La recomendación de Google es crear un AVD por cada una de las imágenes disponibles del sistema, pero en este caso, con una nos bastará, quiero hacer hincapié en el espacio en disco, ya que si además de tener instaladas varias APIs, debe crearse una imagen AVD por cada una …

Para acceder al administrador AVD, se seguirán los mismos pasos que los utilizados para acceder al SDK Manager, pero esta vez, selecciona la opción Virtual Device Manager.

Imagen sección T1-01 1 Imagen sección T1-01 1

Una vez dentro, se mostraría el listado de los AVDs creados, en este caso al tratarse del primero, ese listado estará vacío y la única opción disponibles será Create Virtual Device… En versiones anteriores, en la parte inferior podías encontrar un enlace a Android Dashboards, donde podías encontrar estadísticas sobre los dispositivos utilizados, tamaños de pantalla, dispositivos que utilizan OpenGL, etc.

Imagen sección T1-01 2 Imagen sección T1-01 2

Una vez pulsada la opción Create Virtual Device, o el botón Create Device, se pasará a la selección del dispositivo, Category, en este caso se seleccionará Phone, y se seleccionará un modelo de la lista del centro, encontrarás modelos ofrecidos por Google (los Pixel), o modelos genéricos que encontrarás en la parte baja de la lista.

Imagen sección T1-01 3 Imagen sección T1-01 3

Se recomienda utilizar un model Pixel con Servicios de Google Play, estos, al igual que en los dispositivos reales, permiten hacer uso de funciones avanzadas que como desarrollador, facilitan el uso de ciertos servicios, como el localización, Places, Firebase, etc. Su uso también hará que se necesiten permisos de administrador para realizar ciertas modificaciones sobre el dispositivo emulado.

Imagen sección T1-01 4 Imagen sección T1-01 4

Nota

Se recomienda utilizar una imagen con Google Play y ABI x86_64, ya que ofrecen un mejor rendimiento. Si no se dispones de un equipo con procesador Intel o AMD, se puede utilizar una imagen ARM, pero el rendimiento será mucho peor.

El siguiente paso será seleccionar la imagen del sistema, al tratarse de la primera, verás que todas requieren descarga. Según el momento de instalación, la API a seleccionar puede variar. Se recomienda utilizar, para el curso, no la última, pero que esté al menos dos niveles por debajo, de esta forma se trabajará en un término medio. Deberás descargarla para continuar.

Imagen sección T1-01 5 Imagen sección T1-01 5

El siguiente paso será indicar el nombre del dispositivo, recomiendo indicar información de la API que tiene instalado para facilitar la elección, no será el caso ahora, pero se suelen tener más de dos o tres AVDs creados para las pruebas.

Una opción que puede venir bien es Enable Device Frame, por defecto viene activada, pero se recomienda desactivarla para aligerar levemente el uso del emulador, sobretodo si tu equipo no tiene suficiente memoria.

Imagen sección T1-01 6 Imagen sección T1-01 6

Una vez finalizado el proceso, el dispositivo aparecerá en el listado del Device Manager, ya solo faltará lanzarlo, utilizando el botón play que aparece en la columna Actions, para ver que funciona correctamente. Tras unos segundos de tensa espera, deberá cargarse el dispositivo en el emulador.

Imagen sección T1-01 7 Imagen sección T1-01 7 Imagen sección T1-01 8 Imagen sección T1-01 8

Una vez cargado, es posible que comience a actualizar aplicaciones como si de un dispositivo físico se tratase, aprovecha el momento para configurar el idioma, la cuenta de Google (opcional), etc.

Información

Si el emulador no se inicia, puede ser por varios motivos, los más comunes son:

  • No tener activada la virtualización en la BIOS/UEFI.
  • No tener instalado el Intel HAXM (Intel Hardware Accelerated Execution Manager) o el Hypervisor Framework en Mac.
  • No tener suficiente memoria RAM disponible.
  • Problemas con los controladores de la tarjeta gráfica.
  • Problemas con la versión de Android Studio o del SDK.
  • Problemas con el antivirus o firewall.
  • Problemas con la configuración del emulador.
  • Problemas con el hardware del equipo. Para solucionar estos problemas, se recomienda revisar la configuración del equipo, actualizar los controladores, desactivar el antivirus o firewall, y revisar la configuración del emulador. También se puede consultar la documentación oficial de Android Studio o buscar ayuda en foros especializados.
Consejo

Es recomendable asignar al menos 2 GB de RAM al emulador para un rendimiento óptimo. Si el equipo tiene poca memoria, se puede asignar menos, pero el rendimiento será peor.

Nota

Se recomienda tener al menos dos o más AVDs creados, al menos uno con una versión baja de Android (API 29) y otro con una versión alta (API 30 o superior), para poder probar las aplicaciones en diferentes versiones del sistema operativo, y evitar problemas que pueden surgir por la emulación.


Autor/a: Javier Carrasco Última modificación: 22/09/2025

Dispositivos Reales para pruebas

Otra opción para realizar las pruebas es utilizar un dispositivo físico, un teléfono o una tablet, que tenga instalado Android. Esta opción es la más recomendable, ya que se podrá probar la aplicación en un entorno real, con sus limitaciones y características propias.

Los dispositivos reales ya son otra historia, por un lado, ayuda ver tu aplicación funcionando en un dispositivo real, parece más profesional, además, te permite liberar memoria en el equipo al no tener que cargar un AVD.

Si utilizas macOS, no tienes que hacer nada, al menos nunca se me ha planteado lo contrario, pinchas el dispositivo, AS lo reconoce y a funcionar. Eso sí, necesitarás activar el modo desarrollador en tu dispositivo.

Si no es tu caso, y trabajas con Windows, debes instalar el driver USB, en primer lugar prueba con el Google USB Driver que puedes instalar desde el gestor Android SDK, el mismo que se utilizó para añadir las APIs, seguramente tendrás que reiniciar el sistema.

Imagen sección T1-03 1 Imagen sección T1-03 1

El driver de Google ha mejorado bastante y, salvo que tu dispositivo sea muy muy actual, no deberías tener problemas.

Aviso

Si no funciona, prueba a buscar el driver específico para tu dispositivo, normalmente en la web del fabricante, o en foros especializados, para obtener el ADB driver para tu sistema operativo. Por ejemplo, para un móvil Samsung.

Activar el modo desarrollador

Si decides hacer uso de un dispositivo móvil, deberás activar el modo desarrollador para realizar pruebas y acceder a otras funciones, como la depuración, capturar pantalla e incluso grabarla.

Para acceder a las opciones para desarrolladores en un dispositivo Android, en primer lugar dirígete a los ajustes del dispositivo, busca la información del dispositivo y localiza el número de compilación. Una vez localizado el número de compilación, deberás pulsar sobre él siete veces, verás que aparece un mensaje indicando que las opciones para desarrollador están activadas.

Imagen sección T1-03 2 Imagen sección T1-03 2

Ahora, en la sección Sistema, en las opciones avanzadas deberás buscar Opciones para desarrolladores y activar Depuración por USB.

Imagen sección T1-03 3 Imagen sección T1-03 3

El dispositivo ya estaría listo para realizar las pruebas. Este proceso puede variar según el dispositivo, por ejemplo, en un dispositivo Redmi 9, deberás hacer las siete pulsaciones en la opción Versión de MIUI para activar las opciones para desarrolladores.

Conectar el dispositivo

Una vez que el dispositivo está listo, con el modo desarrollador activado y la depuración por USB habilitada, conecta el dispositivo al ordenador mediante un cable USB. Asegúrate de utilizar un cable que permita la transferencia de datos, no todos los cables USB lo permiten.

Al conectar el dispositivo, es posible que aparezca una ventana emergente en el dispositivo solicitando permiso para permitir la depuración USB desde el ordenador. Asegúrate de aceptar esta solicitud para que Android Studio pueda comunicarse con el dispositivo.


Autor/a: Javier Carrasco Última modificación: 23/09/2025

Chuleta rápida de Remember en Jetpack Compose

1. Básicos de estado

Función Descripción Ejemplo rápido
remember { … } Guarda el estado en recomposición mientras el composable esté en el árbol. val count = remember { mutableStateOf(0) }
rememberSaveable { … } Igual que remember, pero persiste en recreaciones de actividad (rotación, proceso). val count = rememberSaveable { mutableStateOf(0) }

2. Corutinas y lambdas

Función Descripción Ejemplo rápido
rememberCoroutineScope() Devuelve un CoroutineScope ligado al ciclo de vida del composable. val scope = rememberCoroutineScope()
rememberUpdatedState(value) Mantiene siempre la última versión de un valor dentro de efectos (LaunchedEffect, etc.). val onClick by rememberUpdatedState(newValue = action)

3. Animaciones

Función Descripción Ejemplo rápido
rememberInfiniteTransition() Animaciones que se repiten indefinidamente. val alpha by infinite.animateFloat(... )
rememberTransition(state) Transiciones entre estados definidos. val transition = updateTransition(targetState, label="")
rememberSplineBasedDecay() Animación tipo fling con decaimiento. val decay = rememberSplineBasedDecay<Float>()

4. Scroll y listas

Función Descripción Ejemplo rápido
rememberScrollState() Guarda la posición de scroll en Column / Row. val scroll = rememberScrollState()
rememberLazyListState() Guarda el estado de scroll en LazyColumn/LazyRow. val listState = rememberLazyListState()
rememberLazyGridState() Estado de scroll en LazyVerticalGrid o LazyHorizontalGrid. val gridState = rememberLazyGridState()
rememberPagerState() Estado de un pager (HorizontalPager / VerticalPager). val pagerState = rememberPagerState()

5. Gestos e interacción

Función Descripción Ejemplo rápido
rememberDraggableState(onDelta) Controla los desplazamientos en gestos de arrastre. val dragState = rememberDraggableState { delta -> … }
rememberSwipeableState(initialValue) Estado de un componente deslizable (ej: BottomSheet). val sheetState = rememberSwipeableState(0)
rememberPullRefreshState(refreshing, onRefresh) Estado para pull-to-refresh. val refreshState = rememberPullRefreshState(refreshing, { … })
rememberNestedScrollInteropConnection() Conexión de scroll con vistas clásicas Android. val connection = rememberNestedScrollInteropConnection()

6. Otros especializados

Función Descripción Ejemplo rápido
rememberSystemUiController() (Accompanist) Controla status bar y nav bar. val controller = rememberSystemUiController()
rememberInsetsPaddingValues() (Accompanist, deprecated) Padding según insets del sistema. val padding = rememberInsetsPaddingValues()
rememberHapticFeedback() Acceso al motor de vibración/háptica. val haptic = LocalHapticFeedback.current
rememberClipboardManager() Acceso al portapapeles del sistema. val clipboard = LocalClipboardManager.current

🎓 Ejemplo práctico integrando varios remember

 1@Composable
 2fun DemoRememberScreen() {
 3    val count = rememberSaveable { mutableStateOf(0) }
 4    val scrollState = rememberScrollState()
 5    val scope = rememberCoroutineScope()
 6    val transition = rememberInfiniteTransition()
 7
 8    val alpha by transition.animateFloat(
 9        initialValue = 0f,
10        targetValue = 1f,
11        animationSpec = infiniteRepeatable(
12            animation = tween(1000),
13            repeatMode = RepeatMode.Reverse
14        )
15    )
16
17    Column(
18        modifier = Modifier
19            .fillMaxSize()
20            .verticalScroll(scrollState),
21        horizontalAlignment = Alignment.CenterHorizontally
22    ) {
23        Text("Contador: ${count.value}", modifier = Modifier.alpha(alpha))
24
25        Button(onClick = {
26            scope.launch { count.value++ }
27        }) {
28            Text("Incrementar")
29        }
30    }
31}

Autor/a: Javier Carrasco Última modificación: 24/09/2025

Ejemplo práctico 1: Estado observable

Objetivo

Entender el funcionamiento de un observable y las diferencias entre utilizar remember { mutableStateOf(false) } y remember { false }.

Desarrollo

La clase MainActivity es la típica configuración de una actividad con Jetpack Compose, donde se establece el tema y el Scaffold, desde donde se llama a la función composable RememberVsMutableStateDemo:

 1class MainActivity : ComponentActivity() {
 2    override fun onCreate(savedInstanceState: Bundle?) {
 3        super.onCreate(savedInstanceState)
 4        enableEdgeToEdge()
 5        setContent {
 6            ExampleT1_01Theme {
 7                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
 8                    RememberVsMutableStateDemo(Modifier.padding(innerPadding))
 9                }
10            }
11        }
12    }
13}

Observa el código del método RememberVsMutableStateDemo, en el se muestran dos bloques similares pero con comportamientos diferentes:

 1@Composable
 2fun RememberVsMutableStateDemo(modifier: Modifier = Modifier) {
 3    Surface(modifier = modifier.fillMaxSize()) {
 4        Column(
 5            modifier = Modifier
 6                .padding(16.dp)
 7                .fillMaxSize(),
 8            verticalArrangement = Arrangement.spacedBy(24.dp)
 9        ) {
10
11            // 1. Estado observable: cambia y la UI se actualiza.
12            Card {
13                Column(
14                    modifier = Modifier.padding(16.dp),
15                    verticalArrangement = Arrangement.spacedBy(12.dp)
16                ) {
17                    Text(
18                        text = "Estado observable con mutableStateOf",
19                        style = MaterialTheme.typography.titleMedium
20                    )
21                    var checked by remember { mutableStateOf(false) }
22                    Row(
23                        verticalAlignment = Alignment.CenterVertically,
24                        horizontalArrangement = Arrangement.spacedBy(12.dp)
25                    ) {
26                        Switch(
27                            checked = checked,
28                            onCheckedChange = { checked = it }
29                        )
30                        Text(
31                            if (checked) "Estado: ACTIVO (se recompone)" else "Estado: INACTIVO (se recompone)"
32                        )
33                    }
34                    Text(
35                        text = "Aquí usamos un State<Boolean>. Cambiar su valor notifica a Compose y la UI se recompone."
36                    )
37                }
38            }
39
40            // 2. Valor recordado NO observable: no hay recomposición al reasignar la variable local.
41            Card {
42                Column(
43                    modifier = Modifier.padding(16.dp),
44                    verticalArrangement = Arrangement.spacedBy(12.dp)
45                ) {
46                    Text(
47                        text = "Valor recordado sin estado observable",
48                        style = MaterialTheme.typography.titleMedium
49                    )
50
51                    // OJO: esto recuerda "false" una vez, pero NO es State<T>.
52                    var naive = remember { false }
53
54                    Row(
55                        verticalAlignment = Alignment.CenterVertically,
56                        horizontalArrangement = Arrangement.spacedBy(12.dp)
57                    ) {
58                        // La UI LEE 'naive' pero cambiarlo no provoca recomposición.
59                        Switch(
60                            checked = naive,
61                            onCheckedChange = { newValue ->
62                                // Esto cambia la variable local, pero NO notifica a Compose.
63                                naive = newValue
64                                println("Desde el Switch: $naive")
65                            }
66                        )
67                        Text(
68                            if (naive) "Valor leído: TRUE (no observable)" else "Valor leído: FALSE (no observable)"
69                        )
70                    }
71
72                    Button(
73                        onClick = {
74                            // También “cambia” la variable local, pero la UI no se enterará.
75                            naive = !naive
76                            println("Desde el Button: $naive")
77                        }
78                    ) {
79                        Text("Intentar alternar (no actualizará la UI)")
80                    }
81
82                    Text(
83                        text = "Este bloque usa un Boolean 'recordado' pero no observable. " +
84                                "Reasignarlo no dispara recomposición, por lo que la UI no refleja los cambios."
85                    )
86                }
87            }
88
89            // Nota pedagógica opcional
90            AssistChip(
91                onClick = {},
92                label = { Text("Consejo: si necesitas UI reactiva, usa State<T> (p. ej., mutableStateOf).") }
93            )
94        }
95    }
96}
  1. Estado observable con mutableStateOf:

    • La variable checked se define como var checked by remember { mutableStateOf(false) }.
    • Esto crea un estado observable. Cuando el usuario interactúa con el Switch y cambia su valor, la UI se recompone automáticamente para reflejar el nuevo estado.
    • El texto del Switch muestra “Estado: ACTIVO” o “Estado: INACTIVO” según el valor de checked, y este texto se actualiza dinámicamente.
    • Cada vez que checked cambia, Compose detecta el cambio y vuelve a ejecutar la función composable, actualizando la interfaz de usuario.
  2. Valor recordado sin estado observable:

    • La variable naive se define como var naive = remember { false }.
    • Esto recuerda el valor “false” una vez, pero no es un estado observable. Cambiar naive no provoca recomposición.
    • Cuando el usuario interactúa con el Switch o el Button, la variable naive cambia internamente, pero la UI no se actualiza para reflejar estos cambios.
    • El texto del Switch muestra “Valor leído: FALSE” o “Valor leído: TRUE” según el valor de naive, pero este texto no se actualizará nunca.

Conclusión

Este ejemplo ilustra claramente la diferencia entre usar un estado observable (mutableStateOf) y un valor recordado no observable (remember { false }). Para que la UI sea reactiva y se actualice automáticamente cuando los datos cambian, es esencial utilizar estados observables en Jetpack Compose.