Cómo guardar el estado de la IU en Compose

Según hacia dónde se eleve el estado y la lógica requerida, puedes usar diferentes APIs para almacenar y restablecer el estado de tu IU. Cada app usa una combinación de APIs para lograrlo.

Cualquier app para Android podría perder su estado de IU debido a la actividad o la recreación del proceso. Esta pérdida de estado puede producirse debido a los siguientes eventos:

La preservación del estado después de estos eventos es esencial para que el usuario tenga una experiencia positiva. La selección del estado que se conservará depende de los flujos de usuarios únicos de tu app. Como práctica recomendada, como mínimo, debes conservar las entradas del usuario y el estado relacionado con la navegación. Algunos ejemplos pueden ser la posición de desplazamiento de una lista, el ID del elemento sobre el cual el usuario desea obtener más detalles, la selección de las preferencias del usuario en curso o la entrada en los campos de texto.

En esta página, se resumen las API disponibles para almacenar el estado de la IU según dónde se eleve el estado y la lógica que lo necesita.

Lógica de la IU

Si el estado se eleva en la IU, ya sea en funciones que admiten composición o en clases de contenedores de estados sin formato con alcance de Composition, puedes usar rememberSaveable para retener el estado en toda la actividad y el proceso de recreación.

En el siguiente fragmento, se usa rememberSaveable para almacenar un solo estado de elemento de IU booleano:

@Composable
fun ChatBubble(
    message: Message
) {
    var showDetails by rememberSaveable { mutableStateOf(false) }

    ClickableText(
        text = AnnotatedString(message.content),
        onClick = { showDetails = !showDetails }
    )

    if (showDetails) {
        Text(message.timestamp)
    }
}

Figura 1: La burbuja de mensaje de chat se expande y se contrae cuando se presiona.

showDetails es una variable booleana que almacena si la burbuja de chat se contrae o se expande.

rememberSaveable almacena el estado del elemento de la IU en un objeto Bundle a través del mecanismo de estado de la instancia guardada.

Puede almacenar tipos primitivos automáticamente en el paquete. Si tu estado se mantiene en un tipo que no es primitivo, como una clase de datos, puedes usar diferentes mecanismos de almacenamiento, como el uso de Parcelize con las API de Compose, como listSaver y mapSaver o implementar una clase Saver personalizada que extienda el entorno de ejecución de Compose Saver. Consulta la documentación sobre formas de almacenar el estado para obtener más información sobre estos métodos.

En el siguiente fragmento, la API de Compose rememberLazyListState almacena LazyListState, que consiste en el estado de desplazamiento de un LazyColumn o LazyRow, con rememberSaveable. Usa un objeto LazyListState.Saver, que es un ahorro personalizado que puede almacenar y restablecer el estado de desplazamiento. Después de una actividad o una recreación del proceso (por ejemplo, después de un cambio de configuración, como cuando se cambia la orientación del dispositivo), se conserva el estado de desplazamiento.

@Composable
fun rememberLazyListState(
    initialFirstVisibleItemIndex: Int = 0,
    initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
    return rememberSaveable(saver = LazyListState.Saver) {
        LazyListState(
            initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset
        )
    }
}

Práctica recomendada

rememberSaveable usa un objeto Bundle para almacenar el estado de la IU, que se comparte con otras API que también escriben en él, como las llamadas onSaveInstanceState() en tu actividad. Sin embargo, el tamaño de este Bundle es limitado, y el almacenamiento de objetos grandes podría generar excepciones TransactionTooLarge en el entorno de ejecución. Esto puede ser particularmente problemático en una sola app de Activity en la que se usa la misma Bundle en toda la app.

Para evitar este tipo de falla, no debes almacenar objetos complejos grandes ni listas de objetos en el paquete.

En cambio, almacena el estado mínimo requerido, como los ID o las claves, y úsalo para delegar el restablecimiento de estados de IU más complejos a otros mecanismos, como el almacenamiento continuo.

Estas opciones de diseño dependen de los casos prácticos específicos de tu app y cómo se comportan tus usuarios.

Cómo verificar el restablecimiento del estado

Puedes verificar que el estado almacenado con rememberSaveable en tus elementos de Compose se restablezca correctamente cuando se vuelva a crear la actividad o el proceso. Hay APIs específicas para lograrlo, como StateRestorationTester. Consulta la documentación para obtener más información.

Lógica empresarial

Si se eleva el estado de tu elemento de IU a ViewModel porque la lógica empresarial lo requiere, puedes usar las APIs de ViewModel.

Uno de los principales beneficios de usar ViewModel en tu aplicación para Android es que controla los cambios de configuración sin costo alguno. Cuando hay un cambio de configuración y la actividad se destruye y se vuelve a crear, el estado de la IU elevado a ViewModel se conserva en la memoria. Después de la recreación, la instancia ViewModel anterior se adjunta a la instancia de la actividad nueva.

Sin embargo, una instancia ViewModel no sobrevive al cierre del proceso iniciado por el sistema. Para que el estado de la IU sobreviva, usa el módulo de estado guardado para ViewModel, que contiene la API de SavedStateHandle.

