Aspectos básicos de Kotlin para Android 04.2: Situaciones complejas de ciclo de vida

Este codelab es parte del curso Conceptos básicos de Kotlin para Android. Aprovecharás al máximo este curso si trabajas con los codelabs de forma secuencial. Todos los codelabs del curso se enumeran en la página de destino de los codelabs de Android Kotlin Fundamentals.

Introducción

En el último codelab, aprendiste sobre los ciclos de vida de Activity y Fragment, y exploraste los métodos que se llaman cuando cambia el estado del ciclo de vida en actividades y fragmentos. En este codelab, explorarás el ciclo de vida de la actividad con más detalle. También aprenderás sobre la biblioteca de ciclos de vida de Android Jetpack, que puede ayudarte a administrar los eventos del ciclo de vida con código mejor organizado y más fácil de mantener.

Conocimientos que ya deberías tener

  • Qué es una actividad y cómo crear una en tu app
  • Los conceptos básicos de los ciclos de vida de Activity y Fragment, y las devoluciones de llamada que se invocan cuando una actividad pasa de un estado a otro
  • Cómo anular los métodos de devolución de llamada de ciclo de vida onCreate() y onStop() para realizar operaciones en diferentes momentos del ciclo de vida de la actividad o el fragmento

Qué aprenderás

  • Cómo configurar, iniciar y detener partes de tu app en las devoluciones de llamada de ciclo de vida
  • Cómo usar la biblioteca de ciclos de vida de Android para crear un observador de ciclos de vida y facilitar la administración del ciclo de vida de actividades y fragmentos
  • Cómo los cierres de procesos de Android afectan los datos de tu app y cómo guardar y restablecer esos datos automáticamente cuando Android cierra tu app
  • Cómo la rotación del dispositivo y otros cambios de configuración generan cambios en los estados del ciclo de vida y afectan el estado de tu app

Actividades

  • Modifica la app de DessertClicker para incluir una función de temporizador, y comienza y detén ese temporizador en varios momentos del ciclo de vida de la actividad.
  • Modifica la app para usar la biblioteca de ciclo de vida de Android y convierte la clase DessertTimer en un observador del ciclo de vida.
  • Configura y usa Android Debug Bridge (adb) para simular el cierre de procesos de tu app y las devoluciones de llamada de ciclo de vida que se producen en ese momento.
  • Implementa el método onSaveInstanceState() para conservar los datos de la app que podrían perderse si esta se cierra de forma inesperada. Agrega un código para restablecer esos datos cuando se vuelva a iniciar la app.

En este codelab, ampliarás la app de DessertClicker del codelab anterior. Agregarás un temporizador en segundo plano y, luego, convertirás la app para que use la biblioteca de ciclo de vida de Android.

En el codelab anterior, aprendiste a observar los ciclos de vida de la actividad y el fragmento anulando varias devoluciones de llamada de ciclo de vida y registrando cuándo el sistema invoca esas devoluciones de llamada. En esta tarea, explorarás un ejemplo más complejo de administración de tareas de ciclo de vida en la app de DessertClicker. Usarás un temporizador que imprime una instrucción de registro cada segundo, con el recuento de la cantidad de segundos que se ha estado ejecutando.

Paso 1: Configura DessertTimer

  1. Abre la app de DessertClicker del último codelab. (Puedes descargar DessertClickerLogs aquí si no tienes la app).
  2. En la vista Project, expande java > com.example.android.dessertclicker y abre DessertTimer.kt. Observa que, en este momento, todo el código está comentado, por lo que no se ejecuta como parte de la app.
  3. Selecciona todo el código en la ventana del editor. Selecciona Code > Comment with Line Comment o presiona Control+/ (Command+/ en Mac). Este comando quita los comentarios de todo el código del archivo. (Es posible que Android Studio muestre errores de referencia sin resolver hasta que vuelvas a compilar la app).
  4. Ten en cuenta que la clase DessertTimer incluye startTimer() y stopTimer(), que inician y detienen el temporizador. Cuando startTimer() se está ejecutando, el temporizador imprime un mensaje de registro cada segundo, con el recuento total de los segundos que se ha ejecutado el temporizador. A su vez, el método stopTimer() detiene el temporizador y las instrucciones de registro.
  1. Abre MainActivity.kt. En la parte superior de la clase, justo debajo de la variable dessertsSold, agrega una variable para el temporizador:
