¿Qué es una Activity y por qué importa?
Una Activity es un componente de Android que representa una pantalla con la que el usuario interactúa. En una app típica, cada pantalla principal suele estar respaldada por una Activity (o por una sola Activity que aloja varias pantallas si usas navegación moderna). La Activity es responsable de:
- Crear y mostrar la interfaz (por ejemplo, con View Binding o con Compose).
- Responder a eventos del sistema (rotación, volver a primer plano, pasar a segundo plano).
- Coordinar la navegación hacia otras pantallas.
El comportamiento de una Activity está gobernado por su ciclo de vida, una secuencia de callbacks que Android invoca según el estado de la app y la interacción del usuario.
Ciclo de vida de Activity: estados y callbacks clave
Los callbacks más importantes son:
| Callback | Cuándo se llama | Uso típico |
|---|---|---|
onCreate | Al crear la Activity por primera vez (o tras recreación) | Inflar UI, inicializar dependencias, restaurar estado |
onStart | La Activity se vuelve visible | Preparar UI visible, registrar listeners ligeros |
onResume | La Activity está en primer plano y lista para interactuar | Iniciar animaciones, cámara/sensores, reanudar tareas |
onPause | La Activity pierde foco (otra pantalla parcial/total encima) | Guardar cambios rápidos, pausar recursos exclusivos |
onStop | La Activity deja de ser visible | Detener tareas costosas, liberar recursos |
onDestroy | Antes de destruirse (finalización o recreación) | Limpieza final (no siempre garantizado en cierres forzosos) |
Ejemplo base en Kotlin: registrar el ciclo de vida
Este ejemplo imprime en Logcat cada evento. Útil para entender qué ocurre al abrir otra pantalla, bloquear el teléfono o rotar.
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
private val tag = "Lifecycle"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(tag, "onCreate")
// setContentView(...)
}
override fun onStart() {
super.onStart()
Log.d(tag, "onStart")
}
override fun onResume() {
super.onResume()
Log.d(tag, "onResume")
}
override fun onPause() {
Log.d(tag, "onPause")
super.onPause()
}
override fun onStop() {
Log.d(tag, "onStop")
super.onStop()
}
override fun onDestroy() {
Log.d(tag, "onDestroy")
super.onDestroy()
}
}Cómo leer el orden típico
- Al abrir la pantalla:
onCreate → onStart → onResume - Al abrir otra Activity encima:
onPause(y si queda totalmente oculta:onStop) - Al volver:
onRestart → onStart → onResume(si estaba detenida) - Al rotar (si se recrea):
onPause → onStop → onDestroy → onCreate → onStart → onResume
Estado: qué se pierde y cómo preservarlo
Android puede destruir y recrear una Activity por múltiples razones (rotación, cambio de idioma, falta de memoria). Si guardas datos solo en variables, esos valores se perderán al recrearse.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
Para un estado básico de UI (texto escrito, contador, selección simple), puedes usar onSaveInstanceState y restaurar en onCreate (o onRestoreInstanceState).
Ejemplo: contador que sobrevive a rotación
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class CounterActivity : AppCompatActivity() {
private var counter: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(...)
counter = savedInstanceState?.getInt(KEY_COUNTER) ?: 0
// updateCounterUi(counter)
// buttonIncrement.setOnClickListener {
// counter++
// updateCounterUi(counter)
// }
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putInt(KEY_COUNTER, counter)
super.onSaveInstanceState(outState)
}
companion object {
private const val KEY_COUNTER = "key_counter"
}
}Buenas prácticas:
- Guarda solo lo necesario y que sea pequeño (primitivos, Strings, IDs).
- No guardes objetos grandes ni listas enormes en el Bundle.
- Para estado más complejo o datos de negocio, suele ser mejor un enfoque con ViewModel y persistencia; aquí nos enfocamos en estado básico de Activity.
Laboratorio: registrar eventos y preservar estado básico
Objetivo
Crear dos pantallas: una que muestre un “historial” de eventos del ciclo de vida y un contador. El historial y el contador deben sobrevivir a una rotación.
Paso 1: crear variables de estado
Usaremos:
events: lista de Strings con los eventos.counter: entero.
private var counter = 0
private val events = mutableListOf<String>()Paso 2: función para registrar y refrescar UI
La idea es centralizar el registro para no repetir código. En UI real, podrías mostrarlo en un TextView (por ejemplo, uniendo con saltos de línea) o en un RecyclerView.
private fun logEvent(name: String) {
events.add(name)
// lifecycleTextView.text = events.joinToString("\n")
// counterTextView.text = counter.toString()
}Paso 3: registrar callbacks del ciclo de vida
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(...)
counter = savedInstanceState?.getInt(KEY_COUNTER) ?: 0
val restored = savedInstanceState?.getStringArrayList(KEY_EVENTS)
if (restored != null) {
events.clear()
events.addAll(restored)
}
logEvent("onCreate")
// incrementButton.setOnClickListener {
// counter++
// logEvent("counter=$counter")
// }
}
override fun onStart() { super.onStart(); logEvent("onStart") }
override fun onResume() { super.onResume(); logEvent("onResume") }
override fun onPause() { logEvent("onPause"); super.onPause() }
override fun onStop() { logEvent("onStop"); super.onStop() }
override fun onDestroy() { logEvent("onDestroy"); super.onDestroy() }Paso 4: guardar estado en onSaveInstanceState
override fun onSaveInstanceState(outState: Bundle) {
outState.putInt(KEY_COUNTER, counter)
outState.putStringArrayList(KEY_EVENTS, ArrayList(events))
super.onSaveInstanceState(outState)
}
companion object {
private const val KEY_COUNTER = "key_counter"
private const val KEY_EVENTS = "key_events"
}Paso 5: probar escenarios
- Rotación: verifica que el contador y el historial se restauran.
- Ir a otra app y volver: observa
onPause/onStopy luegoonRestart/onStart/onResume(según el caso). - Abrir otra Activity: observa el orden de eventos en ambas pantallas.
Navegación entre pantallas: Intents vs navegación moderna
Hay dos enfoques comunes:
- Intents explícitos: directos y simples para ir de una Activity a otra.
- Navegación moderna: si tu proyecto usa una sola Activity con múltiples pantallas (por ejemplo, con Navigation Component o Compose Navigation), la navegación se gestiona con un grafo/rutas y argumentos tipados.
A continuación verás ambos enfoques, para que puedas aplicar el que encaje con tu proyecto.
Opción A: Navegación con Intents (Activity → Activity)
Crear la segunda pantalla
Supongamos una DetailActivity que recibe un texto y un ID.
Enviar datos con extras
import android.content.Intent
val intent = Intent(this, DetailActivity::class.java).apply {
putExtra(EXTRA_USER_ID, 42)
putExtra(EXTRA_MESSAGE, "Hola desde Main")
}
startActivity(intent)
companion object {
private const val EXTRA_USER_ID = "extra_user_id"
private const val EXTRA_MESSAGE = "extra_message"
}Recibir datos de forma segura (validando)
Los extras pueden faltar (por ejemplo, si alguien abre la Activity de otra forma). Valida y define valores por defecto o finaliza la pantalla si el dato es obligatorio.
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class DetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(...)
val userId = intent.getIntExtra(EXTRA_USER_ID, -1)
val message = intent.getStringExtra(EXTRA_MESSAGE)
if (userId == -1 || message == null) {
finish() // o muestra un error
return
}
// detailTextView.text = "id=$userId, msg=$message"
}
companion object {
private const val EXTRA_USER_ID = "extra_user_id"
private const val EXTRA_MESSAGE = "extra_message"
}
}Tip: encapsular la creación del Intent
Para reducir errores con claves, crea un método de fábrica.
class DetailActivity : AppCompatActivity() {
companion object {
private const val EXTRA_USER_ID = "extra_user_id"
private const val EXTRA_MESSAGE = "extra_message"
fun newIntent(context: android.content.Context, userId: Int, message: String): android.content.Intent {
return android.content.Intent(context, DetailActivity::class.java).apply {
putExtra(EXTRA_USER_ID, userId)
putExtra(EXTRA_MESSAGE, message)
}
}
}
}
// Uso:
startActivity(DetailActivity.newIntent(this, 42, "Hola"))Opción B: Navigation Component con Safe Args (argumentos tipados)
Si tu proyecto usa Navigation Component, Safe Args genera clases para navegar y pasar argumentos con tipos, evitando errores de claves y casts.
Definir argumentos en el grafo
En el destino (por ejemplo, detailFragment), define argumentos como userId (Int) y message (String). Conceptualmente se vería así:
<fragment
android:id="@+id/detailFragment"
android:name="com.example.DetailFragment">
<argument
android:name="userId"
app:argType="integer" />
<argument
android:name="message"
app:argType="string" />
</fragment>Navegar pasando argumentos (tipado)
// Desde el origen (por ejemplo, MainFragment)
val action = MainFragmentDirections.actionMainToDetail(
userId = 42,
message = "Hola con Safe Args"
)
findNavController().navigate(action)Recibir argumentos (tipado)
// En DetailFragment
private val args: DetailFragmentArgs by navArgs()
override fun onViewCreated(view: android.view.View, savedInstanceState: Bundle?) {
val userId = args.userId
val message = args.message
// textView.text = "id=$userId, msg=$message"
}Ventajas prácticas:
- Argumentos con tipos (menos errores en runtime).
- Autocompletado en el IDE.
- Menos “strings mágicos” para claves.
Navegación moderna en Compose (si tu proyecto es Compose)
Si tu app está construida con Jetpack Compose, es común usar rutas y argumentos con NavHost. Una forma segura de pasar datos es:
- Pasar IDs simples por la ruta (por ejemplo,
userId). - Recuperar el resto desde una fuente de datos (repositorio/estado) en el destino.
Ejemplo: ruta con parámetro
// Rutas
object Routes {
const val Home = "home"
const val Detail = "detail/{userId}"
fun detail(userId: Int) = "detail/$userId"
}
// Navegación
navController.navigate(Routes.detail(userId = 42))Leer argumento en el destino
composable(
route = Routes.Detail,
arguments = listOf(androidx.navigation.navArgument("userId") {
type = androidx.navigation.NavType.IntType
})
) { backStackEntry ->
val userId = backStackEntry.arguments?.getInt("userId") ?: return@composable
// Pantalla de detalle usando userId
}Nota de seguridad y robustez: evita pasar objetos grandes serializados por navegación; prefiere IDs y carga el detalle en el destino.
Checklist práctico: dónde poner cada cosa según el ciclo de vida
onCreate: inicialización, inflar UI, leer argumentos/intent, restaurar estado.onStart/onResume: iniciar cosas visibles o interactivas (suscripciones, sensores, animaciones).onPause: persistir cambios rápidos (por ejemplo, texto actual), pausar recursos exclusivos.onStop: liberar recursos pesados, detener tareas que no deben correr en background.onSaveInstanceState: guardar estado mínimo de UI para recreación.