Cómo guardar estados de IU

Preservar y restablecer el estado de la IU de una actividad de manera oportuna durante una actividad iniciada por el sistema o la destrucción de la aplicación es una parte fundamental de la experiencia del usuario. En estos casos, el usuario espera que se conserve el estado de la IU, pero el sistema destruye la actividad y cualquier estado almacenado en ella.

Para salvar las diferencias entre la expectativa del usuario y el comportamiento del sistema, usa una combinación de objetos ViewModel, el método onSaveInstanceState() o almacenamiento local a fin de preservar el estado de la IU durante estas transiciones de instancias de aplicaciones y actividades. Para decidir cómo combinar estas opciones, se debe tener en cuenta la complejidad de los datos de la IU, el tipo de uso de la app y la velocidad de recuperación en relación con el uso de la memoria.

Sin importar el enfoque que adoptes, debes asegurarte de que tu app cumpla con las expectativas de los usuarios con respecto al estado de la IU, y de que proporcione una IU fluida y ágil (evita el retraso durante la carga de datos en la IU, en especial después de cambios de configuración frecuentes, como la rotación). En la mayoría de los casos, debes usar ViewModel y onSaveInstanceState().

En esta página, se analizan las expectativas de los usuarios sobre el estado de la IU, las opciones disponibles para preservar el estado y las compensaciones y limitaciones de cada una.

Expectativas del usuario y comportamiento del sistema

Según la acción que realiza, el usuario espera que se borre o se conserve el estado de la actividad. En algunos casos, el sistema hace automáticamente lo que espera el usuario. En otros casos, hace lo contrario.

Descarte del estado de la IU iniciado por el usuario

El usuario espera que cuando comience una actividad, el estado transitorio de la IU de esa actividad permanezca igual hasta que descarte por completo la actividad. El usuario puede descartar una actividad por completo con una de estas acciones:

  • presionar el botón Atrás
  • deslizar la actividad hacia fuera de la pantalla Overview (Recents)
  • navegar hacia arriba desde la actividad
  • eliminar la aplicación de la pantalla Configuración
  • completar algún tipo de actividad de "finalización" (que está respaldada por Activity.finish())

En estos casos de descartes completos, el usuario asume que se alejó de manera permanente de la actividad, y que si vuelve a abrirla, espera que esta comience desde cero. El comportamiento subyacente del sistema coincide con la expectativa del usuario: se destruye la instancia de la actividad y se la quita de la memoria, junto con cualquier estado almacenado en ella y cualquier registro de estado de instancia guardado y asociado con la actividad.

Existen algunas excepciones a esta regla sobre el descarte completo. Por ejemplo, es posible que un usuario espere que un navegador lo direccione a la página web exacta que estaba viendo antes de salir del navegador usando el botón Atrás.

Descarte del estado de la IU iniciado por el sistema

El usuario espera que se conserve el estado de la IU de una actividad durante un cambio de configuración, como la rotación o el cambio al modo multiventana. Sin embargo, de forma predeterminada, el sistema destruye la actividad cuando se produce este cambio de configuración, y borra cualquier estado de IU almacenado en la instancia de la actividad. Para obtener más información sobre la configuración de los dispositivos, consulta la página de referencia sobre la configuración. Ten en cuenta que es posible (aunque no se recomienda) anular el comportamiento predeterminado para los cambios de configuración. Para obtener más detalles, consulta Cómo manejar por tu cuenta el cambio de configuración.

El usuario también espera que se conserve el estado de la IU de tu actividad si cambia temporalmente a una app diferente y vuelve a la app más tarde. Por ejemplo, el usuario hace una búsqueda y, luego, presiona el botón de inicio o responde una llamada telefónica. Cuando vuelve a la actividad de búsqueda, el usuario espera encontrar la palabra clave de búsqueda y los resultados exactamente como estaban antes.

En este escenario, tu app se ejecuta en segundo plano y el sistema hace todo lo posible para mantener el proceso de tu app en la memoria. Sin embargo, el sistema puede destruir el proceso de la aplicación mientras el usuario está interactuando con otras apps. En ese caso, se destruye la instancia de la actividad, junto con cualquier estado almacenado en ella. Cuando el usuario reinicia la app, la actividad se encuentra inesperadamente en una lista nueva. Para obtener más información sobre el cierre de procesos, consulta Ciclo de vida de procesos y aplicaciones.

Opciones para preservar el estado de la IU

Cuando las expectativas del usuario sobre el estado de la IU no coinciden con el comportamiento predeterminado del sistema, debes guardar y restablecer el estado de la IU del usuario para garantizar que la destrucción iniciada por el sistema sea transparente para el usuario.

Cada una de las opciones para preservar el estado de la IU varía según las siguientes dimensiones que afectan la experiencia del usuario:

