Cómo usar el estado y el ciclo de vida de la actividad

1. Te damos la bienvenida

Este codelab práctico forma parte de la unidad 1: Comienza el curso Conceptos básicos para desarrolladores de Android (versión 2). Aprovecharás al máximo este curso si trabajas en los codelabs en secuencia:

  • Para obtener la lista completa de codelabs del curso, consulta Codelabs de Conceptos básicos para desarrolladores de Android (V2).
  • Para obtener detalles sobre el curso, incluidos los vínculos a todos los capítulos conceptuales, apps y diapositivas, consulta Conceptos básicos para desarrolladores de Android (versión 2).

Introducción

En esta práctica, obtendrás más información sobre el ciclo de vida de la actividad. El ciclo de vida es el conjunto de estados que puede tener una actividad durante toda su vida útil, desde que se crea hasta que se destruye y el sistema recupera sus recursos. A medida que un usuario navega entre las actividades de tu app (y también cuando ingresa a la app y sale de ella), las actividades pasan por diferentes estados en sus ciclos de vida.

Doble problema

Cada etapa del ciclo de vida de una actividad tiene un método de devolución de llamada correspondiente: onCreate(), onStart(), onPause(), etcétera. Cuando una actividad cambia de estado, se invoca el método de devolución de llamada asociado. Ya viste uno de estos métodos: onCreate(). Al anular cualquiera de los métodos de devolución de llamada de ciclo de vida en tus clases de actividad, puedes cambiar el comportamiento predeterminado de la actividad en respuesta a las acciones del usuario o del sistema.

El estado de la actividad también puede cambiar en respuesta a los cambios de configuración del dispositivo, por ejemplo, cuando el usuario rota el dispositivo de la posición vertical a la horizontal. Cuando se producen estos cambios de configuración, la actividad se destruye y se vuelve a crear en su estado predeterminado. Como resultado, el usuario puede perder la información que ingresó en la actividad. Para evitar confundir a los usuarios, es importante que desarrolles tu app con el objetivo de evitar pérdidas de datos inesperadas. Más adelante en esta práctica, experimentarás con los cambios de configuración y aprenderás a preservar el estado de una actividad en respuesta a los cambios en la configuración del dispositivo y otros eventos de ciclo de vida de la actividad.

En esta práctica, agregarás instrucciones de registro a la app de TwoActivities y observarás cambios en el ciclo de vida de la actividad a medida que la usas. Luego, comenzarás a trabajar con estos cambios y a explorar cómo manejar las entradas del usuario en estas condiciones.

Requisitos previos

Deberías ser capaz de hacer lo siguiente:

  • Crea y ejecuta un proyecto de app en Android Studio.
  • Agregar instrucciones de registro a tu app y visualizar esos registros en el panel de Logcat
  • Comprender y trabajar con una actividad y un intent, y sentirse cómodo interactuando con ellos.

Qué aprenderá

  • Cómo funciona el ciclo de vida de la actividad
  • Cuando se inicia, se pausa, se detiene y se destruye una actividad
  • Información sobre los métodos de devolución de llamada de ciclo de vida asociados con los cambios de Activity
  • El efecto de las acciones (como los cambios de configuración) que pueden generar eventos del ciclo de vida de la actividad.
  • Cómo retener el estado de la actividad en eventos de ciclo de vida

Actividades

  • Agrega código a la app TwoActivities de la práctica anterior para implementar las diversas devoluciones de llamada del ciclo de vida de la actividad e incluir instrucciones de registro.
  • Observa los cambios de estado a medida que se ejecuta tu app y a medida que interactúas con cada actividad en ella.
  • Modifica tu app para conservar el estado de la instancia de una actividad que se vuelve a crear de forma inesperada en respuesta al comportamiento del usuario o al cambio de configuración en el dispositivo.

2. Descripción general de la app

En esta práctica, agregarás la app a TwoActivities. La app se ve y se comporta casi de la misma manera que en el último codelab. Contiene dos implementaciones de Activity y le permite al usuario enviar archivos entre ellas. Los cambios que realices en la app en esta práctica no afectarán su comportamiento visible.

3. 3. Tarea 1: Agrega devoluciones de llamada de ciclo de vida a TwoActivities

En esta tarea, implementarás todos los métodos de devolución de llamada del ciclo de vida de la actividad para imprimir mensajes en logcat cuando se invoquen esos métodos. Estos mensajes de registro te permitirán ver cuándo cambia el estado del ciclo de vida de la actividad y cómo afectan esos cambios del ciclo de vida a tu app mientras se ejecuta.

