El ciclo de vida de una Activity de Android: pila, giros y procesos eliminados
Simula lo que hace Android con las actividades: el orden de onCreate, onStart, onResume, onPause, onStop y onDestroy al abrir pantallas, volver atrás, salir al inicio, girar el móvil o tapar con un diálogo, y cómo el Bundle salva el estado cuando el sistema elimina el proceso.
- Ciclo de vida de una Activity
- Pila de actividades (back stack)
- onSaveInstanceState y Bundle
- Cambios de configuración
- Muerte del proceso
- Máquinas de estados
Enunciado
En Android no eres tú quien decide cuándo se crea o se destruye una pantalla: lo decide el sistema, y avisa a la actividad llamando a sus métodos del ciclo de vida. Entenderlo es la diferencia entre una aplicación que pierde lo escrito al girar el móvil y una que no: al girar, la actividad se destruye y se vuelve a crear, y solo sobrevive lo que se guardó en el Bundle de onSaveInstanceState.
La clase Actividad del código de partida ya tiene sus métodos (escriben su nombre y, onCreate, restaura clics del Bundle) y dos contadores: clics, que se guarda en el Bundle, y temporal, un atributo normal que se pierde con la instancia. Tú escribes Sistema, que hace de ActivityManager: con la pila de actividades decide a qué métodos se llama y en qué orden, como en Android 9 o posterior. Para simplificar, al girar solo se recrea la actividad visible (en un móvil real, las de debajo también se recrean cuando vuelven a verse).
Qué tiene que hacer el programa
abrir X: si hay otra visible, primeroonPause()de esa; despuésonCreate(null),onStart()yonResume()de la nueva, que se apila; y al finalonStop()yonSaveInstanceState()de la anterior (guarda el Bundle y quedadetenida). Con la aplicación cerrada, solo los tres de la nueva.atras:onPause()de la visible; si hay otra debajo, se trae al frente (onRestart(),onStart(),onResume(); o, si su instancia no existe, se recrea:onCreate(Bundle),onStart(),onRestoreInstanceState(Bundle),onResume()); y despuésonStop()yonDestroy()de la que se va. Si era la única, se escribeLa aplicación se cierra.inicio(botón de inicio):onPause(),onStop()yonSaveInstanceState()de la visible, y la aplicación pasa a segundo plano.volver: trae la visible al frente (reiniciándola o recreándola, como arriba).matar(solo en segundo plano): todas las instancias desaparecen sin llamar a nada (El sistema elimina el proceso para liberar memoria (sin llamar a ningún método)), pero los Bundle guardados se conservan.girar:onPause(),onStop(),onSaveInstanceState()yonDestroy()de la visible y después se recrea.dialogo:onPause()de la visible (quedapausada);cerrar: suonResume().tocar: suma uno aclicsy atemporalde la visible y escribeX: clics = N, temporal = M.estado:Aplicación: cerrada, oPila:con las entradas de abajo arriba separadas por→(X [estado, clics=N, temporal=M]o, sin instancia,X [destruida, guardado Bundle{clics=N}]) yAplicación: en primer planooen segundo plano, con, proceso eliminadosi lo fue y aún no se ha vuelto.- Errores, por este orden: con la aplicación cerrada,
La aplicación está cerrada; en segundo plano,La aplicación está en segundo plano: usa volver(también paraabrir); con un diálogo abierto,Primero cierra el diálogo(salvocerraryestado). Además:La aplicación no está en segundo plano(volver),El sistema solo elimina procesos en segundo plano(matar) yNo hay ningún diálogo abierto(cerrar). Elmainya escribe cada orden y los errores de formato.
Entrada
Una orden por línea: abrir Nombre (empieza por mayúscula), atras, inicio, volver, matar, girar, dialogo, cerrar, tocar o estado.
Datos de referencia
| Estado | Se ve | Tiene el foco | Se llega con |
|---|---|---|---|
| reanudada | sí | sí | onResume() |
| pausada | sí, en parte | no | onPause() |
| detenida | no | no | onStop() |
| destruida | no | no | onDestroy() o la muerte del proceso |
Ejemplos de ejecución
Tu programa debe escribir exactamente esta salida para estas entradas. Las pruebas del editor incluyen estos ejemplos y otros casos ocultos.
Abrir, tocar, girar y volver atrás
Entrada
abrir Principal tocar abrir Detalle tocar tocar girar estado atras atras estado
Salida por consola
> abrir Principal
Principal.onCreate(null)
Principal.onStart()
Principal.onResume()
> tocar
Principal: clics = 1, temporal = 1
> abrir Detalle
Principal.onPause()
Detalle.onCreate(null)
Detalle.onStart()
Detalle.onResume()
Principal.onStop()
Principal.onSaveInstanceState() → Bundle{clics=1}
> tocar
Detalle: clics = 1, temporal = 1
> tocar
Detalle: clics = 2, temporal = 2
> girar
Detalle.onPause()
Detalle.onStop()
Detalle.onSaveInstanceState() → Bundle{clics=2}
Detalle.onDestroy()
Detalle.onCreate(Bundle{clics=2})
Detalle.onStart()
Detalle.onRestoreInstanceState(Bundle{clics=2})
Detalle.onResume()
> estado
Pila: Principal [detenida, clics=1, temporal=1] → Detalle [reanudada, clics=2, temporal=0]
Aplicación: en primer plano
> atras
Detalle.onPause()
Principal.onRestart()
Principal.onStart()
Principal.onResume()
Detalle.onStop()
Detalle.onDestroy()
> atras
Principal.onPause()
Principal.onStop()
Principal.onDestroy()
La aplicación se cierra
> estado
Aplicación: cerradaSegundo plano y proceso eliminado
Entrada
volver abrir Lista tocar abrir Ficha tocar inicio tocar matar estado volver estado atras
Salida por consola
> volver
La aplicación no está en segundo plano
> abrir Lista
Lista.onCreate(null)
Lista.onStart()
Lista.onResume()
> tocar
Lista: clics = 1, temporal = 1
> abrir Ficha
Lista.onPause()
Ficha.onCreate(null)
Ficha.onStart()
Ficha.onResume()
Lista.onStop()
Lista.onSaveInstanceState() → Bundle{clics=1}
> tocar
Ficha: clics = 1, temporal = 1
> inicio
Ficha.onPause()
Ficha.onStop()
Ficha.onSaveInstanceState() → Bundle{clics=1}
> tocar
La aplicación está en segundo plano: usa volver
> matar
El sistema elimina el proceso para liberar memoria (sin llamar a ningún método)
> estado
Pila: Lista [destruida, guardado Bundle{clics=1}] → Ficha [destruida, guardado Bundle{clics=1}]
Aplicación: en segundo plano, proceso eliminado
> volver
Ficha.onCreate(Bundle{clics=1})
Ficha.onStart()
Ficha.onRestoreInstanceState(Bundle{clics=1})
Ficha.onResume()
> estado
Pila: Lista [destruida, guardado Bundle{clics=1}] → Ficha [reanudada, clics=1, temporal=0]
Aplicación: en primer plano
> atras
Ficha.onPause()
Lista.onCreate(Bundle{clics=1})
Lista.onStart()
Lista.onRestoreInstanceState(Bundle{clics=1})
Lista.onResume()
Ficha.onStop()
Ficha.onDestroy()Guía paso a paso
Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.
1. La pila
Un ArrayDeque con push, pop y peek es la pila de actividades: la cima es la que se ve. Recórrela de abajo arriba con descendingIterator().
2. Un orden que tiene sentido
Al abrir otra pantalla, la anterior se pausa antes de crear la nueva (para que suelte la cámara o el audio enseguida), pero no se detiene hasta que la nueva ya se ve: así nunca hay un momento sin nada en pantalla.
anterior.act.onPause();
nueva.act.onCreate(null);
nueva.act.onStart();
nueva.act.onResume();
anterior.act.onStop();
anterior.guardado = anterior.act.onSaveInstanceState();3. Reiniciar o recrear
Traer una actividad al frente tiene dos caminos: si su instancia sigue viva, onRestart y onStart; si murió (giro o proceso eliminado), una instancia nueva con onCreate(Bundle) y onRestoreInstanceState(Bundle). Escríbelo una vez en un método y úsalo en atras, volver y girar.
4. Lo que se pierde
Mira temporal después de un giro o de matar el proceso: vuelve a 0 porque es un atributo de la instancia. clics sobrevive porque viaja en el Bundle.
Resuélvelo aquí
El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 2 casos ocultos que buscan los errores típicos.
Ejemplo
abrir Principal tocar abrir Detalle tocar tocar girar estado atras atras estado
> abrir Principal
Principal.onCreate(null)
Principal.onStart()
Principal.onResume()
> tocar
Principal: clics = 1, temporal = 1
> abrir Detalle
Principal.onPause()
Detalle.onCreate(null)
Detalle.onStart()
Detalle.onResume()
Principal.onStop()
Principal.onSaveInstanceState() → Bundle{clics=1}
> tocar
Detalle: clics = 1, temporal = 1
> tocar
Detalle: clics = 2, temporal = 2
> girar
Detalle.onPause()
Detalle.onStop()
Detalle.onSaveInstanceState() → Bundle{clics=2}
Detalle.onDestroy()
Detalle.onCreate(Bundle{clics=2})
Detalle.onStart()
Detalle.onRestoreInstanceState(Bundle{clics=2})
Detalle.onResume()
> estado
Pila: Principal [detenida, clics=1, temporal=1] → Detalle [reanudada, clics=2, temporal=0]
Aplicación: en primer plano
> atras
Detalle.onPause()
Principal.onRestart()
Principal.onStart()
Principal.onResume()
Detalle.onStop()
Detalle.onDestroy()
> atras
Principal.onPause()
Principal.onStop()
Principal.onDestroy()
La aplicación se cierra
> estado
Aplicación: cerradaSolución explicada
Ver la solución completa
1import java.util.*;
2
3/** Una actividad con los métodos de su ciclo de vida, como android.app.Activity. */
4class Actividad {
5 final String nombre;
6 int clics; // se guarda en el Bundle con onSaveInstanceState
7 int temporal; // un atributo normal: se pierde con la instancia
8
9 Actividad(String nombre) { this.nombre = nombre; }
10
11 private void decir(String metodo) { System.out.println(nombre + "." + metodo); }
12
13 static String bundle(Map<String, Integer> b) { return b == null ? "null" : "Bundle{clics=" + b.get("clics") + "}"; }
14
15 void onCreate(Map<String, Integer> estado) {
16 if (estado != null) clics = estado.get("clics");
17 decir("onCreate(" + bundle(estado) + ")");
18 }
19 void onStart() { decir("onStart()"); }
20 void onRestart() { decir("onRestart()"); }
21 void onResume() { decir("onResume()"); }
22 void onPause() { decir("onPause()"); }
23 void onStop() { decir("onStop()"); }
24 void onDestroy() { decir("onDestroy()"); }
25
26 Map<String, Integer> onSaveInstanceState() {
27 Map<String, Integer> b = new LinkedHashMap<>();
28 b.put("clics", clics);
29 decir("onSaveInstanceState() → " + bundle(b));
30 return b;
31 }
32
33 void onRestoreInstanceState(Map<String, Integer> b) { decir("onRestoreInstanceState(" + bundle(b) + ")"); }
34}
35
36/** Una entrada de la pila de actividades: la instancia (null si el proceso murió) y su estado guardado. */
37class Entrada {
38 final String nombre;
39 Actividad act;
40 Map<String, Integer> guardado;
41 String estado;
42
43 Entrada(String nombre) { this.nombre = nombre; }
44}
45
46/** Hace de ActivityManager: decide a qué métodos se llama y en qué orden. */
47class Sistema {
48 private final Deque<Entrada> pila = new ArrayDeque<>(); // la cima es la actividad visible
49 private boolean segundoPlano = false, dialogo = false, procesoEliminado = false;
50
51 private Entrada cima() { return pila.peek(); }
52
53 /** Comprueba que la aplicación está en primer plano y sin diálogo; si no, explica por qué. */
54 private boolean enPrimerPlano() {
55 if (pila.isEmpty()) System.out.println("La aplicación está cerrada");
56 else if (segundoPlano) System.out.println("La aplicación está en segundo plano: usa volver");
57 else if (dialogo) System.out.println("Primero cierra el diálogo");
58 else return true;
59 return false;
60 }
61
62 /** Lleva una entrada al primer plano: la recrea si su instancia ya no existe o la reinicia si estaba detenida. */
63 private void traerAlFrente(Entrada e) {
64 if (e.act == null) {
65 e.act = new Actividad(e.nombre);
66 e.act.onCreate(e.guardado);
67 e.act.onStart();
68 e.act.onRestoreInstanceState(e.guardado);
69 } else {
70 e.act.onRestart();
71 e.act.onStart();
72 }
73 e.act.onResume();
74 e.estado = "reanudada";
75 }
76
77 private void detener(Entrada e) {
78 e.act.onStop();
79 e.guardado = e.act.onSaveInstanceState();
80 e.estado = "detenida";
81 }
82
83 void abrir(String nombre) {
84 if (segundoPlano) {
85 System.out.println("La aplicación está en segundo plano: usa volver");
86 return;
87 }
88 if (dialogo) {
89 System.out.println("Primero cierra el diálogo");
90 return;
91 }
92 Entrada anterior = cima();
93 if (anterior != null) {
94 anterior.act.onPause();
95 anterior.estado = "pausada";
96 }
97 Entrada nueva = new Entrada(nombre);
98 nueva.act = new Actividad(nombre);
99 nueva.act.onCreate(null);
100 nueva.act.onStart();
101 nueva.act.onResume();
102 nueva.estado = "reanudada";
103 pila.push(nueva);
104 if (anterior != null) detener(anterior); // la anterior se detiene cuando la nueva ya se ve
105 }
106
107 void atras() {
108 if (!enPrimerPlano()) return;
109 Entrada actual = pila.pop();
110 actual.act.onPause();
111 if (!pila.isEmpty()) traerAlFrente(cima());
112 actual.act.onStop();
113 actual.act.onDestroy();
114 if (pila.isEmpty()) {
115 System.out.println("La aplicación se cierra");
116 procesoEliminado = false;
117 }
118 }
119
120 void inicio() {
121 if (!enPrimerPlano()) return;
122 cima().act.onPause();
123 detener(cima());
124 segundoPlano = true;
125 }
126
127 void volver() {
128 if (pila.isEmpty() || !segundoPlano) {
129 System.out.println("La aplicación no está en segundo plano");
130 return;
131 }
132 segundoPlano = false;
133 procesoEliminado = false;
134 traerAlFrente(cima());
135 }
136
137 void matar() {
138 if (pila.isEmpty() || !segundoPlano) {
139 System.out.println("El sistema solo elimina procesos en segundo plano");
140 return;
141 }
142 // el proceso desaparece de golpe: no se llama a ningún método, pero los Bundle guardados sobreviven
143 for (Entrada e : pila) {
144 e.act = null;
145 e.estado = "destruida";
146 }
147 procesoEliminado = true;
148 System.out.println("El sistema elimina el proceso para liberar memoria (sin llamar a ningún método)");
149 }
150
151 void girar() {
152 if (!enPrimerPlano()) return;
153 Entrada e = cima();
154 e.act.onPause();
155 detener(e);
156 e.act.onDestroy();
157 e.act = null;
158 traerAlFrente(e);
159 }
160
161 void dialogo() {
162 if (!enPrimerPlano()) return;
163 cima().act.onPause();
164 cima().estado = "pausada";
165 dialogo = true;
166 }
167
168 void cerrar() {
169 if (!dialogo) {
170 System.out.println("No hay ningún diálogo abierto");
171 return;
172 }
173 dialogo = false;
174 cima().act.onResume();
175 cima().estado = "reanudada";
176 }
177
178 void tocar() {
179 if (!enPrimerPlano()) return;
180 Actividad a = cima().act;
181 a.clics++;
182 a.temporal++;
183 System.out.println(a.nombre + ": clics = " + a.clics + ", temporal = " + a.temporal);
184 }
185
186 void estado() {
187 if (pila.isEmpty()) {
188 System.out.println("Aplicación: cerrada");
189 return;
190 }
191 List<String> partes = new ArrayList<>();
192 Iterator<Entrada> it = pila.descendingIterator(); // de abajo arriba
193 while (it.hasNext()) {
194 Entrada e = it.next();
195 partes.add(e.act == null ? e.nombre + " [destruida, guardado " + Actividad.bundle(e.guardado) + "]"
196 : e.nombre + " [" + e.estado + ", clics=" + e.act.clics + ", temporal=" + e.act.temporal + "]");
197 }
198 System.out.println("Pila: " + String.join(" → ", partes));
199 System.out.println("Aplicación: " + (segundoPlano ? "en segundo plano" : "en primer plano") + (procesoEliminado ? ", proceso eliminado" : ""));
200 }
201}
202
203public class Main {
204 public static void main(String[] args) {
205 Scanner sc = new Scanner(System.in);
206 Sistema s = new Sistema();
207 while (sc.hasNextLine()) {
208 String linea = sc.nextLine().trim();
209 if (linea.isEmpty()) continue;
210 System.out.println("> " + linea);
211 String[] p = linea.split("\\s+");
212 if (p[0].equals("abrir") && p.length == 2 && p[1].matches("[A-Z]\\w*")) {
213 s.abrir(p[1]);
214 continue;
215 }
216 if (p.length != 1) {
217 System.out.println("Orden no válida: " + linea);
218 continue;
219 }
220 switch (p[0]) {
221 case "atras" -> s.atras();
222 case "inicio" -> s.inicio();
223 case "volver" -> s.volver();
224 case "matar" -> s.matar();
225 case "girar" -> s.girar();
226 case "dialogo" -> s.dialogo();
227 case "cerrar" -> s.cerrar();
228 case "tocar" -> s.tocar();
229 case "estado" -> s.estado();
230 default -> System.out.println("Orden desconocida: " + p[0]);
231 }
232 }
233 }
234}El ciclo de vida existe porque el móvil tiene poca memoria y una sola pantalla: Android pausa lo que se tapa, detiene lo que no se ve y, si necesita memoria, elimina procesos enteros sin avisar. La aplicación tiene que estar preparada para volver como si nada.
Guardar el estado en onSaveInstanceState es la red de seguridad para los giros y la muerte del proceso. Hoy se usa además un ViewModel (que sobrevive a los giros, pero no a la muerte del proceso) con SavedStateHandle, que sí sobrevive.
onPause debe ser rápido (la siguiente pantalla espera a que acabe) y onStop es el sitio para liberar lo pesado o guardar datos. Lo que se crea en onStart se libera en onStop, y lo de onResume, en onPause.
Un diálogo de la propia aplicación (un AlertDialog) no pausa la actividad: lo que la pausa es otra actividad que la tapa en parte, como un diálogo de permisos o una actividad con tema de diálogo.
Para ir más allá
- Añade un
ViewModelque sobreviva a los giros pero no a la muerte del proceso y muestra la diferencia. - Simula los modos de lanzamiento
singleTopysingleTaskenabrir. - Recrea también las actividades de debajo cuando vuelven a verse después de un giro, como en un móvil real.