ViewModel Estado de instancia guardado Almacenamiento persistente
Ubicación del almacenamiento En la memoria Serializado en disco En disco o red
Se mantiene tras el cambio de configuración
Se mantiene tras el cierre de procesos iniciados por el sistema No
Se mantiene tras el descarte completo/onFinish() de la actividad realizado por el usuario No No
Limitaciones de datos Objetos complejos bien, pero espacio limitado por la memoria disponible Solo para tipos primitivos y objetos pequeños y simples, como strings Solo limitado por el espacio en disco o el costo/tiempo de recuperación del recurso de red
Tiempo de lectura/escritura Rápido (solo acceso a memoria) Lento (requiere serialización/deserialización y acceso al disco) Lento (requiere acceso a disco o transacción de red)

Cómo usar ViewModel para manejar los cambios de configuración

ViewModel es ideal para almacenar y administrar datos relacionados con la IU mientras el usuario usa la aplicación de manera activa. Permite un acceso rápido a los datos de la IU y te ayuda a evitar la recuperación de datos de la red o el disco durante la rotación, el cambio de tamaño de la ventana y otros cambios de configuración habituales. Para aprender cómo implementar un ViewModel, consulta la guía de ViewModel.

ViewModel conserva los datos en la memoria, lo que significa que es más económico recuperarlos que a los datos del disco o la red. Un ViewModel está asociado con una actividad (o algún otro propietario del ciclo de vida): permanece en la memoria durante un cambio de configuración y el sistema asocia automáticamente el ViewModel con la nueva instancia de actividad que resulta del cambio de configuración.

El sistema destruye ViewModels de forma automática cuando el usuario cancela tu actividad o fragmento, o si llamas a finish(), lo que indica que se borrará el estado, como el usuario espera en estas situaciones.

A diferencia del estado de instancia guardado, los ViewModels se destruyen durante el cierre de un proceso iniciado por el sistema. Esta es la razón por la que debes usar los objetos ViewModel junto con onSaveInstanceState() (o alguna otra persistencia de disco), y reservar los identificadores en salvadoInstanceState para ayudar a que los modelos de vista vuelvan a cargar los datos después del cierre del sistema.

Si ya tienes una solución en la memoria para almacenar el estado de la IU durante los cambios de configuración, es posible que no necesites usar ViewModel.

Cómo usar onSaveInstanceState() como copia de seguridad para manejar el cierre de un proceso iniciado por el sistema

La devolución de llamada onSaveInstanceState() almacena los datos necesarios para volver a cargar el estado de un controlador de IU, como una actividad o un fragmento, si el sistema destruye el controlador y, luego, lo recrea. Para obtener información sobre cómo implementar el estado de la instancia guardada, consulta Cómo guardar y restablecer el estado de la actividad en la Guía del ciclo de vida de la actividad.

Los paquetes de estado de la instancia que se guardaron se conservan tanto durante los cambios de configuración como durante el cierre del proceso, pero están limitados por la cantidad de almacenamiento y la velocidad porque onSavedInstanceState() serializa los datos en el disco. La serialización puede consumir mucha memoria si los objetos que se serializan son demasiado complejos. Debido a que este proceso se lleva a cabo en el subproceso principal durante un cambio de configuración, la serialización puede provocar una disminución de los marcos e interrupciones visuales si lleva demasiado tiempo.

No uses onSavedInstanceState() para almacenar grandes cantidades de datos, como mapas de bits o estructuras de datos complejas que requieran serialización o deserialización extensas. En cambio, almacena solo tipos primitivos y objetos pequeños y simples, como strings. Por lo tanto, debes usar onSaveInstanceState() para almacenar una cantidad mínima de datos necesarios, como un ID, a fin de volver a crear los datos necesarios para restablecer el estado anterior de la IU, si fallan los otros mecanismos de persistencia. La mayoría de las aplicaciones deberían implementar onSaveInstanceState() para manejar el cierre del proceso iniciado por el sistema.

Según los casos prácticos de tu app, es posible que no necesites usar onSaveInstanceState() en absoluto. Por ejemplo, un navegador podría llevar al usuario exactamente a la misma página web que estaba viendo antes de salir del navegador. Si tu actividad se comporta de este modo, puedes no usar onSaveInstanceState() y, en su lugar, conservar todo a nivel local.

Además, cuando abres una actividad a partir de un intent, el paquete de elementos adicionales se entrega a la actividad, tanto cuando la configuración cambia como cuando el sistema restablece la actividad. Si la consulta de búsqueda se trasladara como un intent adicional, podrías usar el paquete de elementos adicionales, en lugar del paquete onSaveInstanceState(). Para obtener más información sobre los intents adicionales, consulta Intents y filtros de intents.

En ambos casos, usarías un ViewModel para no malgastar ciclos recargando datos de la base de datos durante un cambio de configuración.

Si los datos de la IU que se preservarán son simples y livianos, recomendamos usar solamente onSaveInstanceState().

Nota: Ahora puedes proporcionar acceso a un estado guardado en objetos ViewModel con el módulo de estado guardado para ViewModel (actualmente en la versión alfa). Se puede acceder a este estado guardado mediante un objeto llamado SavedStateHandle. Puedes ver cómo se usa en el codelab Componentes optimizados para ciclos de vida de Android.

