Google se compromete a impulsar la igualdad racial para las comunidades afrodescendientes. Obtén información al respecto.

Guarda estados de IU

Preservar y restablecer de manera oportuna el estado de la IU de una actividad 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 esos casos, el usuario espera que se conserve el estado de la IU, pero el sistema destruye la actividad y elimina 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 el almacenamiento local a fin de conservar 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, los casos de uso de la app y la velocidad de recuperación en relación con el uso de 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, 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 Recientes
  • navegar hacia arriba desde la actividad
  • eliminar la aplicación de la pantalla Configuración
  • completar algún tipo de acción de "finalización" (que está respaldada por 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 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 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 tú mismo.

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 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 Se aceptan objetos complejos, 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)

Usa 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 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 almacenar los identificadores de forma segura en savedInstanceState 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.

Usa onSaveInstanceState() como copia de seguridad para controlar 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. Si deseas obtener información para implementar el estado de instancia guardado, 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 guardada 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, una serialización de larga duración puede provocar una disminución de los marcos y saltos visuales.

No uses onSavedInstanceState() para almacenar grandes cantidades de datos, como mapas de bits, ni estructuras de datos complejas que requieran serialización o deserialización extensas. En su lugar, almacena solo tipos primitivos y objetos simples y pequeños, como String. Por lo tanto, usa onSaveInstanceState() a fin de almacenar una cantidad mínima y necesaria de datos, como un ID, de modo que vuelvas a crear los datos que se necesiten para restablecer la IU al estado anterior en caso de que los otros mecanismos de persistencia fallen. La mayoría de las apps deberían implementar onSaveInstanceState() para manejar el cierre del proceso iniciado por el sistema.

Según los casos de uso 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 esta manera, puedes no usar onSaveInstanceState() y, en su lugar, conservar todo a nivel local.

Además, cuando abres una actividad a partir de un intent, se entrega el paquete de elementos adicionales a la actividad, tanto cuando cambia la configuración como cuando el sistema restablece 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 onSaveInstanceState(). 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.

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

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 a fin de 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 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 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 que no quieres 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 la actividad si se detiene el sistema 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 esos 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, 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 todo lo que agregues a la base de datos debes hacerlo fuera del subproceso principal.

Cuando el usuario busque una canción, independientemente de su complejidad, los datos de canciones que cargues desde la base de datos para el controlador de IU se deben almacenar de inmediato en el objeto ViewModel. También debes guardar la búsqueda en el objeto ViewModel.

Cuando la actividad pasa a segundo plano, el sistema llama 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 guarda la búsqueda en un paquete onSaveInstanceState() y debe pasarla a ViewModel. El ViewModel ve que no tiene resultados de la búsqueda almacenados en caché y delega la carga de esos resultados mediante la búsqueda indicada.
  • La actividad se crea después de un cambio de configuración. La actividad tiene la búsqueda guardada en un paquete onSaveInstanceState(), y el ViewModel ya tiene los resultados de la búsqueda almacenados en caché. Pasa la búsqueda del paquete onSaveInstanceState() a ViewModel, que determina que ya cargó los datos necesarios y que no necesita volver a consultar la base de datos.

Recursos adicionales

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

Blogs