1.1 (Opcional) Copia el proyecto TwoActivities

Para las tareas de esta práctica, modificarás el proyecto existente TwoActivities que compilaste en la última práctica. Si prefieres no modificar el proyecto TwoActivities anterior, sigue los pasos indicados en el Apéndice: Utilidades para hacer una copia del proyecto.

1.2 Implementa devoluciones de llamada en MainActivity

  1. Abre el proyecto TwoActivities en Android Studio y abre MainActivity en el panel Project > Android.
  2. En el método onCreate(), agrega las siguientes instrucciones de registro:
Log.d(LOG_TAG, "-------");
Log.d(LOG_TAG, "onCreate");
  1. Agrega una anulación para la devolución de llamada onStart(), con una sentencia en el registro de ese evento:
@Override
public void onStart(){
    super.onStart();
    Log.d(LOG_TAG, "onStart");
}

Para obtener un acceso directo, selecciona Code > Override Methods in Android Studio. Aparecerá un diálogo con todos los métodos posibles que puedes anular en tu clase. Si eliges uno o más métodos de devolución de llamada de la lista, se inserta una plantilla completa para esos métodos, incluida la llamada necesaria a la superclase.

  1. Usa el método onStart() como plantilla para implementar las devoluciones de llamada de ciclo de vida onPause(), onRestart(), onResume(), onStop() y onDestroy()

Todos los métodos de devolución de llamada tienen las mismas firmas (excepto el nombre). Si copias y pegas onStart() para crear estos otros métodos de devolución de llamada, no olvides actualizar el contenido para llamar al método correcto en la superclase y registrar el método correcto.

  1. Ejecuta tu app.
  2. Haz clic en la pestaña Logcat, en la parte inferior de Android Studio, para mostrar el panel de Logcat. Deberías ver tres mensajes de registro que muestran los tres estados del ciclo de vida por los que pasó la actividad desde que comenzó:
D/MainActivity: -------
D/MainActivity: onCreate
D/MainActivity: onStart
D/MainActivity: onResume

1.3 Implementa devoluciones de llamada de ciclo de vida en SecondActivity

Ahora que implementaste los métodos de devolución de llamada de ciclo de vida para MainActivity, haz lo mismo con SecondActivity.

  1. Abre SecondActivity.
  2. En la parte superior de la clase, agrega una constante para la variable LOG_TAG:
private static final String LOG_TAG = SecondActivity.class.getSimpleName();
  1. Agregar las devoluciones de llamada de ciclo de vida y las instrucciones de registro a la segunda actividad (Puedes copiar y pegar los métodos de devolución de llamada desde MainActivity).
  2. Agrega una instrucción de registro al método returnReply() justo antes del método finish():
Log.d(LOG_TAG, "End SecondActivity");

1.4 Observa el registro mientras se ejecuta la app**

  1. Ejecuta tu app.
  2. Haz clic en la pestaña Logcat, en la parte inferior de Android Studio, para mostrar el panel de Logcat.
  3. Ingresa Actividad en el cuadro de búsqueda. El logcat de Android puede ser muy largo y desordenado. Dado que la variable LOG_TAG de cada clase contiene las palabras MainActivity o SecondActivity, esta palabra clave te permite filtrar el registro solo para las cosas que te interesan.

Doble problema

Experimenta con la app y observa los eventos de ciclo de vida que tienen lugar en respuesta a diferentes acciones. En particular, prueba lo siguiente:

  • Usa la app normalmente (envía un mensaje y responde con otro mensaje).
  • Usa el botón Atrás para volver de la segunda actividad a la actividad principal.
  • Usa la flecha hacia arriba en la barra de la aplicación para volver de la segunda actividad a la actividad principal.
  • Rota el dispositivo en la actividad principal y secundaria en diferentes momentos de tu app y observa lo que sucede en * el registro y en la pantalla.
  • Presiona el botón Recientes (el botón cuadrado que se encuentra a la derecha de Inicio) y cierra la app (presiona la X).
  • Regresa a la pantalla principal y reinicia la app.

SUGERENCIA: Si ejecutas tu app en un emulador, puedes simular la rotación con Control + F11 o Control + Función + F11.

Código de la solución de la Tarea 1

En los siguientes fragmentos de código, se muestra el código de solución para la primera tarea.

MainActivity

