Nivel inicial14 min de lectura · Publicado el

Ciclo de vida de Activities en Android, con Kotlin

Ciclo de vida de una Activity en Android: con onResume la tienda atiende, con onPause cuelga el cartel y con onStop baja la persiana. Activity: en pausa aún se verubentejera.com onResume()atiendeonPause()se ve, no atiendeonStop()no se ve
onResume es la tienda abierta y atendiendo; onPause, el cartel de «vuelvo en cinco minutos»: se ve, pero no atiende; onStop, la persiana bajada: ya no se ve desde la calle.

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

Ciclo de vida de una Activity, de arriba abajo: onCreate() (creada), onStart() (visible), onResume() (en primer plano), onPause() (visible, sin foco), onStop() (no visible) y onDestroy() (destruida). Desde onPause() vuelve a onResume(), y desde onStop() vuelve con onRestart() a onStart().
Lee de arriba abajo: cada caja de la columna central es un callback y debajo, el estado en que deja la Activity. Las flechas laterales son las dos vueltas: de 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

  1. Guardar datos en onPause. Es demasiado breve. Guarda cuando el usuario actúa o, como tarde, en onStop.
  2. Confiar en onDestroy. Si Android mata el proceso, puede no llamarse.
  3. Guardar el estado en la Activity. Se pierde al rotar. Llévalo a un ViewModel o a rememberSaveable.
  4. Dar por hecho que onPause significa «no se ve». En multiventana, una Activity pausada puede seguir viéndose entera. Para lo visual, usa onStop.
  5. Encender en un callback y apagar en otro que no es su pareja. onStart va con onStop; onResume, con onPause.

Fuentes

Comprobado el 7 de noviembre de 2026 en la documentación oficial de Android:

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