Práctica recomendada

SavedStateHandle también usa el mecanismo Bundle para almacenar el estado de la IU, por lo que solo debes usarlo para almacenar el estado de elementos de la IU simple.

El estado de la IU de la pantalla, que se produce con la aplicación de reglas empresariales y el acceso a capas de la aplicación que no sean de la IU, no se deben almacenar en SavedStateHandle debido a su potencial complejidad y tamaño de Google Analytics. Puedes usar diferentes mecanismos para almacenar datos complejos o grandes, como el almacenamiento local persistente. Después de un proceso de recreación, la pantalla se vuelve a crear con el estado transitorio que se almacenó en SavedStateHandle (si existe), y el estado de la IU de la pantalla se vuelve a producir desde la capa de datos.

APIs de SavedStateHandle

SavedStateHandle tiene diferentes APIs para almacenar el estado del elemento de la IU, en particular los siguientes:

Compose State saveable()
StateFlow getStateFlow()

Compose State

Usa la API de saveable de SavedStateHandle para leer y escribir el estado del elemento de la IU como MutableState, para que sobreviva a la actividad y a la recreación del proceso con la configuración de código mínima.

La API de saveable admite tipos primitivos listos para usar y recibe un parámetro stateSaver para usar ahorros personalizados, como rememberSaveable().

En el siguiente fragmento, message almacena los tipos de entrada del usuario en un TextField:

class ConversationViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) {
        mutableStateOf(TextFieldValue(""))
    }
        private set

    fun update(newMessage: TextFieldValue) {
        message = newMessage
    }

    /*...*/
}

val viewModel = ConversationViewModel(SavedStateHandle())

@Composable
fun UserInput(/*...*/) {
    TextField(
        value = viewModel.message,
        onValueChange = { viewModel.update(it) }
    )
}

Consulta la documentación de SavedStateHandle para obtener más información sobre el uso de la API de saveable.

StateFlow

Usa getStateFlow() para almacenar el estado del elemento de la IU y consumirlo como un flujo desde SavedStateHandle. El elemento StateFlow es de solo lectura, y la API requiere que especifiques una clave para que puedas reemplazar el flujo para emitir un valor nuevo. Con la clave que configuraste, puedes recuperar el objeto StateFlow y recopilar el valor más reciente.

En el siguiente fragmento, savedFilterType es una variable StateFlow que almacena un tipo de filtro aplicado a una lista de canales de chat en una app de chat:

private const val CHANNEL_FILTER_SAVED_STATE_KEY = "ChannelFilterKey"

class ChannelViewModel(
    channelsRepository: ChannelsRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val savedFilterType: StateFlow<ChannelsFilterType> = savedStateHandle.getStateFlow(
        key = CHANNEL_FILTER_SAVED_STATE_KEY, initialValue = ChannelsFilterType.ALL_CHANNELS
    )

    private val filteredChannels: Flow<List<Channel>> =
        combine(channelsRepository.getAll(), savedFilterType) { channels, type ->
            filter(channels, type)
        }.onStart { emit(emptyList()) }

    fun setFiltering(requestType: ChannelsFilterType) {
        savedStateHandle[CHANNEL_FILTER_SAVED_STATE_KEY] = requestType
    }

    /*...*/
}

enum class ChannelsFilterType {
    ALL_CHANNELS, RECENT_CHANNELS, ARCHIVED_CHANNELS
}

Cada vez que el usuario selecciona un nuevo tipo de filtro, se llama a setFiltering. Esto guarda un valor nuevo en SavedStateHandle almacenado con la clave _CHANNEL_FILTER_SAVED_STATE_KEY_. savedFilterType es un flujo que emite el valor más reciente almacenado en la clave. filteredChannels está suscrito al flujo para realizar el filtrado del canal.

Consulta la documentación de SavedStateHandle para obtener más información sobre la API de getStateFlow().

Resumen

En la siguiente tabla, se resumen las API que se analizan en esta sección y cuándo debes usar cada una para guardar el estado de la IU:

Evento Lógica de la IU Lógica empresarial en un objeto ViewModel
Cambios de configuración rememberSaveable Automático
Cierre del proceso iniciado por el sistema rememberSaveable SavedStateHandle

La API que se debe usar depende de la ubicación del estado y de la lógica que requiere. Para el estado que se usa en la lógica de la IU, usa rememberSaveable. Para el estado que se usa en la lógica empresarial, si lo mantienes en un ViewModel, guárdalo con SavedStateHandle.

Debes usar las APIs de los paquetes (rememberSaveable y SavedStateHandle) para almacenar pequeñas cantidades de estado de la IU. Estos datos son el mínimo necesario para restablecer la IU a su estado anterior, junto con otros mecanismos de almacenamiento. Por ejemplo, si almacenas el ID de un perfil que el usuario estaba viendo en el paquete, puedes recuperar datos pesados, como los detalles del perfil, desde la capa de datos.

Si deseas obtener más información sobre las diferentes maneras de guardar el estado de la IU, consulta la documentación general sobre guardado del estado de la IU y la página de capa de datos de la guía de arquitectura.