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

1. Te damos la bienvenida

Este codelab práctico es parte de la unidad 1: Introducción del curso Conceptos básicos para desarrolladores de Android (versión 2). Aprovecharás al máximo este curso si trabajas con 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, las apps y las 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.

IDouble trouble

Cada etapa del ciclo de vida de una actividad tiene un método de devolución de llamada correspondiente: onCreate(), onStart(), onPause(), etc. 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(). Si anulas cualquiera de los métodos de devolución de llamada de ciclo de vida en tus clases de Activity, 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:

  • Crear y ejecutar un proyecto de app en Android Studio
  • Agregar instrucciones de registro a tu app y observar esos registros en el panel Logcat
  • Interpretar y trabajar con una actividad y un intent, y haber interactuando con ellos

Lo que aprenderá

  • Cómo funciona el ciclo de vida de la actividad
  • Cuándo 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 actividad
  • 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 de TwoActivities de la práctica anterior para implementar las diversas devoluciones de llamada de ciclo de vida de la actividad y, así, 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 Activity 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 realizar 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 de 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 en 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 cuando se inició:
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. Agrega 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 de 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. Debido a 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.

IDouble trouble

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 principal.
  • Usa la flecha hacia arriba en la barra de la aplicación para volver de la segunda actividad a la 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.

NOTA: 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

En los siguientes fragmentos de código, se muestra el código agregado en MainActivity, pero no toda la clase.

El método onCreate() hace lo siguiente:

@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

En los siguientes fragmentos de código, se muestra 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 de configuración. 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 la 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 la actividad. El sistema guarda la información de estado predeterminada en el paquete de estado de la instancia justo antes de que se detenga la actividad y pasa ese paquete a la nueva instancia de actividad para restablecerla.

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 la actividad y esta 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. Esto se debe a que el segundo diseño y estado de la actividad se generan a partir del diseño y del intent que la activó. Incluso si se vuelve a crear la actividad, el Intent sigue allí, y los datos de ese Intent se usarán cada vez que se llame al método onCreate() en la segunda actividad.

Además, es posible que notes que, en cada actividad, se conserva el texto que escribiste en los elementos EditText de los mensajes o las respuestas, 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 un EditText es uno de esos casos.

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

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

  1. Abre MainActivity.
  2. Agrega esta 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 y, 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 marcarán 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 pasa al paquete de estado con la clave "reply_text".

Solo guardas el estado de esos elementos View que pueden cambiar después de crear la actividad. Los otros elementos View de tu app (EditText y 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 la actividad, también deberás restablecerlo cuando se vuelva a crear la actividad. Puedes hacerlo en onCreate() o mediante la implementación de la devolución de llamada onRestoreInstanceState(), a la que se llama después de onStart() una vez creada 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, es 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 paquete de estado a onCreate() como único argumento. La primera vez que se llama a onCreate() y se inicia la app, el paquete es nulo; no hay estado existente la primera vez que se inicia tu 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 paquete 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 reply_visible en el paquete de estado (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 paquete con la clave "reply_text" y configura el TextView de respuesta para mostrar esa cadena.
mReplyTextView.setText(savedInstanceState.getString("reply_text"));
  1. Haz que el TextView de la respuesta 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

En los siguientes fragmentos de código, se muestra 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() hace lo siguiente:

@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 para agregar un elemento en la actividad principal inicia una segunda actividad que contiene una lista de artículos de compras comunes (queso, arroz, manzanas, etcétera). Usa elementos Button para mostrar los elementos.
  • Cuando se elige un elemento, el usuario vuelve a la actividad principal y se actualiza un TextView vacío para incluir el elemento elegido.

Usa 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 cambia entre diferentes 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() y onDestroy().
  • Anular 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 se destruya la actividad 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 de Bundle para ingresar datos y recuperar datos de Bundle.
  • Restablece el estado de la instancia en onCreate(), que es la forma preferida, o en onRestoreInstanceState(). Atrás