Los siguientes fragmentos de código muestran el código agregado en MainActivity, pero no toda la clase.

El método onCreate():

@Override
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Log the start of the onCreate() method.
        Log.d(LOG_TAG, "-------");
        Log.d(LOG_TAG, "onCreate");

        // Initialize all the view variables.
        mMessageEditText = findViewById(R.id.editText_main);
        mReplyHeadTextView = findViewById(R.id.text_header_reply);
        mReplyTextView = findViewById(R.id.text_message_reply);
}

Los otros métodos del ciclo de vida:

@Override
protected void onStart() {
        super.onStart();
        Log.d(LOG_TAG, "onStart");
}

@Override
protected void onPause() {
        super.onPause();
        Log.d(LOG_TAG, "onPause");
}

@Override
protected void onRestart() {
        super.onRestart();
        Log.d(LOG_TAG, "onRestart");
}

@Override
protected void onResume() {
        super.onResume();
        Log.d(LOG_TAG, "onResume");
}

@Override
protected void onStop() {
        super.onStop();
        Log.d(LOG_TAG, "onStop");
}

@Override
protected void onDestroy() {
        super.onDestroy();
        Log.d(LOG_TAG, "onDestroy");
}

SecondActivity

Los siguientes fragmentos de código muestran el código agregado en SecondActivity, pero no toda la clase.

En la parte superior de la clase SecondActivity, haz lo siguiente:

private static final String LOG_TAG = SecondActivity.class.getSimpleName();

El método returnReply():

public void returnReply(View view) {
        String reply = mReply.getText().toString();
        Intent replyIntent = new Intent();
        replyIntent.putExtra(EXTRA_REPLY, reply);
        setResult(RESULT_OK, replyIntent);
        Log.d(LOG_TAG, "End SecondActivity");
        finish();
}

Los otros métodos del ciclo de vida:

Igual que en MainActivity, arriba.

4. 4. Tarea 2: Guarda y restablece el estado de la instancia de Activity

Según los recursos del sistema y el comportamiento del usuario, cada actividad de tu app puede destruirse y reconstruirse con mucha más frecuencia de la que crees.

Es posible que hayas notado este comportamiento en la última sección cuando rotaste el dispositivo o el emulador. La rotación del dispositivo es un ejemplo de un cambio en la configuración del dispositivo. Aunque la rotación es la más común, todos los cambios de configuración hacen que la actividad actual se destruya y se vuelva a crear como si fuera nueva. Si no tienes en cuenta este comportamiento en tu código, cuando se produzca un cambio en la configuración, es posible que el diseño de tu actividad vuelva a su aspecto predeterminado y valores iniciales, y que los usuarios pierdan su lugar, sus datos o el estado de su progreso en tu app.

El estado de cada actividad se almacena como un conjunto de pares clave-valor en un objeto Bundle llamado estado de la instancia de Activity. El sistema guarda la información de estado predeterminada en el estado de la instancia Bundle justo antes de que se detenga el objeto Activity, y pasa ese Bundle a la nueva instancia de Activity para restablecerlo.

Para evitar perder datos en una actividad cuando se destruye y se vuelve a crear de forma inesperada, debes implementar el método onSaveInstanceState(). El sistema llama a este método en tu actividad (entre onPause() y onStop()) cuando existe la posibilidad de que la actividad se destruya y se vuelva a crear.

Los datos que guardas en el estado de la instancia son específicos solo de esta instancia de esta actividad específica durante la sesión actual de la app. Cuando detienes y reinicias una nueva sesión de la app, se pierde el estado de la instancia de Activity y la actividad vuelve a su aspecto predeterminado. Si necesitas guardar datos del usuario entre sesiones de la app, usa preferencias compartidas o una base de datos. Aprenderás sobre ambos en una práctica posterior.

2.1 Guarda el estado de la instancia de Activity con onSaveInstanceState()

Es posible que hayas notado que rotar el dispositivo no afecta el estado de la segunda actividad en absoluto. Esto se debe a que el segundo diseño y estado de la actividad se generan a partir del diseño y el intent que la activó. Incluso si se recrea la actividad, el intent sigue allí, y los datos en ese intent se usan cada vez que se llama al método onCreate() en la segunda actividad.

Además, puedes notar que, en cada actividad, cualquier texto que escribiste en los elementos EditText de los mensajes o las respuestas se retiene incluso cuando se rota el dispositivo. Esto se debe a que la información de estado de algunos de los elementos View de tu diseño se guarda automáticamente en todos los cambios de configuración, y el valor actual de EditText es uno de esos casos.