Cómo usar la persistencia local para manejar el cierre de procesos para datos complejos o grandes

Se conservará el almacenamiento local persistente, como una base de datos o preferencias compartidas, mientras tu aplicación esté instalada en el dispositivo del usuario (a menos que el usuario borre los datos de tu app). Si bien este almacenamiento local se conserva tras la actividad iniciada por el sistema y el cierre del proceso de la aplicación, puede ser costoso recuperarlo, ya que se tendrá que leer en la memoria. A menudo, este almacenamiento local persistente puede ser parte de la arquitectura de tu aplicación, a fin de almacenar todos los datos que no deseas perder si abres y cierras la actividad.

Ni ViewModel ni el estado guardado de la instancia son soluciones de almacenamiento a largo plazo y, por lo tanto, no reemplazan al almacenamiento local, como una base de datos. En cambio, debes usar estos mecanismos para almacenar temporalmente el estado transitorio de la IU y usar el almacenamiento persistente para otros datos de la app. Consulta la Guía de arquitectura de apps si deseas obtener más detalles sobre cómo aprovechar el almacenamiento local para conservar a largo plazo los datos del modelo de tu app (por ejemplo, durante los reinicios del dispositivo).

Cómo administrar el estado de la IU: divide y vencerás

Puedes guardar y restablecer de manera eficaz el estado de la IU dividiendo el trabajo entre los diversos tipos de mecanismos de persistencia. En la mayoría de los casos, cada uno de estos mecanismos debe almacenar un tipo diferente de datos utilizados en la actividad, en función de las compensaciones de la complejidad de los datos, la velocidad de acceso y el ciclo de vida:

  • Persistencia local: Almacena todos los datos que no quieras perder cuando abras y cierres la actividad.
    • Ejemplo: Una colección de canciones, que puede incluir archivos de audio y metadatos.
  • ViewModel: Almacena en la memoria todos los datos necesarios para mostrar el controlador de IU asociado.
    • Ejemplo: Las canciones de la búsqueda más reciente y la consulta de búsqueda más reciente.
  • onSaveInstanceState(): Almacena una pequeña cantidad de datos necesarios para volver a cargar fácilmente el estado de una actividad si el sistema se detiene y, luego, vuelve a crear el controlador de IU. En lugar de almacenar objetos complejos en este lugar, consérvalos en un almacenamiento local y almacena un ID único para estos objetos en onSaveInstanceState().
    • Ejemplo: Almacenar la consulta de búsqueda más reciente.

Como ejemplo, considera una actividad que te permita buscar en tu biblioteca de canciones. Los distintos eventos se deben administrar de la siguiente manera:

Cuando el usuario agrega una canción, ViewModel determina de inmediato que estos datos se conservarán a nivel local. Si esta canción recién agregada debe mostrarse en la IU, también deberás actualizar los datos en el objeto ViewModel para que refleje que se agregó la canción. Recuerda que debes agregar fuera del subproceso principal todo lo que agregues a la base de datos.

Cuando el usuario busque una canción, sin importar la complejidad de los datos de canciones que cargues desde la base de datos para el controlador de IU, se debería almacenar de inmediato en el objeto ViewModel. También deberías guardar la consulta de búsqueda en el objeto ViewModel.

Cuando la actividad pasa a segundo plano, el sistema realiza una llamada a onSaveInstanceState(). Debes guardar la búsqueda en el paquete onSaveInstanceState(). Esta cantidad de datos pequeña es fácil de guardar. Además, es toda la información que necesitas para que la actividad vuelva a su estado actual.

Cómo restablecer estados complejos: volver a ensamblar las piezas

Cuando sea el momento de que el usuario vuelva a la actividad, hay dos casos posibles para recrearla:

  • La actividad se recrea una vez que el sistema la detuvo. La actividad tiene la consulta guardada en un paquete onSaveInstanceState() y debería transferir la consulta a ViewModel. El objeto ViewModel ve que no tiene resultados de búsqueda en la memoria caché y delega la carga de los resultados de búsqueda mediante la consulta de búsqueda proporcionada.
  • La actividad se crea después de un cambio de configuración. La actividad guarda la consulta en un paquete onSaveInstanceState() y el ViewModel ya tiene los resultados de búsqueda en la memoria caché. Debes trasladar la consulta del paquete onSaveInstanceState() al ViewModel, que determina que ya cargó los datos necesarios y que no necesita volver a consultar la base de datos.

Nota: Cuando se crea una actividad por primera vez, el paquete onSaveInstanceState() no contiene datos y el objeto ViewModel está vacío. Cuando creas el objeto ViewModel, trasladas una consulta vacía, que le indica al objeto ViewModel que todavía no hay datos para cargar. Por lo tanto, la actividad comienza en un estado vacío.

Recursos adicionales

Para obtener más información sobre cómo guardar estados de la IU, consulta los siguientes recursos.

Blogs