Ciclo de vida de Activities en Android, con Kotlin
Abres una pantalla, giras el móvil y el contador vuelve a cero. O sales a mirar un mensaje, vuelves y el vídeo sigue sonando de fondo. Casi siempre es el mismo problema: el código no respeta el ciclo de vida de la Activity, es decir, los estados por los que pasa una pantalla desde que se crea hasta que desaparece.
Piensa en una tienda de barrio. Por la mañana se monta el local (onCreate), se sube la persiana (onStart) y se empieza a atender (onResume). Si el dependiente sale un momento y cuelga el cartel de «vuelvo en cinco minutos», la tienda sigue a la vista pero no atiende (onPause). Al bajar la persiana ya no se ve desde la calle (onStop). Y si cierran el negocio, se desmonta todo (onDestroy). El símil se queda corto en una cosa: Android puede «cerrar la tienda» sin avisar si necesita memoria. Lo verás más abajo.
Qué es una Activity en Android
Una Activity (en español, a veces «actividad») es una clase que representa una pantalla con la que el usuario interactúa. Cada app tiene al menos una, la que se abre al pulsar su icono.
Durante años, cada pantalla era una Activity distinta. Hoy la documentación oficial recomienda que las apps con Jetpack Compose tengan una sola Activity y cambien de pantalla con la librería de navegación. Aun así, esa Activity sigue teniendo su ciclo de vida, y entenderlo es lo que evita los fallos del principio.
Si nunca has creado una, empieza por Hola Mundo con Android Studio y Kotlin.
Los estados del ciclo de vida, de un vistazo
onPause() a onResume(), y de onStop() a onStart() pasando por onRestart(), la caja discontinua, que solo se llama al volver.La documentación oficial tiene el diagrama completo del ciclo de vida, que incluye además la muerte del proceso cuando el sistema necesita memoria.
Cada callback: cuándo se llama y qué hacer ahí
| Callback | Cuándo se llama | Qué hacer ahí |
|---|---|---|
onCreate |
Al crear la Activity. Una sola vez por instancia. Es obligatorio implementarlo. | Lo que se hace una vez: llamar a setContent (Compose) o setContentView (vistas), conectar el ViewModel y registrar observadores. |
onStart |
Justo antes de que la pantalla sea visible. | Preparar lo que debe funcionar mientras se ve. |
onResume |
Cuando pasa a primer plano y el usuario puede tocarla. | Arrancar lo que solo tiene sentido con la pantalla activa, como la vista previa de la cámara. |
onPause |
Primera señal de que el usuario se va. También si otra ventana toma el foco en multiventana. | Pausar lo que no debe seguir sin foco. Es muy breve: no guardes datos, ni llames a la red, ni hagas operaciones de base de datos aquí. |
onStop |
Cuando la pantalla deja de verse. | Liberar lo que no hace falta sin verla y guardar datos si no hubo un momento mejor antes. |
onRestart |
Cuando vuelves a una Activity parada. Siempre le sigue onStart. |
Poca cosa: normalmente basta con onStart. |
onDestroy |
Antes de destruirla: porque termina (finish() o el usuario la cierra) o por un cambio de configuración, como rotar. |
Liberar lo que quede. No confíes en él para guardar nada: si Android mata el proceso, puede no llamarse. |
Escenarios típicos
Esto es lo que ocurre en los casos que más dudas generan. Todos están comprobados en la documentación oficial de Android (enlaces en las fuentes).
| Qué haces | Qué callbacks se llaman |
|---|---|
| Abrir la app | onCreate → onStart → onResume |
| Pulsar Inicio (o ir a Recientes) | onPause → onStop |
| Volver a la app desde Inicio | onRestart → onStart → onResume |
| Rotar la pantalla | onPause → onStop → onDestroy y, en una instancia nueva, onCreate → onStart → onResume |
| Se abre encima algo que la tapa solo en parte (por ejemplo, una Activity semitransparente con aspecto de diálogo) | onPause; al cerrarse, onResume |
| La Activity A abre la B | A onPause → B onCreate → B onStart → B onResume → A onStop |
| Pulsar Atrás en una pantalla secundaria | onPause → onStop → onDestroy |
| Pulsar Atrás en la primera pantalla de la app, en Android 12 o superior | onPause → onStop: la app pasa a segundo plano, como con Inicio. En Android 11 o inferior, además onDestroy. |
| Android necesita memoria y la app está en segundo plano | Mata el proceso entero. onDestroy puede no llamarse. |
Rotar la pantalla: por qué se pierde el estado
Girar el móvil es un cambio de configuración, igual que cambiar el idioma o entrar en multiventana. Android destruye la Activity y crea otra nueva con la configuración nueva. Todo lo que guardabas en variables de la Activity desaparece con ella.
Volviendo a la tienda: es como si la reformaran de noche. Lo que estaba en el mostrador se tira; lo que estaba en el almacén sigue ahí. Ese almacén es el ViewModel.
Pulsar Atrás en Android 12 o superior
Antes, pulsar Atrás en la primera pantalla de una app cerraba la Activity. Desde Android 12, el sistema la manda a segundo plano, igual que el botón de Inicio. Así la app se reabre más rápido. Si tu código daba por hecho que Atrás siempre llama a onDestroy, revísalo. Tienes más sobre este botón en Botón Back y Handler en Android.
Compruébalo tú: el orden de los callbacks en la consola
El artículo original lo mostraba con una Activity que lanzaba un Toast en cada callback. Al abrirla, aparecían uno tras otro: primero onCreate y luego onStart.
Si no tienes un emulador a mano, este programa en Kotlin puro reproduce el orden que describe la documentación. Es una simulación: no usa Android, solo imita los callbacks para que veas los escenarios en la consola. Los check comprueban que cada escenario sigue el orden documentado.
// Simulación en Kotlin puro: NO es Android. Reproduce el orden de callbacks
// que documenta developer.android.com para ver los escenarios en la consola.
val registro = mutableListOf<String>()
class ContadorViewModel { var pulsaciones = 0 } // sobrevive a la rotación
class PantallaActivity(val nombre: String, val viewModel: ContadorViewModel) {
var pulsacionesEnLaActivity = 0 // se pierde al recrearla
private fun log(callback: String) { registro += "$nombre.$callback" }
fun onCreate() = log("onCreate")
fun onStart() = log("onStart")
fun onResume() = log("onResume")
fun onPause() = log("onPause")
fun onStop() = log("onStop")
fun onRestart() = log("onRestart")
fun onDestroy() = log("onDestroy")
}
fun abrir(a: PantallaActivity) { a.onCreate(); a.onStart(); a.onResume() }
fun irAlFondo(a: PantallaActivity) { a.onPause(); a.onStop() }
fun volver(a: PantallaActivity) { a.onRestart(); a.onStart(); a.onResume() }
fun escenario(titulo: String, esperado: List<String>, accion: () -> Unit) {
registro.clear()
accion()
println("$titulo:\n ${registro.joinToString(" → ")}")
check(registro == esperado) { "Orden inesperado en «$titulo»: $registro" }
}
fun main() {
val viewModel = ContadorViewModel()
var main = PantallaActivity("Main", viewModel)
escenario("1. Abrir la app", listOf("Main.onCreate", "Main.onStart", "Main.onResume")) { abrir(main) }
escenario("2. Pulsar Inicio", listOf("Main.onPause", "Main.onStop")) { irAlFondo(main) }
escenario("3. Volver a la app", listOf("Main.onRestart", "Main.onStart", "Main.onResume")) { volver(main) }
main.pulsacionesEnLaActivity = 3
viewModel.pulsaciones = 3
escenario(
"4. Rotar la pantalla",
listOf("Main.onPause", "Main.onStop", "Main.onDestroy", "Main.onCreate", "Main.onStart", "Main.onResume"),
) {
irAlFondo(main); main.onDestroy()
main = PantallaActivity("Main", viewModel) // nueva instancia, mismo ViewModel
abrir(main)
}
println(" Contador en la Activity: ${main.pulsacionesEnLaActivity} | en el ViewModel: ${viewModel.pulsaciones}")
escenario("5. Se abre encima una activity semitransparente", listOf("Main.onPause")) { main.onPause() }
escenario(" Se cierra", listOf("Main.onResume")) { main.onResume() }
val detalle = PantallaActivity("Detalle", ContadorViewModel())
escenario(
"6. Main abre Detalle",
listOf("Main.onPause", "Detalle.onCreate", "Detalle.onStart", "Detalle.onResume", "Main.onStop"),
) { main.onPause(); abrir(detalle); main.onStop() }
escenario("7. Atrás en la primera pantalla (Android 12+)", listOf("Main.onPause", "Main.onStop")) { irAlFondo(main) }
}
Guárdalo como CicloDeVida.kt, compílalo y ejecútalo:
kotlinc CicloDeVida.kt -include-runtime -d ciclo.jar
java -jar ciclo.jar
Salida:
1. Abrir la app:
Main.onCreate → Main.onStart → Main.onResume
2. Pulsar Inicio:
Main.onPause → Main.onStop
3. Volver a la app:
Main.onRestart → Main.onStart → Main.onResume
4. Rotar la pantalla:
Main.onPause → Main.onStop → Main.onDestroy → Main.onCreate → Main.onStart → Main.onResume
Contador en la Activity: 0 | en el ViewModel: 3
5. Se abre encima una activity semitransparente:
Main.onPause
Se cierra:
Main.onResume
6. Main abre Detalle:
Main.onPause → Detalle.onCreate → Detalle.onStart → Detalle.onResume → Main.onStop
7. Atrás en la primera pantalla (Android 12+):
Main.onPause → Main.onStop
Fíjate en el escenario 4: tras rotar, el contador de la Activity vuelve a 0 y el del ViewModel sigue en 3.
Verlo en una app real: Logcat y un observador del ciclo de vida
En el artículo original, cada callback se sobrescribía en una clase base con un Toast, y la Activity heredaba de ella:
open class LifeCycleEventsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Toast.makeText(this, "onCreate", Toast.LENGTH_SHORT).show()
}
override fun onStart() {
super.onStart()
Toast.makeText(this, "onStart", Toast.LENGTH_SHORT).show()
}
override fun onResume() {
super.onResume()
Toast.makeText(this, "onResume", Toast.LENGTH_SHORT).show()
}
override fun onPause() {
super.onPause()
Toast.makeText(this, "onPause", Toast.LENGTH_SHORT).show()
}
override fun onStop() {
super.onStop()
Toast.makeText(this, "onStop", Toast.LENGTH_SHORT).show()
}
override fun onRestart() {
super.onRestart()
Toast.makeText(this, "onRestart", Toast.LENGTH_SHORT).show()
}
override fun onDestroy() {
super.onDestroy()
Toast.makeText(this, "onDestroy", Toast.LENGTH_SHORT).show()
}
}
Funciona, pero hoy hay dos formas mejores:
- Logcat en lugar de Toast. Un Toast se ve un par de segundos y uno detrás de otro; en Logcat (la ventana de registros de Android Studio) tienes todos los mensajes en orden y puedes filtrarlos por etiqueta.
- Un observador en lugar de herencia. Jetpack Lifecycle te deja escribir una clase aparte que escucha el ciclo de vida de cualquier Activity. No obliga a heredar de nada y se reutiliza en todas las pantallas.
import android.util.Log
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
class RegistroDeCiclo(private val etiqueta: String) : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) { Log.d(etiqueta, "onCreate") }
override fun onStart(owner: LifecycleOwner) { Log.d(etiqueta, "onStart") }
override fun onResume(owner: LifecycleOwner) { Log.d(etiqueta, "onResume") }
override fun onPause(owner: LifecycleOwner) { Log.d(etiqueta, "onPause") }
override fun onStop(owner: LifecycleOwner) { Log.d(etiqueta, "onStop") }
override fun onDestroy(owner: LifecycleOwner) { Log.d(etiqueta, "onDestroy") }
}
Lo registras una vez en onCreate:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(RegistroDeCiclo("CicloDeVida"))
setContent { Contador() }
}
// Lifecycle no tiene un evento para onRestart: si quieres verlo, sobrescríbelo.
override fun onRestart() {
super.onRestart()
Log.d("CicloDeVida", "onRestart")
}
}
Ejecuta la app, escribe CicloDeVida en el filtro de Logcat, y prueba a rotar, pulsar Inicio y volver.
Cómo encaja hoy con Jetpack: ViewModel y Compose
La documentación actual de Android lo resume así: no metas lógica de negocio en onStart u onResume. Deja que cada pieza se ocupe de su parte del ciclo de vida.
La Activity monta la pantalla; el estado vive fuera de ella.
| Necesitas… | Usa | Sobrevive a rotar | Sobrevive a que Android mate el proceso |
|---|---|---|---|
| Estado de la pantalla y su lógica | ViewModel |
Sí | No, salvo lo que guardes con SavedStateHandle |
| Estado pequeño de la interfaz (texto escrito, una pestaña) | rememberSaveable |
Sí | Sí |
| Datos del usuario que no pueden perderse | Almacenamiento local (por ejemplo, una base de datos) | Sí | Sí |
| Estado temporal de un composable | remember |
No | No |
Así se ve con un contador en Compose:
class ContadorViewModel : ViewModel() {
private val _pulsaciones = MutableStateFlow(0)
val pulsaciones: StateFlow<Int> = _pulsaciones.asStateFlow()
fun pulsar() = _pulsaciones.update { it + 1 }
}
@Composable
fun Contador(viewModel: ContadorViewModel = viewModel()) {
// Deja de recoger el flujo cuando la pantalla no se ve y vuelve al verse.
val pulsaciones by viewModel.pulsaciones.collectAsStateWithLifecycle()
var sinGuardar by remember { mutableIntStateOf(0) }
Button(onClick = { viewModel.pulsar(); sinGuardar++ }) {
Text("ViewModel: $pulsaciones · remember: $sinGuardar")
}
}
Pulsa varias veces y rota: el número del ViewModel se mantiene y el de remember vuelve a 0. Es lo mismo que mostraba la simulación, ahora en Android.
Y si un composable necesita encender algo al verse y apagarlo al dejar de verse, como la ubicación o la cámara, no hace falta tocar la Activity. LifecycleStartEffect lo empareja con onStart/onStop, y LifecycleResumeEffect, con onResume/onPause:
@Composable
fun Ubicacion(seguidor: SeguidorDeUbicacion) {
LifecycleStartEffect(seguidor) {
seguidor.empezar()
onStopOrDispose { seguidor.parar() }
}
}
SeguidorDeUbicacion es una clase tuya de ejemplo. Lo importante es la forma: lo que se enciende y lo que se apaga quedan juntos, y no puedes olvidarte de la mitad.
Hay otra ventaja: un ViewModel es una clase normal de Kotlin, así que su lógica se puede probar con tests unitarios sin arrancar un emulador. Si quieres empezar por ahí, mira TDD: desarrollo dirigido por tests.
Errores frecuentes
- Guardar datos en
onPause. Es demasiado breve. Guarda cuando el usuario actúa o, como tarde, enonStop. - Confiar en
onDestroy. Si Android mata el proceso, puede no llamarse. - Guardar el estado en la Activity. Se pierde al rotar. Llévalo a un
ViewModelo arememberSaveable. - Dar por hecho que
onPausesignifica «no se ve». En multiventana, una Activity pausada puede seguir viéndose entera. Para lo visual, usaonStop. - Encender en un callback y apagar en otro que no es su pareja.
onStartva cononStop;onResume, cononPause.
Fuentes
Comprobado el 7 de noviembre de 2026 en la documentación oficial de Android:
- The activity lifecycle: qué hacer en cada callback, qué no hacer en
onPause, multiventana, muerte del proceso y orden al abrir una Activity desde otra. - Handle activity state changes: rotación, diálogos y activities que tapan la pantalla, Inicio y Atrás.
- Tasks and the back stack: comportamiento de Atrás en la primera pantalla en Android 12 y superior.
- Referencia de
Activity:onRestartsiempre va seguido deonStart. - Lifecycle in Jetpack Compose y componentes lifecycle-aware con vistas:
DefaultLifecycleObserver,LifecycleStartEffect,LifecycleResumeEffectycollectAsStateWithLifecycle. - Notas de versión de Lifecycle: versión estable 2.11.0 (junio de 2026);
@OnLifecycleEventobsoleta en favor deDefaultLifecycleObserveroLifecycleEventObserver.
La simulación en Kotlin puro se ha ejecutado con Kotlin 2.4.21 sobre Java 21.0.13 (imagen Docker eclipse-temurin:21) y la salida es la que aparece arriba. Los fragmentos de Android (RegistroDeCiclo, MainActivity, Contador y Ubicacion) no se han ejecutado en un dispositivo: siguen las APIs de la documentación enlazada, con Lifecycle 2.11.0 (artefactos lifecycle-runtime-compose y lifecycle-viewmodel-compose).