Por lo tanto, el único estado de Activity que te interesa son los elementos TextView para el encabezado de respuesta y el texto de respuesta en la Activity principal. Ambos elementos TextView son invisibles de forma predeterminada; solo aparecen una vez que envías un mensaje a la Activity principal desde la segunda Activity.

En esta tarea, agregarás código para preservar el estado de la instancia de estos dos elementos TextView usando onSaveInstanceState().

  1. Abre MainActivity.
  2. Agrega la implementación de esqueleto de onSaveInstanceState() a la actividad, o usa Code > Override Methods para insertar una anulación de esqueleto.
@Override
public void onSaveInstanceState(Bundle outState) {
          super.onSaveInstanceState(outState);
}
  1. Verifica si el encabezado es visible actualmente. De ser así, coloca ese estado de visibilidad en el paquete de estado con el método putBoolean() y la clave "reply_visible".
 if (mReplyHeadTextView.getVisibility() == View.VISIBLE) {
        outState.putBoolean("reply_visible", true);
    }

Recuerda que el encabezado y el texto de la respuesta se marcan como invisibles hasta que haya una respuesta de la segunda actividad. Si el encabezado está visible, significa que hay datos de respuesta que deben guardarse. Ten en cuenta que solo nos interesa ese estado de visibilidad; no es necesario guardar el texto real del encabezado, ya que ese texto nunca cambia.

  1. Dentro de esa misma verificación, agrega el texto de respuesta al paquete.
outState.putString("reply_text",mReplyTextView.getText().toString());

Si el encabezado es visible, puedes suponer que también lo es el mensaje de respuesta. No es necesario probar ni guardar el estado de visibilidad actual del mensaje de respuesta. Solo el texto real del mensaje se ingresa en el paquete de estado con la clave "reply_text".

Solo guardas el estado de esos elementos View que pueden cambiar después de crear la Activity. Los otros elementos View de tu app (EditText, Button) se pueden recrear desde el diseño predeterminado en cualquier momento.

Ten en cuenta que el sistema guardará el estado de algunos elementos View, como el contenido de EditText.

2.2 Restablece el estado de la instancia de Activity en onCreate()

Una vez que hayas guardado el estado de la instancia de Activity, también deberás restablecerlo cuando se vuelva a crear la Activity. Puedes hacer esto en onCreate() o mediante la implementación de la devolución de llamada onRestoreInstanceState(), al que se llama después de onStart() después de que se crea la actividad.

La mayoría de las veces, el mejor lugar para restablecer el estado de la actividad es en onCreate(), para garantizar que la IU, incluido el estado, esté disponible lo antes posible. A veces, resulta conveniente hacerlo en onRestoreInstanceState() después de completar toda la inicialización o permitir que las subclases decidan si desean usar tu implementación predeterminada.

  1. En el método onCreate(), después de que las variables View se inicialicen con findViewById(), agrega una prueba para asegurarte de que savedInstanceState no sea nulo.
// Initialize all the view variables.
mMessageEditText = findViewById(R.id.editText_main);
mReplyHeadTextView = findViewById(R.id.text_header_reply);
mReplyTextView = findViewById(R.id.text_message_reply);

// Restore the state.
if (savedInstanceState != null) {
}

Cuando se crea tu actividad, el sistema pasa el estado Bundle a onCreate() como único argumento. La primera vez que se llama a onCreate() y se inicia tu app, el paquete es nulo, es decir, no hay estado existente la primera vez que se inicia la app. Las llamadas posteriores a onCreate() tendrán un paquete propagado con los datos que almacenaste en onSaveInstanceState().

  1. Dentro de esa verificación, obtén la visibilidad actual (verdadero o falso) del Bundle con la clave "reply_visible".
if (savedInstanceState != null) {
    boolean isVisible = 
                     savedInstanceState.getBoolean("reply_visible");
}
  1. Agrega una prueba debajo de la línea anterior para la variable isVisible.
if (isVisible) {
}

Si hay una clave response_visible en el estado Bundle (y, por lo tanto, isVisible es verdadero), deberás restablecer el estado.

  1. Dentro de la prueba isVisible, haz que el encabezado sea visible.
mReplyHeadTextView.setVisibility(View.VISIBLE);
  1. Obtén el mensaje de respuesta de texto del Bundle con la clave "reply_text" y configura la TextView de respuesta para que muestre esa cadena.
