Saltar al contenido
K
Jetpack ComposePaso a paso36 min de lectura · 12 min de máquina Intermedio

Efectos y ciclo de vida: DisposableEffect

Al terminar: Registrar y deshacer un efecto atado al ciclo de vida de la pantalla con DisposableEffect, e instalar lifecycle-runtime-compose.

Al terminar esta lección habrás escrito tú un efecto que registra algo y lo quita al salir (DisposableEffect). Reconocerás el error de compilación que da olvidar el onDispose. Y lo verás funcionar en Marcador: un contador de las veces que la pantalla pasa a segundo plano.

Importa porque un observador (un objeto al que te apuntas para que Android le avise cada vez que pasa algo) que se registra y no se quita sigue vivo aunque la pantalla ya no exista (una fuga), y no da ningún error a la vista. Tema: DisposableEffect. Instalas una librería, lifecycle-runtime-compose, en el primer apartado del paso (es la que da acceso al ciclo de vida desde Compose).

DisposableEffect: registrar y deshacer

DisposableEffect(key) { … } sirve cuando lo que empiezas hay que deshacerlo: registras algo al entrar y lo quitas en onDispose { … }, que se ejecuta cuando la clave cambia o cuando el composable sale de la composición. Sin quitarlo, el objeto registrado seguiría vivo (una fuga).

Los dos ingredientes que aparecen en el ejemplo (Log.d escribe en Logcat, la ventana de registros de Android Studio, como en El ciclo de vida de la Activity):

  • LifecycleEventObserver: un objeto que recibe cada evento del ciclo de vida (ON_START, ON_STOP…) y se engancha con lifecycle.addObserver(…). Es una interfaz (un contrato de métodos, como las de Herencia, interfaces y composición) con un solo método, y por eso Kotlin permite construirla escribiendo solo una lambda: LifecycleEventObserver { _, evento -> … }. La lambda recibe dos parámetros (el propietario y el evento) y el guion bajo _ significa «no uso este».
  • LocalLifecycleOwner.current: otro CompositionLocal, como el que usa MaterialTheme (Compose: Scaffold, slots y tema); .current lo lee. Este da el propietario del ciclo de vida (la Activity). Vive en lifecycle-runtime-compose, la librería que instalas en el paso de abajo: en el código lleva import androidx.lifecycle.compose.LocalLifecycleOwner.
ejemplo mínimo: escuchar el ciclo de vida (con comentarios)
@Composable
fun EscuchaElCiclo() {
    val propietario = LocalLifecycleOwner.current                     // la Activity que aloja esta composición
    DisposableEffect(propietario) {                                   // clave: el propietario
        val observador = LifecycleEventObserver { _, evento ->        // _: el propietario, que no se usa aquí
            Log.d("Ciclo", "evento: $evento")                         // ON_CREATE, ON_START, ON_RESUME… (se ve en Logcat)
            if (evento == Lifecycle.Event.ON_START) Log.d("Ciclo", "ya se ve")  // reaccionar a UN evento concreto
        }
        propietario.lifecycle.addObserver(observador)                 // al entrar: registrar
        onDispose { propietario.lifecycle.removeObserver(observador) } // al salir: quitar (siempre la última línea)
    }
}

Lifecycle.Event es la lista cerrada de eventos posibles (un enum, como en Enum y when exhaustivo), escrita dentro de Lifecycle; por eso se nombra Lifecycle.Event.ON_START.

La clave es el propietario. Casi nunca cambia (la Activity es la misma mientras la pantalla vive); se pone como clave por seguridad: si cambiara, Compose ejecutaría onDispose para desenganchar el observador del viejo y volvería a ejecutar el bloque para engancharlo al nuevo.

El ObservadorDelCiclo de Marcador es este mismo ejemplo con dos añadidos: cuando el evento es ON_STOP avisa con una lambda (alPararse) y escribe cada paso en Logcat. Esa lambda pasa por rememberUpdatedState, por la razón de Efectos en Compose: LaunchedEffect: el observador vive mucho tiempo y no debe quedarse con una lambda vieja. Lo escribirás tú en el paso de más abajo.

Antes, el mismo mecanismo con algo que se ve en pantalla, en un edificio inventado. Un objeto Timbre, de fuera de Compose, guarda una lista de quién quiere enterarse cuando suena. El portero se apunta al llegar y se borra al irse:

ejemplo mínimo: un portero que se apunta al timbre (con comentarios)
object Timbre {                                               // algo de fuera de Compose
    val oyentes = mutableListOf<() -> Unit>()                 // quién quiere enterarse
    fun sonar() = oyentes.forEach { it() }                    // avisa a todos
}
 
@Composable
fun Recepcion() {
    var hayPortero by remember { mutableStateOf(true) }
    Column {
        Row(verticalAlignment = Alignment.CenterVertically) {
            Switch(checked = hayPortero, onCheckedChange = { hayPortero = it })
            Text("Portero en la puerta")
        }
        Button(onClick = { Timbre.sonar() }) { Text("Llamar al timbre") }
        if (hayPortero) Portero()                               // apagado: Portero sale de la composición
    }
}
 
@Composable
fun Portero() {
    var visitas by remember { mutableIntStateOf(0) }
    DisposableEffect(Unit) {                                  // Unit: se registra una sola vez
        val oyente: () -> Unit = { visitas++ }                // lo que hace al oír el timbre
        Timbre.oyentes.add(oyente)                            // al entrar: apuntarse
        onDispose { Timbre.oyentes.remove(oyente) }           // al salir: borrarse (siempre la última línea)
    }
    Text("Visitas: $visitas")
}

Lo que ves en pantalla: el interruptor encendido, el botón y Visitas: 0. Tras dos llamadas al timbre, Visitas: 2.

Si apagas el interruptor, llamas tres veces al timbre y lo vuelves a encender, ves Visitas: 0, no 5. Al apagar, Portero salió de la composición, onDispose lo borró del timbre y nadie contó esas tres llamadas; al encender entra un Portero nuevo, con su remember en blanco. Sin el onDispose, el portero viejo seguiría apuntado al timbre, contando visitas para una pantalla que ya no existe: eso es la fuga. El ejercicio de abajo te deja comprobarlo contando los oyentes.

Error típico: acabar el bloque con otra cosa

con el bug: el bloque no termina en onDispose
DisposableEffect(propietario) {
    val observador = LifecycleEventObserver { _, evento -> Log.d("Efectos", "evento: $evento") }
    propietario.lifecycle.addObserver(observador)
    onDispose { propietario.lifecycle.removeObserver(observador) }
    Log.d("Efectos", "observador registrado")   // ← la última línea ya no es onDispose
}
corregido: el Log va antes de onDispose
DisposableEffect(propietario) {
    val observador = LifecycleEventObserver { _, evento -> Log.d("Efectos", "evento: $evento") }
    propietario.lifecycle.addObserver(observador)
    Log.d("Efectos", "observador registrado")
    onDispose { propietario.lifecycle.removeObserver(observador) }
}

Si la última línea del bloque no es onDispose, el compilador falla, y el mensaje habla de tipos, no de «falta onDispose»:

e: file:///…/ui/Efectos.kt:103:9 Return type mismatch: expected 'DisposableEffectResult', actual 'Int'.

El bloque tiene que devolver un DisposableEffectResult, y eso solo lo produce onDispose. Log.d(…) devuelve un Int, y por eso el mensaje dice actual 'Int'. La ruta y los números de línea del mensaje son los de tu fichero, y varían.

Con este ya tienes las seis herramientas de efectos de Compose, las de Efectos en Compose: LaunchedEffect, cada una en su lección:

HerramientaPara quéSe deshaceCuándo corre
LaunchedEffect(claves)Trabajo suspendido (corrutina)Se cancela al salir o al cambiar una claveAl entrar y cuando cambia una clave
rememberUpdatedState(x)Que un efecto largo vea siempre el último valorNo hace faltaSe actualiza en cada recomposición
rememberCoroutineScope()Corrutinas lanzadas desde un onClickSe cancela al salirCuando tú la lanzas
derivedStateOf { … }Un estado calculado que avisa solo si cambia su resultadoNo hace faltaCuando cambia lo que lee
DisposableEffect(claves)Registrar algo y quitarloonDispose { … }Al entrar y cuando cambia una clave
SideEffectPublicar estado a un objeto ajenoNo se deshaceTras cada pasada completada

Reto · Ejercicio: la emisora de radio

Es la misma mecánica del portero, pero comprobada: ahora ves cuántos oyentes quedan apuntados. Lo haces en la vista previa interactiva (la de Estado en Compose).

En la carpeta ejercicios (la que creaste en La rotación y la muerte del proceso): clic derecho sobre com.sebastiannarvaez.marcador → New → Package → escribe .ejercicios.emisora_de_radio. Luego clic derecho sobre esa carpeta → New → Kotlin Class/File → File → Radio.