private lateinit var dessertTimer : DessertTimer;
  1. Desplázate hacia abajo hasta onCreate() y crea un nuevo objeto DessertTimer, justo después de la llamada a setOnClickListener():
dessertTimer = DessertTimer()


Ahora que tienes un objeto de temporizador de postre, considera dónde deberías iniciar y detener el temporizador para que se ejecute solo cuando la actividad esté en pantalla. En los próximos pasos, verás algunas opciones.

Paso 2: Inicia y detén el temporizador

Se llama al método onStart() justo antes de que se muestre la actividad. Se llama al método onStop() después de que la actividad deja de ser visible. Estas devoluciones de llamada parecen ser buenas candidatas para iniciar y detener el temporizador.

  1. En la clase MainActivity, inicia el temporizador en la devolución de llamada onStart():
override fun onStart() {
   super.onStart()
   dessertTimer.startTimer()

   Timber.i("onStart called")
}
  1. Detén el temporizador en onStop():
override fun onStop() {
   super.onStop()
   dessertTimer.stopTimer()

   Timber.i("onStop Called")
}
  1. Compila y ejecuta la app. En Android Studio, haz clic en el panel de Logcat. En el cuadro de búsqueda de Logcat, ingresa dessertclicker, que filtrará por las clases MainActivity y DessertTimer. Observa que, una vez que se inicia la app, el temporizador también comienza a ejecutarse de inmediato.
  2. Haz clic en el botón Atrás y observa que el temporizador se detiene de nuevo. El temporizador se detiene porque se destruyeron tanto la actividad como el temporizador que controla.
  3. Usa la pantalla Recientes para volver a la app. Observa en Logcat que el temporizador se reinicia desde 0.
  4. Haz clic en el botón Compartir. Observa en Logcat que el temporizador sigue en ejecución.

  5. Haz clic en el botón Inicio. Observa en Logcat que el temporizador deja de ejecutarse.
  6. Usa la pantalla Recientes para volver a la app. Observa en Logcat que el temporizador se reinicia desde donde lo dejaste.
  7. En MainActivity, en el método onStop(), comenta la llamada a stopTimer(). Si agregas un comentario a stopTimer(), se muestra el caso en el que inicias una operación en onStart(), pero olvidas detenerla de nuevo en onStop().
  8. Compila y ejecuta la app, y haz clic en el botón de inicio después de que se inicie el temporizador. Aunque la app esté en segundo plano, el temporizador se ejecuta y usa recursos del sistema de forma continua. Si el temporizador sigue funcionando, se produce una pérdida de memoria en tu app, y probablemente no sea el comportamiento que deseas.

    El patrón general es que, cuando configuras o inicias algo en una devolución de llamada, detienes o quitas ese elemento en la devolución de llamada correspondiente. De esta manera, evitas que se ejecute algo cuando ya no es necesario.
  1. Quita los comentarios de la línea en onStop() donde detienes el temporizador.
  2. Corta y pega la llamada a startTimer() de onStart() a onCreate(). Este cambio demuestra el caso en el que inicializas y comienzas un recurso en onCreate(), en lugar de usar onCreate() para inicializarlo y onStart() para comenzarlo.
  3. Compila y ejecuta la app. Observa que el temporizador comienza a funcionar, como se espera.
  4. Haz clic en Home para detener la app. El temporizador dejará de funcionar, como se espera.
  5. Usa la pantalla Recientes para volver a la app. Observa que el temporizador no se reinicia en este caso, ya que onCreate() solo se llama cuando se inicia la app, no cuando una app vuelve a primer plano.

Puntos clave para recordar:

  • Cuando configures un recurso en una devolución de llamada de ciclo de vida, también debes desconfigurarlo.
  • Realiza la configuración y la limpieza en los métodos correspondientes.
  • Si configuras algo en onStart(), detén o desmantela la configuración en onStop().

En la app de DessertClicker, es bastante fácil ver que, si iniciaste el temporizador en onStart(), debes detenerlo en onStop(). Solo hay un temporizador, por lo que no es difícil recordar cómo detenerlo.

En una app para Android más compleja, es posible que configures muchas cosas en onStart() o onCreate() y, luego, las desactives todas en onStop() o onDestroy(). Por ejemplo, es posible que tengas animaciones, música, sensores o temporizadores que debas configurar y desmontar, y también iniciar y detener. Si olvidas uno, se producen errores y dolores de cabeza.