mReplyTextView.setText(savedInstanceState.getString("reply_text"));
  1. Haz que la respuesta TextView también sea visible:
mReplyTextView.setVisibility(View.VISIBLE);
  1. Ejecuta la app. Prueba girar el dispositivo o el emulador para asegurarte de que el mensaje de respuesta (si hay uno) permanezca en la pantalla después de volver a crear la actividad.

Código de la solución de la Tarea 2

En los siguientes fragmentos de código, se muestra el código de la solución para esta tarea.

MainActivity

Los siguientes fragmentos de código muestran el código agregado en MainActivity, pero no toda la clase.

El método onSaveInstanceState():

@Override
public void onSaveInstanceState(Bundle outState) {
   super.onSaveInstanceState(outState);
   // If the heading is visible, message needs to be saved.
   // Otherwise we're still using default layout.
   if (mReplyHeadTextView.getVisibility() == View.VISIBLE) {
       outState.putBoolean("reply_visible", true);
       outState.putString("reply_text", 
                      mReplyTextView.getText().toString());
   }
}

El método onCreate():

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);

   Log.d(LOG_TAG, "-------");
   Log.d(LOG_TAG, "onCreate");

   // Initialize all the view variables.
   mMessageEditText = findViewById(R.id.editText_main);
   mReplyHeadTextView = findViewById(R.id.text_header_reply);
   mReplyTextView = findViewById(R.id.text_message_reply);

   // Restore the saved state. 
   // See onSaveInstanceState() for what gets saved.
   if (savedInstanceState != null) {
       boolean isVisible = 
                     savedInstanceState.getBoolean("reply_visible");
       // Show both the header and the message views. If isVisible is
       // false or missing from the bundle, use the default layout.
       if (isVisible) {
           mReplyHeadTextView.setVisibility(View.VISIBLE);
           mReplyTextView.setText(savedInstanceState
                                  .getString("reply_text"));
           mReplyTextView.setVisibility(View.VISIBLE);
       }
   }
}

El proyecto completo:

Proyecto de Android Studio: TwoActivitiesLifecycle

5. Programación

Desafío: Crea una app de lista de compras simple con una actividad principal para la lista que está creando el usuario y otra para una lista de artículos de compras comunes.

  • La actividad principal debe contener la lista que se compilará, que debe contener diez elementos TextView vacíos.
  • Un botón de Agregar artículo en la actividad principal inicia una segunda actividad que contiene una lista de artículos de compra comunes (queso, arroz, manzanas, etc.). Usa elementos Button para mostrar los elementos.
  • Cuando se elige un artículo, el usuario vuelve a la actividad principal y se actualiza una TextView vacía para incluir el artículo elegido.

Usar un Intent para pasar información de una actividad a otra Asegúrate de que se guarde el estado actual de la lista de compras cuando el usuario rote el dispositivo.

6. Resumen

  • El ciclo de vida de la actividad es un conjunto de estados por los que migra una actividad, que comienza cuando se crea por primera vez y finaliza cuando el sistema Android recupera los recursos para esa actividad.
  • A medida que el usuario navega de una actividad a otra, dentro y fuera de tu app, cada actividad se mueve entre los estados del ciclo de vida de la actividad.
  • Cada estado del ciclo de vida de la actividad tiene un método de devolución de llamada correspondiente que puedes anular en tu clase de actividad.
  • Los métodos de ciclo de vida son onCreate(), onStart(), onPause(), onRestart(), onResume(), onStop(), onDestroy().
  • La anulación de un método de devolución de llamada de ciclo de vida te permite agregar un comportamiento que ocurre cuando tu actividad pasa a ese estado.
  • Puedes agregar métodos de anulación de esqueleto a tus clases en Android Studio con Code > Override.
  • Los cambios en la configuración del dispositivo, como la rotación, hacen que la actividad se destruya y se vuelva a crear como si fuera nueva.
  • Una parte del estado de la actividad se conserva cuando se produce un cambio de configuración, incluidos los valores actuales de los elementos EditText. Para todos los demás datos, debes guardarlos de forma explícita.
  • Guarda el estado de la instancia de Activity en el método onSaveInstanceState().
  • Los datos de estado de la instancia se almacenan como pares clave-valor simples en un paquete. Usa los métodos Bundle para ingresar datos en el Bundle y recuperarlos.
  • Restablece el estado de la instancia en onCreate(), que es la forma preferida, o onRestoreInstanceState(). Atrás