Cómo guardar estados de la IU

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:

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
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 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 usa SavedStateHandle. 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