La biblioteca de Lifecycle, que forma parte de Android Jetpack, simplifica esta tarea. La biblioteca es especialmente útil en los casos en que debes hacer un seguimiento de muchas partes móviles, algunas de las cuales se encuentran en diferentes estados del ciclo de vida. La biblioteca invierte el funcionamiento de los ciclos de vida: Por lo general, la actividad o el fragmento le indican a un componente (como DessertTimer) qué hacer cuando se produce una devolución de llamada de ciclo de vida. Sin embargo, cuando usas la biblioteca de ciclo de vida, el componente en sí observa los cambios de ciclo de vida y, luego, hace lo que se necesita cuando ocurren esos cambios.

La biblioteca de Lifecycle tiene tres partes principales:

  • Propietarios del ciclo de vida, que son los componentes que tienen (y, por lo tanto, "poseen") un ciclo de vida. Activity y Fragment son propietarios del ciclo de vida. Los propietarios del ciclo de vida implementan la interfaz LifecycleOwner.
  • La clase Lifecycle, que contiene el estado real de un propietario del ciclo de vida y activa eventos cuando se producen cambios en el ciclo de vida
  • Observadores del ciclo de vida, que observan el estado del ciclo de vida y realizan tareas cuando cambia el ciclo de vida Los observadores del ciclo de vida implementan la interfaz LifecycleObserver.

En esta tarea, convertirás la app de DessertClicker para usar la biblioteca de ciclo de vida de Android y aprenderás cómo la biblioteca facilita la administración de los ciclos de vida de actividades y fragmentos de Android.

Paso 1: Convierte DessertTimer en un LifecycleObserver

Una parte clave de la biblioteca de ciclo de vida es el concepto de observación del ciclo de vida. La observación permite que las clases (como DessertTimer) conozcan el ciclo de vida de la actividad o el fragmento, y se inicien y detengan en respuesta a los cambios en esos estados del ciclo de vida. Con un observador del ciclo de vida, puedes quitar la responsabilidad de iniciar y detener objetos de los métodos de actividad y fragmento.

  1. Abre la clase DesertTimer.kt.
  2. Cambia la firma de la clase DessertTimer para que se vea de la siguiente manera:
class DessertTimer(lifecycle: Lifecycle) : LifecycleObserver {

Esta nueva definición de clase hace dos cosas:

