Salvar o estado da interface no Compose

Dependendo de onde o estado é elevado e da lógica necessária, é possível usar diferentes APIs para armazenar e restaurar o estado da interface. Cada app usa uma combinação de APIs para fazer isso da melhor forma.

Qualquer app Android pode perder o estado da interface devido à recriação da atividade ou do processo. Os eventos abaixo podem causar essa perda:

A preservação do estado após esses eventos é essencial para uma experiência positiva do usuário. A seleção do estado a ser preservado depende dos fluxos de usuários únicos do app. Como prática recomendada, preserve pelo menos o estado da entrada e da navegação do usuário. Isso inclui a posição de rolagem de uma lista, o código do item sobre o qual o usuário quer mais detalhes, a seleção em andamento de preferências do usuário ou a entrada em campos de texto.

Esta página resume as APIs disponíveis para armazenar o estado da interface, dependendo de onde ele é elevado e da lógica que precisa dele.

Lógica da interface

Se o estado for elevado na interface, seja em funções combináveis ou em classes de detentores de estado simples com escopo para a composição, você vai poder usar rememberSaveable para reter o estado na recriação de processos e atividades.

No snippet abaixo, o rememberSaveable é usado para armazenar um único estado de elemento da interface 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. O balão da mensagem de chat é aberto e recolhido quando um usuario toca nele.

showDetails é uma variável booleana que é armazenada quando o balão de diálogo é recolhido ou aberto.

O rememberSaveable armazena o estado do elemento da interface em um Bundle usando o mecanismo de estado salvo da instância.

Ele pode armazenar tipos primitivos no pacote automaticamente. Se o estado for preservado em um tipo não primitivo, como uma classe de dados, vai ser possível usar diferentes mecanismos de armazenamento, como a anotação Parcelize, usando as APIs do Compose, como listSaver e mapSaver, ou implementar uma classe de armazenamento personalizada estendendo a classe Saver de execução do Compose. Consulte a documentação Formas de armazenar o estado para saber mais sobre esses métodos.

No snippet abaixo, a API rememberLazyListState do Compose armazena LazyListState, que consiste no estado de rolagem de uma LazyColumn ou LazyRow, usando rememberSaveable. Ele usa um LazyListState.Saver, que é um armazenamento personalizado que pode manter e restaurar o estado de rolagem. Esse estado é preservado após uma recriação de processo ou atividade, por exemplo, depois de uma mudança de configuração, como modificar a orientação do dispositivo.

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

Prática recomendada

O rememberSaveable usa um Bundle para armazenar o estado da interface, que é compartilhado por outras APIs que também gravam nele, como chamadas onSaveInstanceState() na atividade. No entanto, o tamanho desse Bundle é limitado, e o armazenamento de objetos grandes pode levar a exceções TransactionTooLarge durante a execução. Isso pode ser especialmente problemático em apps com uma única Activity em que o mesmo Bundle é usado em todo o app.

Para evitar esse tipo de falha, não armazene objetos grandes e complexos ou listas de objetos no pacote.

Em vez disso, armazene o estado mínimo necessário, como IDs ou chaves, e use-o para delegar a restauração do estado da interface mais complexo a outros mecanismos, como o armazenamento permanente.

Essas escolhas de design dependem dos casos de uso específicos do app e de como os usuários esperam que ele se comporte.

Verificar a restauração do estado

É possível verificar se o estado armazenado com rememberSaveable nos elementos do Compose é restaurado corretamente quando a atividade ou o processo é recriado. Há APIs específicas para isso, como StateRestorationTester. Confira a documentação sobre Testes para saber mais.

Lógica de negócios

Se o estado do elemento da interface for elevado ao ViewModel por ser necessário para a lógica de negócios, use as APIs do ViewModel.

Um dos principais benefícios de usar um ViewModel no app Android é que ele processa as mudanças de configuração sem custos. Quando há uma mudança de configuração e a atividade é destruída e recriada, o estado da interface elevado ao ViewModel é mantido na memória. Após a recriação, a antiga instância de ViewModel é anexada à nova instância da atividade.