Escribe tres piezas en Radio.kt:

  • Emisora, un objeto de fuera de Compose con una lista de oyentes (funciones que reciben un texto) y una función que emite una noticia a todos, como el timbre de arriba.
  • Radio, que recuerda la última noticia (empieza en «nada todavía»). Al entrar en la composición se apunta a la emisora con un oyente que guarda la noticia; al salir se borra. Escribe «La radio dice: …».
  • SalaDeRadio, con un interruptor «Radio encendida» (encendido al empezar), un botón «Emitir noticia» que emite «noticia 1», «noticia 2»…, un botón «¿Cuántos escuchan?» que copia en pantalla el número de oyentes apuntados («Escuchan: N») y la Radio solo si el interruptor está encendido.

Añade una vista previa con SalaDeRadio. En el modo interactivo: pulsa «¿Cuántos escuchan?» y emite una noticia; apaga la radio, pregunta otra vez y emite dos; enciéndela y pregunta otra vez. Debes ver:

  • «Escuchan: 1» y «La radio dice: noticia 1».
  • Con la radio apagada, «Escuchan: 0».
  • Al encenderla, «La radio dice: nada todavía» (es un remember nuevo) y «Escuchan: 1», no 2: el onDispose borró al oyente viejo.

Pista para onDispose: dentro va lo contrario de lo que hiciste al entrar; si añadiste el oyente a la lista de la emisora, ahí lo quitas de esa lista. Si te olvidas de onDispose, no compila (es el error típico de más arriba). Y si lo dejas vacío (onDispose { }), compila, pero al encender la radio saldría «Escuchan: 2»: eso es la fuga.

Ver solución
app/src/main/java/com/sebastiannarvaez/marcador/ejercicios/emisora_de_radio/Radio.kt
package com.sebastiannarvaez.marcador.ejercicios.emisora_de_radio
 
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
 
object Emisora {                                              // algo de fuera de Compose
    val oyentes = mutableListOf<(String) -> Unit>()
    fun emitir(noticia: String) = oyentes.forEach { it(noticia) }
}
 
@Composable
fun Radio() {
    var ultima by remember { mutableStateOf("nada todavía") }
    DisposableEffect(Unit) {
        val oyente: (String) -> Unit = { noticia -> ultima = noticia }
        Emisora.oyentes.add(oyente)                           // al entrar: apuntarse
        onDispose { Emisora.oyentes.remove(oyente) }          // al salir: borrarse
    }
    Text("La radio dice: $ultima")
}
 
@Composable
fun SalaDeRadio(modifier: Modifier = Modifier) {
    var encendida by remember { mutableStateOf(true) }
    var noticias by remember { mutableIntStateOf(0) }
    var escuchan by remember { mutableIntStateOf(0) }
    Column(modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
        Row(verticalAlignment = Alignment.CenterVertically) {
            Switch(checked = encendida, onCheckedChange = { encendida = it })
            Text("Radio encendida")
        }
        Button(onClick = { noticias++; Emisora.emitir("noticia $noticias") }) { Text("Emitir noticia") }
        Button(onClick = { escuchan = Emisora.oyentes.size }) { Text("¿Cuántos escuchan?") }
        Text("Escuchan: $escuchan")
        if (encendida) Radio()                                // apagada: Radio sale de la composición
    }
}
 
@Preview(showBackground = true)
@Composable
private fun SalaDeRadioPreview() {
    MaterialTheme { SalaDeRadio() }
}

Paso · el contador de «segundo plano»

Objetivo. Instalar lifecycle-runtime-compose, escribir tú en Efectos.kt el composable ObservadorDelCiclo y llamarlo desde PantallaMarcador, con la variable pausas que lo cuenta. Al terminar, la línea «Veces que la pantalla pasó a segundo plano: N» sube cada vez que pulsas Home y vuelves, y Logcat cuenta cuándo se registra y se quita el observador.

Gradle Scripts
libs.versions.toml (Version Catalog "libs")se modifica: [libraries]
build.gradle.kts (Module :app)se modifica: dependencies
app
kotlin+java → com.sebastiannarvaez.marcador
feature
marcador
ui
Efectos.ktse modifica
PantallaMarcador.ktse modifica
Cuatro ficheros que se modifican, tal como los ves en la vista Android.

Instala lifecycle-runtime-compose

La receta de Los ficheros de Gradle de tu proyecto, en corto.