  • El constructor toma un objeto Lifecycle, que es el ciclo de vida que observa el temporizador.
  • La definición de la clase implementa la interfaz LifecycleObserver.
  1. Debajo de la variable runnable, agrega un bloque init a la definición de la clase. En el bloque init, usa el método addObserver() para conectar el objeto de ciclo de vida que se pasó desde el propietario (la actividad) a esta clase (el observador).
 init {
   lifecycle.addObserver(this)
}
  1. Anota startTimer() con @OnLifecycleEvent annotation y usa el evento de ciclo de vida ON_START. Todos los eventos de ciclo de vida que tu observador de ciclo de vida puede observar se encuentran en la clase Lifecycle.Event.
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun startTimer() {
  1. Haz lo mismo con stopTimer(), usando el evento ON_STOP:
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stopTimer()

Paso 2: Modifica MainActivity

Tu clase MainActivity ya es propietaria del ciclo de vida a través de la herencia, ya que la superclase FragmentActivity implementa LifecycleOwner. Por lo tanto, no es necesario que hagas nada para que tu actividad tenga en cuenta el ciclo de vida. Todo lo que tienes que hacer es pasar el objeto de ciclo de vida de la actividad al constructor de DessertTimer.

  1. Abre MainActivity. En el método onCreate(), modifica la inicialización de DessertTimer para incluir this.lifecycle:
dessertTimer = DessertTimer(this.lifecycle)

La propiedad lifecycle de la actividad contiene el objeto Lifecycle que posee esta actividad.

  1. Quita la llamada a startTimer() en onCreate() y la llamada a stopTimer() en onStop(). Ya no es necesario que le digas a DessertTimer qué hacer en la actividad, ya que DessertTimer ahora observa el ciclo de vida por sí mismo y recibe notificaciones automáticamente cuando cambia el estado del ciclo de vida. Ahora, todo lo que haces en estas devoluciones de llamada es registrar un mensaje.
  2. Compila y ejecuta la app, y abre Logcat. Observa que el temporizador comenzó a funcionar, como se esperaba.
  3. Haz clic en el botón de inicio para poner la app en segundo plano. Observa que el temporizador dejó de funcionar, como se esperaba.

¿Qué sucede con tu app y sus datos si Android la cierra mientras está en segundo plano? Es importante comprender este caso límite complejo.

Cuando tu app pasa a segundo plano, no se destruye, solo se detiene y espera a que el usuario vuelva a acceder a ella. Sin embargo, una de las principales preocupaciones del SO Android es mantener la actividad que está en primer plano funcionando sin problemas. Por ejemplo, si el usuario está usando una app de GPS para ayudarlo a tomar un autobús, es importante renderizar esa app de GPS rápidamente y seguir mostrando las indicaciones. Es menos importante que la app de DessertClicker, que el usuario tal vez no haya visto en unos días, se ejecute sin problemas en segundo plano.

Android regula las apps en segundo plano para que la de primer plano pueda ejecutarse sin problemas. Por ejemplo, Android limita la cantidad de procesamiento que pueden realizar las apps en segundo plano.

En ocasiones, Android incluso cierra todo el proceso de la app, lo que incluye todas las actividades asociadas con ella. Android realiza este tipo de cierre cuando el sistema está sobrecargado y corre el riesgo de tener un atraso visual, por lo que en este momento no se ejecutan devoluciones de llamadas ni código adicional. El proceso de tu app simplemente se cierra, de manera silenciosa, en segundo plano. Sin embargo, para el usuario, no parece que la app se haya cerrado. Cuando el usuario vuelve a una app que el SO Android cerró, Android la reinicia.

En esta tarea, simularás el cierre de un proceso de Android y examinarás qué sucede con tu app cuando se inicia de nuevo.

Paso 1: Usa adb para simular el cierre de un proceso

Android Debug Bridge (adb) es una herramienta de línea de comandos que te permite enviar instrucciones a emuladores y dispositivos conectados a tu computadora. En este paso, usarás adb para cerrar el proceso de tu app y ver qué sucede cuando Android la cierra.

  1. Compila y ejecuta tu app. Haz clic en el pastelito varias veces.
  2. Presiona el botón de inicio para poner la app en segundo plano. Tu app ahora está detenida y está sujeta a cerrarse si Android necesita los recursos que está usando.
  3. En Android Studio, haz clic en la pestaña Terminal para abrir la terminal de línea de comandos.
  4. Escribe adb y presiona Retorno.

    Si ves muchos resultados que comienzan con Android Debug Bridge version X.XX.X y terminan con tags to be used by logcat (see logcat —help), todo está bien. Si, en cambio, ves adb: command not found, asegúrate de que el comando adb esté disponible en tu ruta de ejecución. Para obtener instrucciones, consulta "Agrega adb a tu ruta de ejecución" en el capítulo Utilidades.
  5. Copia y pega este comentario en la línea de comandos y presiona Retorno:
adb shell am kill com.example.android.dessertclicker

Este comando indica a los dispositivos o emuladores conectados que detengan el proceso con el nombre de paquete dessertclicker, pero solo si la app se ejecuta en segundo plano. Como tu app estaba en segundo plano, no se muestra nada en la pantalla del dispositivo o emulador para indicar que se detuvo el proceso. En Android Studio, haz clic en la pestaña Run para ver un mensaje que dice "Application terminated". Haz clic en la pestaña Logcat para ver que nunca se ejecutó la devolución de llamada onDestroy(). Tu actividad simplemente finalizó.

  1. Usa la pantalla Recientes para volver a la app. Tu app aparecerá en Recientes, ya sea que se haya ejecutado en segundo plano o se haya detenido por completo. Cuando usas la pantalla Recientes para volver a la app, la actividad se inicia de nuevo. La actividad pasa por todo el conjunto de devoluciones de llamada de inicio del ciclo de vida, incluida onCreate().
  2. Observa que, cuando se reinició la app, se restableció tu "puntuación" (tanto la cantidad de postres vendidos como el total en dólares) a los valores predeterminados (0). Si Android cerró tu app, ¿por qué no guardó su estado?

    Cuando el SO reinicia tu app, Android hace todo lo posible para restablecerla al estado en el que se encontraba antes. Android toma el estado de algunas de tus vistas y lo guarda en un paquete cada vez que sales de la actividad. Algunos ejemplos de datos que se guardan automáticamente son el texto en un EditText (siempre que tengan un ID establecido en el diseño) y la pila de actividades de tu actividad.

    Sin embargo, a veces el SO Android no conoce todos tus datos. Por ejemplo, si tienes una variable personalizada como revenue en la app de DessertClicker, el SO de Android no conoce estos datos ni su importancia para tu actividad. Debes agregar estos datos al paquete por tu cuenta.

Paso 2: Usa onSaveInstanceState() para guardar los datos del paquete

El método onSaveInstanceState() es la devolución de llamada que usas para guardar los datos que podrías necesitar si el SO Android destruye tu app. En el diagrama de devolución de llamada de ciclo de vida, se llama a onSaveInstanceState() después de que se detiene la actividad. Se llama cada vez que tu app pasa a segundo plano.

Considera la llamada onSaveInstanceState() como una medida de seguridad. Te da la posibilidad de guardar una pequeña cantidad de información en un paquete cuando tu actividad se va de primer plano. El sistema guarda estos datos ahora porque, si espera hasta que se cierre tu app, es posible que el SO esté bajo presión de recursos. Guardar los datos cada vez garantiza que los datos de actualización en el paquete estén disponibles para restablecerse si es necesario.

  1. En MainActivity, anula la devolución de llamada onSaveInstanceState() y agrega una instrucción de registro Timber.
override fun onSaveInstanceState(outState: Bundle) {
   super.onSaveInstanceState(outState)

   Timber.i("onSaveInstanceState Called")
}
  1. Compila y ejecuta la app, y haz clic en el botón de inicio a fin de ponerla en segundo plano. Ten en cuenta que la devolución de llamada a onSaveInstanceState() se produce justo después de onPause() y onStop():
  2. En la parte superior del archivo, justo antes de la definición de la clase, agrega estas constantes:
const val KEY_REVENUE = "revenue_key"
const val KEY_DESSERT_SOLD = "dessert_sold_key"
const val KEY_TIMER_SECONDS = "timer_seconds_key"

Usarás estas claves para guardar y recuperar datos del paquete de estado de la instancia.

  1. Desplázate hacia abajo hasta onSaveInstanceState() y observa el parámetro outState, que es del tipo Bundle.

    Un paquete es una colección de pares clave-valor en la que las claves siempre son cadenas. Puedes colocar valores primitivos, como valores int y boolean, en el paquete.
    Debido a que el sistema conserva este paquete en la RAM, se recomienda mantener una cantidad pequeña de datos en el paquete. El tamaño de este paquete también es limitado, aunque varía según el dispositivo. Por lo general, debes almacenar mucho menos de 100,000, de lo contrario, corres el riesgo de que tu app falle con el error TransactionTooLargeException.
  2. En onSaveInstanceState(), coloca el valor revenue (un número entero) en el paquete con el método putInt():
outState.putInt(KEY_REVENUE, revenue)

El método putInt() (y métodos similares de la clase Bundle, como putFloat() y putString()), toma dos argumentos: una string para la clave (la constante KEY_REVENUE) y el valor real para guardar.

  1. Repite el mismo proceso con la cantidad de postres vendidos y el estado del temporizador:
outState.putInt(KEY_DESSERT_SOLD, dessertsSold)
outState.putInt(KEY_TIMER_SECONDS, dessertTimer.secondsCount)

Paso 3: Usa onCreate() para restablecer datos de paquetes

  1. Desplázate hasta onCreate() y revisa la firma del método:
override fun onCreate(savedInstanceState: Bundle) {

Observa que, cada vez que se llama a onCreate(), este obtiene un Bundle. Cuando se reinicia tu actividad debido a un cierre de proceso, el paquete que guardaste se pasa a onCreate(). Si se inició la actividad de nuevo, este paquete en onCreate() es null. Entonces, si el paquete no es null, sabes que estás "volviendo a crear" la actividad desde un punto conocido anteriormente.

  1. Agrega este código a onCreate(), después de la configuración de DessertTimer:
if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
}

La prueba de null determina si hay datos en el paquete o si este es null, lo que a su vez te indicará si la app se inició de nuevo o si se volvió a crear después de un cierre. Esta prueba es un patrón común para restablecer datos del paquete.

Ten en cuenta que la clave que usaste aquí (KEY_REVENUE) es la misma que usaste para putInt(). A fin de garantizar que uses la misma clave cada vez, te recomendamos que definas esas claves como constantes. Usa getInt() a fin de obtener los datos del paquete, tal como usaste putInt() a efectos de colocar datos en él. El método getInt() toma dos argumentos:

  • Una string que actúa como la clave, por ejemplo, "key_revenue" para el valor de los importes
  • Un valor predeterminado en caso de que no exista uno para esa clave en el paquete

El número entero que se obtiene del paquete se asigna a la variable revenue y la IU usará ese valor.

  1. Agrega métodos getInt() para restablecer la cantidad de postres vendidos y el valor del temporizador:
if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
   dessertTimer.secondsCount =
       savedInstanceState.getInt(KEY_TIMER_SECONDS, 0)
}
  1. Compila y ejecuta la app. Presiona el pastelito al menos cinco veces hasta que cambie a una rosquilla. Haz clic en Inicio para poner la app en segundo plano.
  2. En la pestaña Terminal de Android Studio, ejecuta adb para cerrar el proceso de la app.
adb shell am kill com.example.android.dessertclicker
  1. Usa la pantalla Recientes para volver a la app. Observa que, esta vez, la app regresa con los valores correctos de ingresos y postres vendidos del paquete. No obstante, también ten en cuenta que el postre volvió a ser un pastelito. Queda un paso más para asegurarse de que la app regrese desde un cierre a la manera exacta en la que estaba.
  2. En MainActivity, revisa el método showCurrentDessert(). Ten en cuenta que este método determina qué imagen de postre se debe mostrar en la actividad según la cantidad actual de postres vendidos y la lista de postres en la variable allDesserts.
for (dessert in allDesserts) {
   if (dessertsSold >= dessert.startProductionAmount) {
       newDessert = dessert
   }
    else break
}

Este método depende de la cantidad de postres vendidos a fin de elegir la imagen correcta. Por lo tanto, no es necesario realizar ninguna acción para almacenar una referencia a la imagen en el paquete de onSaveInstanceState(). En ese paquete, ya almacenaste la cantidad de postres que se venden.

