El ciclo de vida de la Activity
Al terminar: Ver en Logcat cada callback de la Activity al arrancar, ir a Home y volver, y rotar, y saber por qué onDestroy no significa que el usuario se fue.
Al terminar esta lección, cada vez que Android cree, pause, oculte o destruya la pantalla de Marcador, dejará una línea en Logcat con el tag Ciclo. Verás con tus ojos qué llama el sistema y en qué orden. Parte de lo que dejó La Activity: :app ya depende de :torneo y Marcador ya corre en el emulador.
Importa porque todo lo que viene después (rotar, hilos, memoria) es una consecuencia de que el sistema decide cuándo empieza y acaba una pantalla, y tu código solo reacciona a sus avisos. Temas: la Activity y su ciclo de vida, callback a callback; Logcat; el error de creer que onDestroy significa «el usuario se fue».
El ciclo de vida, callback a callback
Dos palabras de la cita de abajo. finish es la orden con la que tu propio código cierra una pantalla (lo usarás en Intents explícitos y una segunda pantalla). Y un cambio de configuración es un cambio del entorno del dispositivo que afecta a la pantalla, como girar el móvil de vertical a horizontal; en Rotación y muerte del proceso lo verás con calma.
| Callback | Cuándo lo llama el sistema | La pantalla está… |
|---|---|---|
onCreate | Al crearla (una vez por instancia) | aún no visible |
onStart | Justo antes de hacerse visible | visible, pero aún sin recibir toques |
onResume | Cuando ya puedes interactuar | delante, con foco |
onPause | Al perder el foco (otra pantalla encima, de otra app o de otra Activity) | sin foco; puede verse todavía en parte (si lo que hay encima es transparente o de menor tamaño) o no |
onStop | Cuando deja de verse del todo | oculta |
onRestart | Al volver desde onStop, antes de onStart | oculta, a punto de verse |
onDestroy | Antes de destruirla (la cierra el usuario o tu código con finish, o la destruye el sistema) | destruida |
Dos aclaraciones sobre la tabla. Un diálogo de tu propia app (una ventanita de aviso) no cuenta como «otra pantalla encima»: es una ventana de la misma Activity, que sigue siendo la pantalla en primer plano, y por eso no sale ningún callback. Un diálogo del sistema (el que pide un permiso, por ejemplo) sí es otra pantalla, y sí dispara onPause. Y el botón Atrás en la pantalla principal de la app, desde Android 12, no llama a onDestroy: la manda al fondo, como Home. onDestroy llega cuando el usuario cierra una pantalla que no es la principal, cuando tu código llama a finish o cuando el sistema la destruye.
Dos palabras de esa tabla. Tener foco es ser la pantalla que recibe los toques y las teclas; visible es solo que se ve, aunque no reciba nada. Por eso hay un momento, entre onStart y onResume, en que la pantalla se ve pero no responde aún, y otro, en onPause, en que otra ventana ya tiene el foco pero la tuya sigue asomando por debajo. Y multiventana es el modo en que el sistema pone dos apps a la vez en la pantalla, cada una en una mitad; en ese modo la ventana sin foco sigue visible. En la documentación de Google verás que a una Activity que ya no se ve se le dice que está en estado STOPPED (parada): es solo una forma de hablar de lo que pasa tras onStop.
Hay dos callbacks más que no forman parte de esa escalera: onSaveInstanceState (el sistema te deja guardar datos antes de destruirla, es el tema de la próxima lección) y onRestoreInstanceState (te los devuelve). Y otros tres, más raros, que también sobrescribirás para verlos:
| Callback | Cuándo lo llama el sistema |
|---|---|
onNewIntent | Llega un Intent (un mensaje para abrir o dirigir una pantalla) a una Activity que ya existe. Hoy lo sobrescribes, pero no llegarás a dispararlo: se explica en Intents implícitos y filtros y se usa de verdad en Deep links en Android |
onConfigurationChanged | Cambia la configuración (por ejemplo, la orientación) y el Manifest ha declarado que la Activity lo gestiona ella. Si no, no llega nunca |
onTopResumedActivityChanged | La Activity gana o pierde el «primer plano» (útil en multiventana; existe desde Android 10, API 29) |
La última fila habla de un nivel de API, el número que identifica cada versión de Android (Android 10 es la API 29, Android 9 la 28; lo viste en Instalar Android Studio y el SDK). Cuenta: siete de la primera tabla, onSaveInstanceState y onRestoreInstanceState del párrafo anterior, y tres de la segunda tabla. Son doce callbacks, y en Marcador sobrescribirás los doce.
Pruébalo antes de escribir código
Antes de tocar MainActivity, juega con el ciclo. Es tu ejercicio de esta lección, y se hace en tres tiempos. Primero predice, antes de pulsar nada, y apunta la respuesta en un papel:
- Si pulsas Home y vuelves a abrir la app, ¿qué callbacks saldrán, y en qué orden?
- Si rotas el móvil, ¿será la misma instancia de la pantalla (el mismo objeto) o una nueva? En el registro simulado lo sabrás por un código tras la
@, un número que identifica a cada instancia: si cambia, es otra. - Si abres un diálogo de tu propia app (una ventanita de aviso encima de la pantalla), ¿saldrá algún callback?
Después pruébalo: abre Marcador, ve a Home y vuelve, pon una pantalla encima, rota el móvil. Cada callback se enciende en la escalera (bajando a la izquierda, subiendo a la derecha) y se escribe en un registro simulado, con el mismo formato que verás luego en tu emulador. Con Paso a paso se ve además el momento, entre onStart y onResume, en que la pantalla ya se ve pero aún no recibe toques.
Empieza abriendo Marcador desde su icono.
Y por último compara con tus respuestas:
Ver solución
- Con Home salen
onPause,onStop,onRestart,onStartyonResume, y la pantalla no se destruye. - Al rotar, sí cambia el código tras la
@: el sistema crea otra instancia. - Un diálogo de tu propia app no dispara ningún callback: es una ventana de la misma Activity, que sigue en primer plano (ver las aclaraciones bajo la tabla).
Logcat: donde escribe tu app
Una app no tiene consola como el programa de :torneo: nadie ve su println. Para mirar qué hace por dentro, Android lleva un registro (un diario) donde escribe el sistema, las librerías y tu app, y Logcat es la herramienta que lo enseña. Log.d(tag, mensaje) escribe un mensaje de nivel Debug (depuración) con un tag (etiqueta) que eliges tú. El tag es el filtro para encontrar tus líneas entre miles del sistema. Aquí será Ciclo.
import android.util.Log // la clase de Android que escribe en el registro
// Dentro de cualquier clase de la app:
Log.d("Ciclo", "onCreate") // aparece en Logcat como «D Ciclo : onCreate»Se ve de dos formas:
- En Android Studio, abriendo la ventana Logcat con View → Tool Windows → Logcat. Tiene un campo de búsqueda arriba: escribe
tag:Cicloy solo quedan las líneas con ese tag. Sin filtro verás muchísimas líneas del sistema, y es normal. Logcat solo muestra algo cuando hay una app ejecutándose en un emulador o un móvil: lo harás en el paso de abajo. - En la terminal, con
adb logcat -s Ciclo(-ssignifica «solo este tag»).adbes la herramienta que habla con el emulador; la instalaste en Instalar Android Studio y el SDK. Desde aquí y en todo el módulo vas a escribir órdenes deadb: están reunidas, con lo que hace cada una, en la Chuleta deadbdel glosario. Ábrela en otra pestaña y vuelve a ella cuando una orden no te suene.
Error típico: creer que onDestroy significa «el usuario se fue»
onDestroy se llama en dos situaciones distintas (mira la cita de arriba). Si tu código asume la primera, se ejecutará también cuando el sistema solo está recreando la pantalla.
override fun onDestroy() {
super.onDestroy()
anotar("el usuario ha terminado con la pantalla") // falso al rotar
}(anotar es el ayudante que escribirá cada línea en Logcat; lo escribes en el paso de abajo.)
override fun onDestroy() {
super.onDestroy()
// isFinishing es true si el usuario cierra; isChangingConfigurations, si es una recreación.
anotar("onDestroy (isFinishing=$isFinishing, isChangingConfigurations=$isChangingConfigurations)")
}Falla porque el sistema destruye y vuelve a crear la Activity cuando giras el móvil. En la salida real de más abajo, al rotar, onDestroy dice isFinishing=false, isChangingConfigurations=true.
Paso · Registra el ciclo en MainActivity
Objetivo: que MainActivity sobrescriba los doce callbacks del ciclo de vida y cada uno deje una línea en Logcat.
- En la vista Android, despliega
app → kotlin+java → com.sebastiannarvaez.marcadory haz doble clic enMainActivity.kt. Es laMainActivityque dejaste en La Activity: se queda como está, y le añades los callbacks. - Los
importlos puedes escribir a mano o dejar que Android Studio los añada: con el cursor sobre un nombre en rojo, pulsa Option+Enter (Alt+Enter en Windows y Linux) y elige el import. Si te ofrece varios, elige el deandroidx.
Piezas nuevas de este paso:
super.onXxx(…): llama al método de la clase madre, que hace su trabajo de base (por ejemplo, restaurar el estado de las vistas, los elementos de pantalla del sistema clásico de Android: botones, textos, imágenes). Va primero en todos los callbacks. En los del ciclo de vida, sin esa llamada Android lanzaSuperNotCalledException; en los demás no avisa, pero se pierde lo que hace la clase madre.Log.d(tag, mensaje);System.identityHashCode(this).toString(16): un número que identifica esta instancia de la Activity, en hexadecimal. Hexadecimal es otra forma de escribir números, con 16 cifras (del 0 al 9 y de la a a la f) en lugar de 10; aquí solo sirve para que el número sea corto, yc764bd7es simplemente un nombre para esta instancia.onSaveInstanceState(outState: Bundle)yonTopResumedActivityChanged(isTopResumedActivity: Boolean): dos de los doce callbacks con parámetros nuevos para ti. La firma de una función es su nombre más la lista de lo que recibe y devuelve; para sobrescribir un callback hay que copiarla exacta. El primero recibe unBundle, el paquete donde guardas datos antes de que la Activity se destruya (el tema de la próxima lección); el segundo recibe unBooleanque dice si la Activity tiene el primer plano.javaClass.simpleName: el nombre de la clase,MainActivity.isFinishingeisChangingConfigurations: propiedades de la Activity (ver el error típico).
Así se ven juntas esas piezas en una Activity de prueba, con otro callback y otra etiqueta que los de Marcador:
class PruebaActivity : ComponentActivity() { // hereda de la Activity de Jetpack
override fun onPause() { // override: sustituye el método de la clase madre
super.onPause() // primero, el trabajo de base de la clase madre
apuntar("onPause") // después, lo tuyo: una línea en Logcat
}
private fun apuntar(texto: String) { // private: solo se usa dentro de esta clase
val clase = javaClass.simpleName // "PruebaActivity"
val instancia = System.identityHashCode(this).toString(16) // número de ESTA instancia, en hexadecimal
Log.d("Prueba", "$clase@$instancia $texto") // tag "Prueba"; se filtra con tag:Prueba
}
}Si pulsas Home con PruebaActivity en pantalla y filtras Logcat por tag:Prueba, la línea que sale es esta (el código tras la @ será otro):
D Prueba : PruebaActivity@1a2b3c onPauseEscribir a mano doce métodos con la firma exacta es lento y fácil de equivocar, y nadie lo hace así: Android Studio los genera. Con el cursor dentro de la clase MainActivity (entre sus llaves, en una línea vacía), abre Code → Override Methods… (el atajo es Ctrl+O, con la tecla Control también en Mac). Sale una lista con todo lo que se puede sobrescribir: escribe el nombre para filtrarla, marca varios con Cmd+clic y pulsa OK. El IDE escribe cada override fun … con su firma correcta y la llamada a super; tú solo añades la línea anotar("…").
Intenta escribirlo tú:
- Que cada callback de la primera tabla deje una línea en Logcat, con el tag
Ciclo, que diga el nombre de la clase, el número de esta instancia y el nombre del callback. - Esa línea la escribe un método privado de la clase,
anotar, que reciben todos. onCreateya está escrito: le añades su línea, que además dice si llegó estado guardado («no» la primera vez).- El resto los generas con el menú. Los otros cinco (
onSaveInstanceState,onRestoreInstanceState,onNewIntent,onConfigurationChangedyonTopResumedActivityChanged) tienen firmas con tipos que aún no conoces: genéralos igual, o cópialos del bloque de abajo, que lleva los doce completos.
Lo que debes ver, cuando lo ejecutes en el paso siguiente: la tabla en pantalla y, en Logcat, estas tres líneas en este orden (el código tras la @ cambia en cada ejecución). En la ventana Logcat de Android Studio verás delante de cada línea la fecha, la hora y el nombre del paquete; el mensaje del final es el mismo:
D Ciclo : MainActivity@c764bd7 onCreate (estado guardado: no)
D Ciclo : MainActivity@c764bd7 onStart
D Ciclo : MainActivity@c764bd7 onResumeEl título del bloque de abajo es la ruta del fichero en tu proyecto; en la vista Android está en app → kotlin+java.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
package com.sebastiannarvaez.marcador
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.sebastiannarvaez.marcador.torneo.Ejemplo
// Una Activity es UNA pantalla que el sistema crea, pausa y destruye cuando
// quiere. No la construyes tu con `MainActivity()`: la crea Android, y te avisa
// de cada cambio llamando a estos metodos (los «callbacks» del ciclo de vida).
//
// Aqui cada callback deja una linea en Logcat con el tag `Ciclo`. Para verlas:
// adb logcat -s Ciclo
// o en Android Studio: Logcat, y filtra por `tag:Ciclo`.
class MainActivity : ComponentActivity() {
// `super.onXxx()` primero en todos: la clase madre hace su trabajo (por
// ejemplo, restaurar el estado de las vistas) y `ComponentActivity` avisa a
// Compose y a los componentes de Jetpack. Sin la llamada a `super`, Android
// lanza SuperNotCalledException.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
anotar("onCreate (estado guardado: ${if (savedInstanceState == null) "no" else "si"})")
// Prueba de que `:app` ve el modulo `:torneo`: la tabla del torneo de ejemplo.
val tabla = Ejemplo.torneoConPartidos().tablaDePosiciones()
setContent {
MaterialTheme {
Column(Modifier.padding(24.dp)) {
Text("Marcador")
tabla.forEach { fila ->
Text("${fila.equipo.nombre}: ${fila.puntos} pts")
}
}
}
}
}
override fun onStart() {
super.onStart()
anotar("onStart")
}
// Solo se llama al VOLVER a una Activity que estaba parada (no la primera vez).
override fun onRestart() {
super.onRestart()
anotar("onRestart")
}
override fun onResume() {
super.onResume()
anotar("onResume")
}
override fun onPause() {
super.onPause()
anotar("onPause")
}
override fun onStop() {
super.onStop()
anotar("onStop")
}
override fun onDestroy() {
super.onDestroy()
// isFinishing: true si el usuario cierra la pantalla (atras); false si
// el sistema la destruye (rotacion, falta de memoria).
anotar("onDestroy (isFinishing=$isFinishing, isChangingConfigurations=$isChangingConfigurations)")
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
anotar("onSaveInstanceState")
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
anotar("onRestoreInstanceState")
}
// Solo llega si la Activity ya existe y le mandan un Intent nuevo.
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
anotar("onNewIntent")
}
// Solo se llama si el Manifest declara android:configChanges para ese cambio.
// Sin esa declaracion, Android destruye y recrea la Activity y este callback
// no llega nunca.
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
anotar("onConfigurationChanged")
}
override fun onTopResumedActivityChanged(isTopResumedActivity: Boolean) {
super.onTopResumedActivityChanged(isTopResumedActivity)
anotar("onTopResumedActivityChanged($isTopResumedActivity)")
}
private fun anotar(mensaje: String) {
// Log.d(tag, mensaje): nivel Debug. El tag es el filtro para encontrarlo.
Log.d("Ciclo", "${javaClass.simpleName}@${System.identityHashCode(this).toString(16)} $mensaje")
}
}Paso · Arranca y lee las tres primeras líneas
Objetivo: ver en Logcat cómo empieza la vida de una Activity.
- Pulsa Run (con
appen el selector de configuración y el emulador arrancado, como en La Activity). Android Studio reinstala la app encima de la anterior; tarda segundos. En la pantalla debe seguir la tabla de antes. - Abre View → Tool Windows → Logcat. Escribe
tag:Cicloen el campo de búsqueda de arriba. Deben salir, de arriba abajo,onCreate (estado guardado: no),onStartyonResume, con el mismo código tras la@: así empieza la vida de una Activity.
Paso · Provoca el ciclo y léelo
Objetivo: ver en Logcat los callbacks de un arranque, de un «Home y volver» y de una rotación.
Provócalo en tres tandas. Antes de empezar, cierra la app del todo con adb shell am force-stop com.sebastiannarvaez.marcador, para que el siguiente arranque sea desde cero. El botón Atrás no sirve para esto: desde Android 12, en la pantalla principal de una app abierta desde su icono, Atrás no la destruye, la manda al fondo como Home (lo verás en el laboratorio de arriba). Los comandos de adb van desde la Terminal de Android Studio (o cualquier terminal) y están explicados en la Chuleta de adb:
- Vacía Logcat:
adb logcat -c-
A mano en el emulador: abre la app desde el icono, pulsa Home y vuelve a abrirla desde el icono (o desde la lista de apps recientes: en el emulador, el botón cuadrado de la barra de navegación, o deslizar hacia arriba desde abajo y mantener un momento; toca la tarjeta de Marcador).
-
Gira el móvil con los botones del emulador. La barra de herramientas pegada al borde de la ventana del emulador tiene dos botones de giro: un icono de un móvil con una flecha curva, uno para girar a la izquierda y otro a la derecha (al pasar el ratón dice algo como «Rotate»). Púlsalo una vez: el móvil queda en horizontal. Después lee la salida:
adb logcat -d -s Ciclo-d vuelca lo que ya hay y termina, y -s Ciclo deja solo el tag Ciclo (también puedes leerlo en la ventana Logcat de Android Studio con tag:Ciclo). Cuando termines de leerla, pulsa el otro botón de giro para dejar el móvil en vertical. Si no encuentras los botones, la Chuleta de adb trae las órdenes de settings que giran el móvil desde la terminal. Verás algo como esto con la versión de Android que tenga tu dispositivo virtual (aquí se han quitado la fecha y los números de proceso del principio de cada línea; los códigos tras la @ cambian en cada ejecución):
D Ciclo : MainActivity@c764bd7 onCreate (estado guardado: no)
D Ciclo : MainActivity@c764bd7 onStart
D Ciclo : MainActivity@c764bd7 onResume
D Ciclo : MainActivity@c764bd7 onTopResumedActivityChanged(true)
D Ciclo : MainActivity@c764bd7 onTopResumedActivityChanged(false)
D Ciclo : MainActivity@c764bd7 onPause
D Ciclo : MainActivity@c764bd7 onStop
D Ciclo : MainActivity@c764bd7 onSaveInstanceState
D Ciclo : MainActivity@c764bd7 onRestart
D Ciclo : MainActivity@c764bd7 onStart
D Ciclo : MainActivity@c764bd7 onResume
D Ciclo : MainActivity@c764bd7 onTopResumedActivityChanged(true)
D Ciclo : MainActivity@c764bd7 onTopResumedActivityChanged(false)
D Ciclo : MainActivity@c764bd7 onPause
D Ciclo : MainActivity@c764bd7 onStop
D Ciclo : MainActivity@c764bd7 onSaveInstanceState
D Ciclo : MainActivity@c764bd7 onDestroy (isFinishing=false, isChangingConfigurations=true)
D Ciclo : MainActivity@6929fbc onCreate (estado guardado: si)
D Ciclo : MainActivity@6929fbc onStart
D Ciclo : MainActivity@6929fbc onRestoreInstanceState
D Ciclo : MainActivity@6929fbc onResume
D Ciclo : MainActivity@6929fbc onTopResumedActivityChanged(true)Tres cosas que leer ahí:
onSaveInstanceStatesale después deonStop. Así ocurre desde Android 9 (API 28); antes se llamaba antes deonStop.- Al rotar, el código tras la
@cambia (c764bd7→6929fbc): es otra instancia. YonCreatediceestado guardado: si, porque el sistema le entrega el paquete que guardó. onConfigurationChangedno aparece. Solo se llamaría si el Manifest declaraseandroid:configChangespara ese cambio; sin esa declaración el sistema recrea la Activity.onTopResumedActivityChanged(Android 10) no es del ciclo básico: sale porque hemos sobrescrito todos los callbacks. Entre los dos primerosonResumeestá lo que hizo tu Home:onPause,onStop,onRestartyonStart, con la misma instancia.
Un detalle del código: si no llamas a super en un callback del ciclo de vida (onCreate, onStart, onResume, onPause, onStop, onRestart, onDestroy), Android lanza SuperNotCalledException, según la documentación de las Activity. En los otros callbacks no salta ningún error, pero se pierde en silencio lo que hace la clase madre, por ejemplo guardar el estado de las vistas.
Punto de control
MainActivity.ktse abre sin nada en rojo, con sus doce callbacks, y./gradlew :app:assembleDebugtermina enBUILD SUCCESSFUL.- Run reinstala Marcador en el emulador y sigue mostrando «Marcador» y las cuatro líneas de equipo y puntos.
- Logcat, con
tag:Ciclo(oadb logcat -d -s Cicloen la terminal), muestraonCreate (estado guardado: no),onStartyonResumecon el mismo código tras la@, y al rotar, un código distinto. - Tras girar con los botones del emulador, Logcat muestra
onDestroy (isFinishing=false, isChangingConfigurations=true)y el emulador vuelve a estar en vertical.
Comprueba lo que entendiste
6 preguntas · necesitas 5 para completar la lección
1.Abres la app, pulsas Home y vuelves a abrirla. ¿Cuáles de los callbacks del ciclo básico (los de la escalera, sin contar los de guardado) salen entre el primer
onResumey el segundo?2.Giras el móvil y en Logcat ves
onDestroy (isFinishing=false, isChangingConfigurations=true). ¿Qué significa?3.Quieres ver solo los mensajes que escribe tu Activity con
Log.d("Ciclo", …). ¿Qué haces?4.En Logcat ves que
onSaveInstanceStatesale justo después deonStop. ¿Es un error de tu código?5.Giras el emulador con
MainActivitysobrescribiendoonConfigurationChanged, pero ese callback no aparece en Logcat. ¿Por qué?6.Sobrescribes
onStartpero olvidas la líneasuper.onStart(). ¿Qué ocurre al ejecutar la app?