1 · Qué instalas. lifecycle-runtime-compose conecta el ciclo de vida de la Activity (El ciclo de vida de la Activity) con Compose: de ella sale LocalLifecycleOwner, lo primero que escribes hoy. Ya llega sola con la librería androidx.compose.ui (una dependencia transitiva: la trae otra librería que ya tienes, como foundation en Compose: composables y modifiers); la declaras tú porque tu código la usa directamente.

2 · La versión. Nada en [versions]: comparte número con lifecycle-runtime-ktx (lifecycleRuntimeKtx). Duplicarlo repetiría la trampa de Los ficheros de Gradle de tu proyecto: el catálogo tenía dos números para lo mismo, kotlin y jetbrainsKotlinJvm, y si no coinciden Gradle se queda con el más alto sin avisar.

3 · La librería. En Gradle Scripts → libs.versions.toml (Version Catalog "libs"), al final de [libraries], antes de la cabecera [bundles]:

catálogo: línea nueva en [libraries] (con comentarios)
# version.ref = lifecycleRuntimeKtx (2.11.0): el número de toda la familia lifecycle
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeKtx" }

4 · Un plugin, no. Es una librería: no hay [plugins] ni apply false.

5 · La dependencia. En build.gradle.kts (Module :app), dentro de dependencies { … }, debajo de libs.androidx.lifecycle.runtime.ktx:

build.gradle.kts (Module :app): línea nueva (con comentarios)
    // implementation: el código de la app la usa al compilar y al ejecutar
    implementation(libs.androidx.lifecycle.runtime.compose)

6 · Sync Now. Debe terminar sin errores rojos. Si falla (la caja «Si Sync sale en rojo» de Los ficheros de Gradle de tu proyecto), lo más probable es un alias con puntos donde el catálogo lleva guiones.

Compilar no demuestra nada, porque la librería ya llegaba sola: lo que cambia es que ahora la declaras tú. En la Terminal de Android Studio, pide a Gradle el árbol de dependencias y filtra las de androidx.lifecycle:

./gradlew :app:dependencies --configuration gratisDebugCompileClasspath | grep "^+--- androidx.lifecycle"

Qué hace cada parte:

  • --configuration gratisDebugCompileClasspath: de todas las listas de dependencias que tiene el proyecto, pide la que se usa para compilar la variante gratisDebug (la de Gradle: flavors y variantes).
  • | grep "…": se queda solo con las líneas que cumplen el patrón.
  • ^ dentro del patrón: «al principio de la línea». +--- es como Gradle empieza las dependencias de primer nivel, las que declaras tú en build.gradle.kts; las que traen otras librerías van más a la derecha, con sangría, y el ^ las descarta.

Debes ver exactamente estas dos (sin tu línea, solo la primera; (*) avisa de que esa rama ya se imprimió):

+--- androidx.lifecycle:lifecycle-runtime-ktx:2.11.0 (*)
+--- androidx.lifecycle:lifecycle-runtime-compose:2.11.0 (*)

Escribe el observador

Punto de partida. Para abrir un fichero, en el panel Project (con el selector en Android) despliega app → kotlin+java → com.sebastiannarvaez.marcador → feature → marcador → ui y haz doble clic. Tras la lección anterior, Efectos.kt tiene ETIQUETA, MINUTOS_DEL_PARTIDO, RelojDelPartido, TituloDeLaActividad y PanelDelCronometro.

Piezas nuevas de este paso:

  • DisposableEffect(clave) { … onDispose { … } }, LifecycleEventObserver y LocalLifecycleOwner.current: arriba.
  • lifecycle.addObserver(o) y lifecycle.removeObserver(o): enganchan y sueltan un observador del ciclo de vida.
  • Lifecycle.Event.ON_STOP: el evento que llega cuando la pantalla deja de verse (por ejemplo, con Home).
  • ;: separa dos órdenes escritas en la misma línea, como en el onReiniciar de Efectos en Compose: LaunchedEffect.