  1. En onCreate(), en el bloque que restablece el estado del paquete, llama a showCurrentDessert():
 if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
   dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
   dessertTimer.secondsCount = 
      savedInstanceState.getInt(KEY_TIMER_SECONDS, 0)
   showCurrentDessert()                   
}
  1. Compila y ejecuta la app, y ponla en segundo plano. Usa adb para detener el proceso. Usa la pantalla Recientes para volver a la app. Observa ahora que la cantidad de postres vendidos, los ingresos totales y la imagen del postre se restablecen correctamente.

Hay un último caso especial en la administración del ciclo de vida de la actividad y el fragmento que es importante comprender: se trata de cómo los cambios en la configuración afectan el ciclo de vida de tus actividades y fragmentos.

Un cambio de configuración ocurre cuando cambia el estado del dispositivo de manera tan radical que la forma más simple de que el sistema resuelva el cambio es cerrar y volver a crear la actividad por completo. Por ejemplo, si el usuario cambia el idioma del dispositivo, es posible que todo el diseño deba cambiar para adaptarse a diferentes orientaciones de texto. Si el usuario enchufa el dispositivo a un conector o agrega un teclado físico, es posible que el diseño de la app deba aprovechar otro diseño o tamaño de visualización. Además, si cambia la orientación del dispositivo (si el dispositivo rota del modo de retrato al modo de paisaje, o viceversa), es posible que el diseño deba modificarse a fin de que se ajuste a la nueva orientación.

