En esta guía, se analizan las expectativas de los usuarios sobre el estado de la IU y las opciones disponibles para preservarlos.
El guardado y restablecimiento rápido del estado de la IU de una actividad después de que el sistema destruya actividades o aplicaciones es esencial para una buena experiencia del usuario. Los usuarios esperan que el estado de la IU se conserve, pero el sistema puede destruir la actividad y su estado almacenado.
Para salvar las diferencias entre la expectativa del usuario y el comportamiento del sistema, usa una combinación de los siguientes métodos:
- Objetos
ViewModel
- Estados de instancia guardados dentro de los siguientes contextos:
- Jetpack Compose:
rememberSaveable
- Objetos View: API de
onSaveInstanceState()
. - ViewModels:
SavedStateHandle
- Jetpack Compose:
- Almacenamiento local para conservar el estado de la IU durante las transiciones de apps y actividad
La solución óptima depende de la complejidad de los datos de tu IU, de los casos de uso de tu app y de encontrar un equilibrio entre la velocidad de acceso a los datos y el uso de la memoria.
Asegúrate de que tu app cumpla con las expectativas de los usuarios y ofrezca una interfaz responsiva y rápida. Evita demoras cuando se cargan datos en la IU, en especial después de cambios de configuración comunes, como la rotación.
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, 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:
- Deslizar la actividad hacia fuera de la pantalla Recientes
- Cerrar la app o forzar su cierre desde la pantalla Configuración
- Reiniciar el dispositivo
- Completar algún tipo de acción de "finalización" (con el respaldo de
Activity.finish()
)
En estos casos de descartes completos, el usuario da por sentado que se alejó de manera permanente de la actividad, y que, si vuelve a abrirla, comenzará de nuevo. El comportamiento subyacente del sistema coincide con la expectativa del usuario: se destruye la instancia de la actividad y se 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 limpia 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 Maneja el cambio de configuración por tu cuenta.
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 tuya 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 regresa a la actividad de búsqueda, espera encontrar la palabra clave de la 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 vuelve inesperadamente a su estado inicial. 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 | en la memoria | en el disco o en la red |
Se mantiene tras el cambio de configuración | Sí | Sí | Sí |
Se mantiene tras el cierre de procesos iniciados por el sistema | No | Sí | Sí |
Se mantiene tras el descarte completo/onFinish() de la actividad realizado por el usuario | No | No | Sí |
Limitaciones de datos | Se aceptan objetos complejos, pero el espacio es limitado por la memoria disponible | Solo para tipos primitivos y objetos pequeños y simples, como strings | Solo limitado por el espacio en el 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) | Lento (requiere acceso al 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 a 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 recuperar esos datos que los 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 lo asocia automáticamente con la nueva instancia de actividad que resulta del cambio de configuración.
El sistema destruye ViewModels automáticamente cuando el usuario cancela tu actividad o fragmento, o bien 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. Para volver a cargar datos después del cierre de un proceso iniciado por el sistema en un ViewModel, usa la API de SavedStateHandle
. Como alternativa, si los datos se relacionan con la IU y no necesitan mantenerse en el ViewModel, usa onSaveInstanceState()
en el sistema de View o rememberSaveable
en Jetpack Compose. Si los datos son datos de la aplicación, podría ser conveniente conservarlos en el disco.
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 el estado de instancia guardado como copia de seguridad para controlar el cierre de un proceso iniciado por el sistema
La devolución de llamada onSaveInstanceState()
en el sistema de View, rememberSaveable
en Jetpack Compose y SavedStateHandle
en ViewModels almacenan 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. Si deseas obtener información para implementar el estado de instancia de guardado con onSaveInstanceState
, 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 instancia guardada se conservan durante los cambios de configuración y los cierres de procesos, pero están limitados por el almacenamiento y la velocidad, ya que las diferentes APIs serializan datos. 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, una serialización de larga duración puede provocar una disminución de los marcos y saltos visuales.
No uses el estado de instancia de guardado para almacenar grandes cantidades de datos, como mapas de bits, ni estructuras de datos complejas que requieran serializaciones o deserializaciones extensas. En su lugar, almacena solo tipos primitivos y objetos simples y pequeños, como String
. Por lo tanto, usa el estado de instancia guardado para almacenar una cantidad mínima de datos necesarios, como un ID, y volver a crear los datos necesarios a fin de restablecer la IU a su estado anterior en caso de que fallen los demás mecanismos de conservación. La mayoría de las apps deberían implementar estas sugerencias 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 el estado de instancia de guardado 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 esta manera, no necesitas usar el estado de instancia de guardado y, en su lugar, puedes conservar todo a nivel local.
Además, cuando abres una actividad a partir de un intento, el paquete de elementos adicionales se entrega a la actividad tanto cuando la configuración cambia como cuando el sistema restaura la actividad. Si una parte de los datos de estado de la IU, como una búsqueda, se pasara como un intent adicional cuando se lanzara una actividad, podrías usar el paquete de elementos adicionales en lugar del paquete de estado de instancia de guardado. Para obtener más información sobre los intents adicionales, consulta Intents y filtros de intents.
En cualquiera de estos casos, debes usar un ViewModel
para evitar desperdiciar ciclos recargando datos de la base de datos durante un cambio de configuración.
En los casos en que los datos de la IU que se desea conservar fueran simples y livianos, puedes usar solo las API de estado de instancia guardado para preservar tus datos de estado.
Cómo vincular el estado guardado con SavedStateRegistry
A partir de Fragment 1.1.0 o su dependencia transitiva Activity 1.0.0, los controladores de IU, como Activity
o Fragment
, implementan SavedStateRegistryOwner
y proporcionan un SavedStateRegistry
vinculado a ese controlador. SavedStateRegistry
permite que los componentes se vinculen con el estado guardado de tu controlador de IU para consumirlo o contribuir a él. Por ejemplo, el módulo de estado guardado para ViewModel usa SavedStateRegistry
para crear un SavedStateHandle
y proporcionarlo a tus objetos ViewModel
. Puedes recuperar el SavedStateRegistry
desde el controlador de IU llamando a getSavedStateRegistry()
.
Los componentes que contribuyen al estado guardado deben implementar SavedStateRegistry.SavedStateProvider
, que define un solo método llamado saveState()
. El método saveState()
permite que tu componente muestre un Bundle
que contiene cualquier estado que se deba guardar desde ese componente.
SavedStateRegistry
llama a este método durante la fase de guardado del ciclo de vida del controlador de IU.
Kotlin
class SearchManager : SavedStateRegistry.SavedStateProvider { companion object { private const val QUERY = "query" } private val query: String? = null ... override fun saveState(): Bundle { return bundleOf(QUERY to query) } }
Java
class SearchManager implements SavedStateRegistry.SavedStateProvider { private static String QUERY = "query"; private String query = null; ... @NonNull @Override public Bundle saveState() { Bundle bundle = new Bundle(); bundle.putString(QUERY, query); return bundle; } }
Para registrar un SavedStateProvider
, llama a registerSavedStateProvider()
en el SavedStateRegistry
y pasa una clave para asociarla con el proveedor y sus datos. Los datos del proveedor guardados anteriormente se pueden recuperar del estado guardado llamando a consumeRestoredStateForKey()
en SavedStateRegistry
y pasando la clave asociada con los datos del proveedor.
En una Activity
o un Fragment
, puedes registrar un SavedStateProvider
en onCreate()
después de llamar a super.onCreate()
. Como alternativa, puedes establecer un LifecycleObserver
en un SavedStateRegistryOwner
, que implementa LifecycleOwner
y registra el SavedStateProvider
una vez que ocurre el evento ON_CREATE
. Si usas un LifecycleObserver
, puedes separar el registro y la recuperación del estado guardado previamente desde el SavedStateRegistryOwner
.
Kotlin
class SearchManager(registryOwner: SavedStateRegistryOwner) : SavedStateRegistry.SavedStateProvider { companion object { private const val PROVIDER = "search_manager" private const val QUERY = "query" } private val query: String? = null init { // Register a LifecycleObserver for when the Lifecycle hits ON_CREATE registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_CREATE) { val registry = registryOwner.savedStateRegistry // Register this object for future calls to saveState() registry.registerSavedStateProvider(PROVIDER, this) // Get the previously saved state and restore it val state = registry.consumeRestoredStateForKey(PROVIDER) // Apply the previously saved state query = state?.getString(QUERY) } } } override fun saveState(): Bundle { return bundleOf(QUERY to query) } ... } class SearchFragment : Fragment() { private var searchManager = SearchManager(this) ... }
Java
class SearchManager implements SavedStateRegistry.SavedStateProvider { private static String PROVIDER = "search_manager"; private static String QUERY = "query"; private String query = null; public SearchManager(SavedStateRegistryOwner registryOwner) { registryOwner.getLifecycle().addObserver((LifecycleEventObserver) (source, event) -> { if (event == Lifecycle.Event.ON_CREATE) { SavedStateRegistry registry = registryOwner.getSavedStateRegistry(); // Register this object for future calls to saveState() registry.registerSavedStateProvider(PROVIDER, this); // Get the previously saved state and restore it Bundle state = registry.consumeRestoredStateForKey(PROVIDER); // Apply the previously saved state if (state != null) { query = state.getString(QUERY); } } }); } @NonNull @Override public Bundle saveState() { Bundle bundle = new Bundle(); bundle.putString(QUERY, query); return bundle; } ... } class SearchFragment extends Fragment { private SearchManager searchManager = new SearchManager(this); ... }
Cómo usar la persistencia local para controlar 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 y el cierre del proceso de la aplicación iniciados por el sistema, 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 de instancia guardado son soluciones de almacenamiento a largo plazo y, por lo tanto, no reemplazan el 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 app. Consulta la Guía de arquitectura de apps si deseas obtener más detalles para aprovechar el almacenamiento local a fin de 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 de la aplicación que no quieras perder si abres y cierras 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 la IU asociada y el estado de la IU de la pantalla.- Ejemplo: Las canciones de la búsqueda más reciente y la consulta de búsqueda más reciente
- Estado de instancia guardado: Almacena una pequeña cantidad de datos necesarios para volver a cargar fácilmente el estado de la IU si el sistema se detiene y vuelve a crear la IU. En lugar de almacenar objetos complejos aquí, consérvalos en un almacenamiento local y almacena un ID único para estos objetos en las APIs de estado de instancia de guardado.
- 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, el ViewModel
determina de inmediato que se conservarán esos datos a nivel local. Si se debe mostrar en la IU esta canción agregada recientemente, también debes actualizar los datos en el objeto ViewModel
para que reflejen que se agregó la canción. Recuerda que siempre que agregues algo a la base de datos, debes hacerlo fuera del subproceso principal.
Cuando el usuario busque una canción, sin importar la complejidad de los datos de canciones que cargues desde la base de datos, debe almacenarse inmediatamente en el objeto ViewModel
como parte del estado de la IU de la pantalla.
Cuando la actividad pasa a segundo plano y el sistema llama a las APIs de estado de instancia de guardado, la consulta de búsqueda debe almacenarse en ese estado en caso de que se vuelva a crear el proceso. Dado que la información es necesaria para cargar los datos de aplicación aquí conservados, almacena la búsqueda en el ViewModel SavedStateHandle
. Esta es toda la información que necesitas para cargar los datos y restablecer la IU 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. El sistema tiene la consulta guardada en un paquete de estado de instancia de guardado, y la IU debe pasar la consulta a
ViewModel
si no se usaSavedStateHandle
.ViewModel
ve que no tiene resultados de la búsqueda almacenados en caché y delega la carga de esos resultados con la búsqueda indicada. - La actividad se crea después de un cambio de configuración. Como la instancia
ViewModel
no se destruyó,ViewModel
tiene toda la información almacenada en caché y no necesita volver a consultar la base de datos.
Recursos adicionales
Si deseas obtener más información para guardar estados de la IU, consulta los siguientes recursos.
Blogs
- ViewModels: un ejemplo simple
- ViewModels: Persistence,
onSaveInstanceState()
, restauración del estado de IU y cargadores - Codelab de componentes que tienen en cuenta el ciclo de vida de Android
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Módulo de estado guardado para ViewModel
- Cómo manejar ciclos de vida con componentes optimizados para ciclos de vida
- Descripción general de ViewModel