Cómo se hace. Primero la instalación de arriba (catálogo, build.gradle.kts y Sync Now). Después, intenta escribirlo tú, en este orden:

  1. Abre Efectos.kt y añade a su lista de import los de la primera pieza de abajo (Android Studio también los propone con Option+Enter).
  2. Entre RelojDelPartido y TituloDeLaActividad, escribe ObservadorDelCiclo: un composable que recibe qué hacer cuando la pantalla se para. Al entrar en la composición engancha un observador al ciclo de vida de la pantalla, que anota en Logcat cada evento que ve y, cuando la pantalla deja de verse, ejecuta esa acción (siempre la más reciente); al salir, lo suelta. Anota también cuándo lo engancha y cuándo lo suelta.
  3. En PanelDelCronometro, añade el parámetro pausas (justo después de ultimoAviso) y una línea más: «Veces que la pantalla pasó a segundo plano: …».
  4. Abre PantallaMarcador.kt. Declara pausas justo después de ultimoAviso (sobrevive a la rotación y empieza en 0), añade la llamada a ObservadorDelCiclo justo después de RelojDelPartido(…) { … } y antes de TituloDeLaActividad(…) (la acción que le pasas suma uno a pausas cada vez que la pantalla se para), y pasa pausas al panel.
  5. Ejecuta la app (el apartado «Ejecútalo y míralo en Logcat» de abajo te dice cómo).

Todas las líneas de Logcat van con la etiqueta Efectos. Lo que debes ver al abrir la app:

D Efectos: el composable ve ON_CREATE
D Efectos: el composable ve ON_START
D Efectos: el composable ve ON_RESUME
D Efectos: observador REGISTRADO
D Efectos: título publicado: Marcador 0-0

Y al volver de Home, la línea «Veces que la pantalla pasó a segundo plano» en 1; al cambiar de pestaña, observador QUITADO.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código

Imports nuevos de Efectos.kt, mezclados con los que ya tiene y en su sitio alfabético:

app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/Efectos.kt
import androidx.compose.runtime.DisposableEffect
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner

(Log, getValue, rememberUpdatedState y Composable ya los tiene, de las lecciones anteriores.) La función, en Efectos.kt entre RelojDelPartido y TituloDeLaActividad:

app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/Efectos.kt
@Composable
fun ObservadorDelCiclo(alPararse: () -> Unit) {
    val propietario = LocalLifecycleOwner.current
    val alPararseActual by rememberUpdatedState(alPararse)
 
    // key = propietario: si cambiara el LifecycleOwner, se des-registra del viejo y se
    // registra en el nuevo.
    DisposableEffect(propietario) {
        val observador = LifecycleEventObserver { _, evento ->
            Log.d(ETIQUETA, "el composable ve $evento")
            if (evento == Lifecycle.Event.ON_STOP) alPararseActual()
        }
        propietario.lifecycle.addObserver(observador)
        Log.d(ETIQUETA, "observador REGISTRADO")
        onDispose {
            propietario.lifecycle.removeObserver(observador)
            Log.d(ETIQUETA, "observador QUITADO")
        }
    }
}

El panel con pausas (sustituye a PanelDelCronometro; es el parámetro nuevo y su línea de texto):

app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/Efectos.kt
@Composable
fun PanelDelCronometro(
    minuto: Int,
    corriendo: Boolean,
    ultimoAviso: String,
    pausas: Int,
    onAlternar: () -> Unit,
    onReiniciar: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Column(modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
        Text("Minuto $minuto'", style = MaterialTheme.typography.headlineMedium)
        Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
            Button(onClick = onAlternar) { Text(if (corriendo) "Pausa" else if (minuto == 0) "Empezar" else "Reanudar") }
            Button(onClick = onReiniciar) { Text("Reiniciar") }
        }
        Text("Último aviso (cada 15'): $ultimoAviso")
        Text("Veces que la pantalla pasó a segundo plano: $pausas")
    }
}

En PantallaMarcador.kt, la variable, justo después de var ultimoAviso:

app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PantallaMarcador.kt
    var pausas by rememberSaveable { mutableIntStateOf(0) }

La llamada, justo después de RelojDelPartido(…) { … } y antes de TituloDeLaActividad(estado.marcador):

app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PantallaMarcador.kt
    ObservadorDelCiclo(alPararse = { pausas++ })

Y pausas en la llamada al panel, que queda así:

app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PantallaMarcador.kt
                PanelDelCronometro(
                    minuto, corriendo, ultimoAviso, pausas,
                    onAlternar = { corriendo = !corriendo },
                    onReiniciar = { corriendo = false; minuto = 0; ultimoAviso = "ninguno" },
                )

Qué hace cada pieza

  • ObservadorDelCiclo es el ejemplo mínimo con dos añadidos. Escribe cada evento en Logcat y, cuando llega ON_STOP, llama a alPararseActual(). La pantalla le pasa { pausas++ }, y por eso el contador sube. rememberUpdatedState evita que el observador se quede con una lambda vieja, como en el reloj.
  • El DisposableEffect no declara más claves que el propietario: el observador se registra una vez al entrar y se quita al salir, no cada vez que cambia el minuto o el marcador.
  • pausas está en rememberSaveable: cuenta las veces que la pantalla pasó a segundo plano, y eso no se pierde al girar el móvil.