Paso 1: Explora la rotación del dispositivo y las devoluciones de llamada del ciclo de vida

  1. Compila y ejecuta tu app, y abre Logcat.
  2. Rota el dispositivo o el emulador al modo horizontal. Puedes rotar el emulador hacia la izquierda o la derecha con los botones de rotación o con Control y las teclas de flecha (Command y las teclas de flecha en Mac).
  3. Revisa el resultado en Logcat. Filtra el resultado en MainActivity.
    Ten en cuenta que, cuando el dispositivo o el emulador rotan la pantalla, el sistema llama a todas las devoluciones de llamada de ciclo de vida para cerrar la actividad. Luego, mientras se vuelve a crear la actividad, el sistema llama a todas las devoluciones de llamada de ciclo de vida para iniciarla.
  4. En MainActivity, comenta todo el método onSaveInstanceState().
  5. Vuelve a compilar y ejecutar tu app. Haz clic en el pastelito varias veces y rota el dispositivo o el emulador. Esta vez, cuando se rota el dispositivo y la actividad se cierra y se vuelve a crear, esta se inicia con los valores predeterminados.

    Cuando se produce un cambio de configuración, Android usa el mismo paquete de estado de instancia que aprendiste en la tarea anterior para guardar y restablecer el estado de la app. Al igual que con el cierre del proceso, usa onSaveInstanceState() para colocar los datos de la app en el paquete. Luego, restablece los datos en onCreate() para evitar perder los datos del estado de la actividad si se rota el dispositivo.
  6. En MainActivity, quita la marca de comentario del método onSaveInstanceState(), ejecuta la app, haz clic en el cupcake y rota la app o el dispositivo. Observa que, esta vez, los datos de postres se conservan durante la rotación de la actividad.