No entanto, uma instância de ViewModel não sobrevive ao encerramento do processo iniciado pelo sistema. Para que o estado da interface sobreviva a esse encerramento, use o módulo Saved State para o ViewModel, que contém a API SavedStateHandle.

Prática recomendada

O SavedStateHandle também usa o mecanismo Bundle para armazenar o estado da interface. Use-o apenas para armazenar um estado do elemento da interface simples.

O estado da interface da tela, que é produzido com a aplicação de regras de negócios e o acesso a camadas do app diferentes da interface, não pode ser armazenado em SavedStateHandle devido à possível complexidade e tamanho do app. Use mecanismos diferentes para armazenar dados complexos ou grandes, como o armazenamento permanente local. Após a recriação de um processo, a tela é recriada com o estado transitório restaurado que foi armazenado no SavedStateHandle (se houver), e o estado da interface da tela é produzido de novo pela camada de dados.

APIs SavedStateHandle.

O SavedStateHandle tem diferentes APIs para armazenar o estado do elemento da interface. Estas são as principais:

State do Compose saveable()
StateFlow getStateFlow()

State do Compose

Use a API saveable do SavedStateHandle para ler e gravar o estado do elemento da interface como MutableState. Assim, ele sobrevive à recriação de atividades e processos com uma configuração mínima de código.

A API saveable oferece suporte a tipos primitivos e recebe um parâmetro stateSaver para usar armazenamentos personalizados, assim como rememberSaveable().

No snippet abaixo, message armazena os tipos de entrada do usuário em um 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) }
    )
}

Consulte a documentação do SavedStateHandle para mais informações sobre como usar a API saveable.

StateFlow

Use getStateFlow() para armazenar o estado do elemento da interface e consumi-lo como um fluxo do SavedStateHandle. O StateFlow (link em inglês) é somente leitura, e a API exige que você especifique uma chave para substituir o fluxo e emitir um novo valor. Com a chave configurada, é possível extrair o StateFlow e coletar o valor mais recente.

No snippet abaixo, savedFilterType é uma variável de StateFlow que armazena um tipo de filtro aplicado a uma lista de canais em um 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
}

Sempre que o usuário seleciona um novo tipo de filtro, setFiltering é chamado. Isso salva um novo valor no arquivo SavedStateHandle armazenado com a chave. _CHANNEL_FILTER_SAVED_STATE_KEY_. savedFilterType é um fluxo que emite o valor mais recente armazenado na chave. filteredChannels fica inscrito no fluxo para realizar a filtragem do canal.

Consulte a documentação SavedStateHandle para mais informações sobre a API getStateFlow().

Resumo

A tabela abaixo resume as APIs abordadas nesta seção e quando usar cada uma para salvar o estado da interface:

Evento Lógica da interface Lógica de negócios em um ViewModel
Mudanças de configuração rememberSaveable Automática
Encerramento do processo iniciado pelo sistema rememberSaveable SavedStateHandle

A API a ser usada depende de onde o estado é mantido e da lógica que ele exige. Para o estado usado na lógica da interface, use rememberSaveable. Para o estado usado na lógica de negócios, se ele for preservado em um ViewModel, salve-o usando SavedStateHandle.

Use as APIs de pacote (rememberSaveable e SavedStateHandle) para armazenar pequenas quantidades de estado da interface. Esses dados são o mínimo necessário para restaurar a interface ao estado anterior com outros mecanismos de armazenamento. Por exemplo, se você armazenar o ID de um perfil que o usuário estava visualizando no pacote, poderá buscar dados pesados, como detalhes de perfil, da camada de dados.

Para mais informações sobre as diferentes maneiras de salvar o estado da interface, consulte a documentação geral sobre Salvar estados da interface e a página do guia de arquitetura sobre a camada de dados.