Ejecútalo y míralo en Logcat

Ejecuta la app con Run (configuración app, variante gratisDebug como en Gradle: flavors y variantes, emulador arrancado como en La Activity). Abre View → Tool Windows → Logcat y escribe tag:Efectos en el campo de búsqueda de arriba. Si prefieres la terminal, adb logcat -c vacía el registro y adb logcat -s Efectos se queda escuchando la etiqueta (para salir, Ctrl+C).

  1. Al abrir la app, Logcat muestra cinco líneas seguidas:

    D Efectos: el composable ve ON_CREATE
    D Efectos: el composable ve ON_START
    D Efectos: el composable ve ON_RESUME
    D Efectos: observador REGISTRADO
    D Efectos: título publicado: Marcador 0-0

    Las tres primeras sorprenden: nadie ha cambiado de estado, y sin embargo el observador «ve» tres eventos. Es lo normal. Cuando enganchas un observador con addObserver a una pantalla que ya está en RESUMED, Android lo pone al día en el acto: le entrega los eventos que ya pasaron (ON_CREATE, ON_START y ON_RESUME) para que parta del mismo estado que la pantalla. Salen antes de observador REGISTRADO porque esa línea la escribe tu código después de volver de addObserver, y los eventos se entregan dentro de él. Solo ON_STOP hace subir el contador.

  2. Pulsa Home y vuelve a la app (con el botón de recientes del emulador, o abriéndola desde la lista de aplicaciones). En Logcat verás pasar ON_PAUSE y ON_STOP al salir y ON_START y ON_RESUME al volver; no vuelve a salir ON_CREATE ni observador REGISTRADO, porque la pantalla no se destruyó y el observador sigue enganchado. La línea «Veces que la pantalla pasó a segundo plano» vale ahora 1. Si el reloj estaba en marcha (Efectos en Compose: LaunchedEffect), ha seguido corriendo.

  3. Cambia a la pestaña Partidos, la que hiciste en Pestañas con NavigationBar: aparece observador QUITADO, porque PantallaMarcador salió de la composición y se ejecutó onDispose. Al volver a Marcador, aparece otra vez observador REGISTRADO, precedido de los tres el composable ve ON_… del paso 1 (es un observador nuevo y se pone al día). No hace falta cerrar la app.

Provoca el error típico

En ObservadorDelCiclo, añade una última línea después de onDispose { … }, por ejemplo Log.d(ETIQUETA, "listo"). Al compilar, Android Studio marca el error con el mensaje del apartado «Error típico» (Return type mismatch: expected 'DisposableEffectResult', actual 'Int'). Bórrala y vuelve a ejecutar.

Punto de control

  • La app compila y se ejecuta sin errores en rojo, con ObservadorDelCiclo escrita por ti.
  • Al abrir la pantalla, Logcat (tag:Efectos) muestra el composable ve ON_CREATE, ON_START y ON_RESUME, y después observador REGISTRADO y título publicado: Marcador 0-0.
  • En la Terminal, el comando de dependencies de la instalación muestra lifecycle-runtime-compose en el primer nivel del árbol, junto a lifecycle-runtime-ktx.
  • Tras Home y volver, «Veces que la pantalla pasó a segundo plano» vale 1; tras otro Home y volver, 2.
  • Al cambiar a la pestaña Partidos aparece observador QUITADO, y al volver a Marcador, otro observador REGISTRADO.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.¿Qué tiene que ser la última sentencia del bloque de un DisposableEffect?

  2. 2.Un composable registra un observador dentro de un DisposableEffect y el usuario cambia a otra pestaña, de modo que ese composable sale de la composición. ¿Qué ocurre?

  3. 3.Necesitas registrar un listener al entrar en pantalla y quitarlo al salir. ¿Qué efecto usas?

  4. 4.Al compilar ves Return type mismatch: expected 'DisposableEffectResult', actual 'Int'. ¿Qué ha pasado?

  5. 5.Un Portero se apunta a un Timbre con DisposableEffect. Apagas el interruptor que lo muestra, llamas tres veces al timbre y lo vuelves a encender. «Visitas» marca 0. ¿Por qué?

Faltan 5 de 5