Proyecto de Android Studio: DessertClickerFinal

Sugerencias sobre el ciclo de vida

  • Si configuras o inicias algo en una devolución de llamada de ciclo de vida, detén o quita ese elemento en la devolución de llamada correspondiente. Al detener el elemento, te aseguras de que no siga funcionando cuando ya no sea necesario. Por ejemplo, si configuras un temporizador en onStart(), debes pausarlo o detenerlo en onStop().
  • Usa onCreate() solo para inicializar las partes de tu app que se ejecutan una vez, cuando se inicia la app por primera vez. Usa onStart() para iniciar las partes de tu app que se ejecutan cuando se inicia la app y cada vez que vuelve al primer plano.

Biblioteca de Lifecycle

  • Usa la biblioteca de ciclos de vida de Android para transferir el control del ciclo de vida de la actividad o el fragmento al componente real que debe tener en cuenta el ciclo de vida.
  • Los propietarios del ciclo de vida son componentes que tienen (y, por lo tanto, "poseen") ciclos de vida, incluidos Activity y Fragment. Los propietarios del ciclo de vida implementan la interfaz LifecycleOwner.
  • Los observadores del ciclo de vida prestan atención al estado actual del ciclo de vida y realizan tareas cuando cambia el ciclo de vida. Los observadores del ciclo de vida implementan la interfaz LifecycleObserver.
  • Los objetos Lifecycle contienen los estados del ciclo de vida reales y activan eventos cuando cambia el ciclo de vida.

Para crear una clase que priorice el ciclo de vida, haz lo siguiente:

  • Implementa la interfaz LifecycleObserver en las clases que deben tener en cuenta el ciclo de vida.
  • Inicializa una clase de observador del ciclo de vida con el objeto de ciclo de vida de la actividad o el fragmento.
  • En la clase del observador del ciclo de vida, anota los métodos optimizados para el ciclo de vida con el cambio de estado del ciclo de vida que les interesa.

    Por ejemplo, la anotación @OnLifecycleEvent(Lifecycle.Event.ON_START) indica que el método está observando el evento de ciclo de vida onStart.

Cierre de procesos y guardado del estado de la actividad

  • Android regula las apps que se ejecutan en segundo plano para que la de primer plano pueda ejecutarse sin problemas. Esta regulación incluye limitar la cantidad de procesamiento que pueden realizar las apps en segundo plano y, a veces, incluso cerrar todo el proceso de la app.
  • El usuario no puede saber si el sistema cerró una app en segundo plano. La app sigue apareciendo en la pantalla de Recientes y debería reiniciarse en el mismo estado en el que la dejó el usuario.
  • Android Debug Bridge (adb) es una herramienta de línea de comandos que te permite enviar instrucciones a emuladores y dispositivos conectados a tu computadora. Puedes usar adb para simular el cierre de un proceso en tu app.
  • Cuando Android cierra el proceso de tu app, no se llama al método del ciclo de vida onDestroy(). La app simplemente se detiene.

Cómo conservar el estado de actividades y fragmentos

  • Cuando la app pasa a segundo plano, justo después de que se llama a onStop(), los datos de la app se guardan en un paquete. Algunos datos de app, como el contenido de un EditText, se guardan automáticamente.
  • El paquete es una instancia de Bundle, que es una colección de claves y valores. Las claves siempre son strings.
  • Usa la devolución de llamada onSaveInstanceState() para guardar otros datos en el paquete que quieras conservar, incluso si la app se cerró automáticamente. Para colocar datos en el paquete, usa los métodos de paquete que comienzan con put, como putInt().
  • Puedes recuperar los datos del paquete en el método onRestoreInstanceState() o con mayor frecuencia en onCreate(). El método onCreate() tiene un parámetro savedInstanceState que contiene el paquete.
  • Si la variable savedInstanceState contiene null, se inició la actividad sin un paquete de estado y no hay datos de estado para recuperar.
  • A fin de recuperar datos del paquete con una clave, usa los métodos Bundle que comienzan con get, como getInt().

Cambios de configuración

  • Un cambio de configuración ocurre cuando cambia el estado del dispositivo de manera tan radical que la forma más simple de que el sistema resuelva el cambio es cerrar y volver a crear la actividad.
  • El ejemplo más común de un cambio de configuración es cuando el usuario rota el dispositivo del modo vertical al modo horizontal, o viceversa. Un cambio de configuración también puede ocurrir cuando cambia el idioma del dispositivo o se conecta un teclado físico.
  • Cuando se produce un cambio de configuración, Android invoca todas las devoluciones de llamada de cierre del ciclo de vida de la actividad. Luego, Android reinicia la actividad desde cero y ejecuta todas las devoluciones de llamada de inicio del ciclo de vida.
  • Cuando Android cierra una app debido a un cambio de configuración, reinicia la actividad con el paquete de estado disponible para onCreate().
  • Al igual que con el cierre del proceso, guarda el estado de tu app en el paquete, en onSaveInstanceState().

Curso de Udacity:

Documentación para desarrolladores de Android:

Otro:

En esta sección, se enumeran las posibles actividades para el hogar para los alumnos que trabajan en este codelab como parte de un curso dirigido por un instructor. Depende del instructor hacer lo siguiente:

  • Si es necesario, asigna una tarea.
  • Comunicarles a los alumnos cómo enviar las actividades para el hogar.
  • Califica las actividades para el hogar.

Los instructores pueden usar estas sugerencias en la medida que quieran y deben asignar cualquier otra actividad para el hogar que consideren apropiada.

Si estás trabajando en este codelab por tu cuenta, usa estas actividades para el hogar para probar tus conocimientos.

Cómo cambiar una app

Abre la app de DiceRoller de la lección 1. (Si no la tienes, puedes descargar la app aquí). Compila y ejecuta la app, y observa que, si rotas el dispositivo, se pierde el valor actual del dado. Implementa onSaveInstanceState() para conservar ese valor en el paquete y restablecerlo en onCreate().

Responde estas preguntas:

Pregunta 1

Tu app contiene una simulación física que requiere un procesamiento intensivo para mostrarse. Luego, el usuario recibe una llamada telefónica. ¿Cuál de estas afirmaciones es verdadera?

  • Durante la llamada telefónica, debes continuar procesando las posiciones de los objetos en la simulación física.
  • Durante una llamada telefónica, debes dejar de procesar las posiciones de los objetos en la simulación física.

Pregunta 2

¿Qué método de ciclo de vida deberías anular para pausar la simulación cuando la app no está en pantalla?

  • onDestroy()
  • onStop()
  • onPause()
  • onSaveInstanceState()

Pregunta 3

Para hacer que una clase priorice el ciclo de vida a través de la biblioteca de ciclo de vida de Android, ¿qué interfaz debe implementar la clase?

  • Lifecycle
  • LifecycleOwner
  • Lifecycle.Event
  • LifecycleObserver

Pregunta 4

¿Bajo qué circunstancias el método onCreate() en tu actividad recibe un objeto Bundle con datos en él (es decir, que el objeto Bundle no es null)? Es posible que se aplique más de una respuesta.

  • La actividad se reinicia luego de que el dispositivo se rota.
  • La actividad se inicia desde cero.
  • Se retoma la actividad una vez que vuelve del segundo plano.
  • Se reinicia el dispositivo.

Comienza la siguiente lección: 5.1: ViewModel y ViewModelFactory

Para obtener vínculos a otros codelabs de este curso, consulta la página de destino de los codelabs de Conceptos básicos de Kotlin para Android.