Este guia discute as expectativas do usuário sobre o estado da interface e as opções disponíveis para preservá-lo.
Salvar e restaurar o estado da interface rapidamente após o sistema destruir a atividade host ou o processo do aplicativo é essencial para uma boa experiência do usuário. Os usuários esperam que o estado da interface permaneça o mesmo, mas o sistema pode destruir a atividade que hospeda a tela e o estado armazenado nela.
Para resolver a diferença entre as expectativas do usuário e o comportamento do sistema, use uma combinação destes métodos:
- Objetos
ViewModel. - Estado salvo nos seguintes contextos:
- Elementos combináveis:
rememberSerializableerememberSaveable. - ViewModels:
SavedStateHandle.
- Elementos combináveis:
- Armazenamento local para manter o estado da interface durante transições de apps e telas.
A solução ideal depende da complexidade dos dados da interface, dos casos de uso do app e de encontrar o equilíbrio entre a velocidade do acesso aos dados e o uso de memória.
Confira se o app atende às expectativas dos usuários e oferece uma interface rápida e responsiva. Evite atrasos ao carregar dados na interface, principalmente depois de mudanças comuns na configuração, como a rotação.
Expectativas do usuário e comportamento do sistema
Dependendo da ação de um usuário, ele espera que o estado da interface seja apagado ou preservado. Em alguns casos, o sistema faz o que o usuário espera automaticamente. Em outros casos, o sistema faz o oposto.
Dispensa do estado da interface iniciada pelo usuário
O usuário espera que, ao navegar até uma tela, o estado transitório da interface permaneça o mesmo até que ele dispense totalmente a tela. Confira como um usuário pode dispensar totalmente uma tela ou um app:
- Deslizando o app para fora da tela "Visão geral" (Recentes).
- Encerrando ou forçando o encerramento do app na tela Configurações.
- Reinicializando o dispositivo.
- Concluindo algum tipo de ação "de conclusão" (com suporte de
Activity.finish()).
Nesses casos, o usuário espera que ele tenha saído permanentemente da tela e, se voltar, a expectativa é que ela seja iniciada em um estado limpo. O comportamento subjacente do sistema para esses cenários de dispensa corresponde à expectativa do usuário: a instância da atividade de host será destruída e removida da memória, juntamente com qualquer estado armazenado nela e qualquer registro de estado salvo associado a ela.
Há algumas exceções a essa regra sobre a dispensa total. Por exemplo, um usuário pode esperar que o botão "Voltar" o leve de volta à página exata que estava visualizando antes de sair do navegador.
Dispensa do estado da IU iniciada pelo sistema
O usuário espera que o estado da interface de uma tela permaneça o mesmo durante uma mudança de configuração, como girar ou mudar o dispositivo para o modo de várias janelas. No entanto, por padrão, o sistema destrói a atividade host quando ocorre uma mudança de configuração, excluindo permanentemente qualquer estado da interface armazenado nela. Para saber mais sobre as configurações do dispositivo, consulte Reagir a mudanças de configuração no Jetpack Compose.
É possível substituir o comportamento padrão para mudanças de configuração, mas isso não é recomendado. Consulte Como processar as mudanças de configuração para saber mais.
O usuário também espera que o estado da interface do app permaneça o mesmo caso ele acesse outro app temporariamente e depois volte para o seu. Por exemplo, o usuário faz uma pesquisa em uma tela e depois pressiona o botão home ou atende a uma ligação. Quando ele retorna à tela de pesquisa, espera encontrar a palavra-chave da rede de pesquisa e os resultados ali, exatamente como antes.
Nesse cenário, o app é colocado em segundo plano, e o sistema faz o melhor possível para manter o processo do app na memória. No entanto, o sistema pode destruir o processo do aplicativo enquanto o usuário está interagindo com outros apps. Nesse caso, a atividade host é destruída, bem como qualquer estado armazenado nela. Quando o usuário reinicia o app, a tela está inesperadamente em um estado limpo. Para saber mais sobre o encerramento do processo, consulte Processos e ciclo de vida do app.
Opções para preservar o estado da interface
Quando as expectativas do usuário sobre o estado da interface forem diferentes do comportamento padrão do sistema, salve e restaure o estado da interface do usuário para garantir que a destruição iniciada pelo sistema seja transparente.
Cada uma das opções para preservar o estado da interface varia de acordo com as seguintes dimensões que afetam a experiência do usuário:
| ViewModel | Estado salvo | Armazenamento permanente | |
|---|---|---|---|
| Local de armazenamento | Na memória | Na memória | No disco ou na rede |
| Sobrevive à mudança da configuração | Sim | Sim | Sim |
| Sobrevive ao encerramento do processo iniciado pelo sistema | Não | Sim | Sim |
Sobrevive quando o usuário dispensa completamente a tela/finish() |
Não | Não | Sim |
| Limitações de dados | Objetos complexos funcionam bem, mas o espaço é limitado pela memória disponível | Somente para tipos primitivos e pequenos objetos simples, como String |
Limitado apenas pelo espaço em disco ou custo / tempo de extração do recurso de rede |
| Tempo de leitura/gravação | Rápido (somente acesso à memória) | Lento (exige serialização/desserialização) | Lento (requer acesso ao disco ou transação de rede) |
Usar ViewModel para lidar com mudanças de configuração
O ViewModel é ideal para armazenar e gerenciar dados relacionados à interface enquanto o usuário está ativo no aplicativo. Ele permite acesso rápido aos dados da interface e evita uma nova busca de dados da rede ou do disco durante a rotação, o redimensionamento de janela e outras mudanças de configuração que ocorrem com frequência. Para aprender a implementar um ViewModel, consulte o Guia do ViewModel.
O ViewModel retém os dados na memória, o que significa que é mais barato extrair do que usar os dados do disco ou da rede. Um ViewModel está associado a um proprietário de ciclo de vida, como um destino de navegação ou uma atividade. Ele permanece na memória durante uma mudança de configuração, e o sistema associa automaticamente o ViewModel à nova instância de proprietário do ciclo de vida resultante da mudança de configuração.
Ao contrário do estado salvo, os ViewModels são destruídos durante o encerramento do processo iniciado pelo sistema. Para recarregar dados após um encerramento do processo iniciado pelo sistema em um ViewModel, use
a API SavedStateHandle. Como alternativa, se os dados estiverem relacionados à interface
e não precisarem ser mantidos no ViewModel, use rememberSerializable. Para tipos de dados primitivos ou cenários em que você não quer usar @Serializable, use rememberSaveable. Se eles forem dados do aplicativo, pode ser
melhor armazená-los no disco.
Se você já tiver uma solução na memória para armazenar o estado da interface nas mudanças de configuração, talvez não seja necessário usar o ViewModel.
Usar o estado salvo como backup para lidar com o encerramento do processo iniciado pelo sistema
APIs como rememberSerializable e rememberSaveable no Compose e
SavedStateHandle nos ViewModels armazenam os dados necessários para recarregar o estado da interface
se o sistema destruir e depois recriar um componente. Para processar estruturas de dados complexas com mais eficiência, o SavedStateHandle oferece suporte à serialização Kotlinx pela extensão saved {}, permitindo persistir e restaurar objetos com segurança de tipos sem problemas, além de tipos primitivos padrão. Para saber como
implementar o estado salvo usando rememberSaveable, consulte Estado e Jetpack
Compose.
Os pacotes de estado salvo são mantidos após mudanças de configuração e encerramentos de processo, mas são limitados pela quantidade de armazenamento e velocidade, já que as diferentes APIs serializam dados. A serialização poderá consumir muita memória se os objetos que estão sendo serializados forem complexos. Como esse processo acontece na linha de execução principal durante uma mudança de configuração, a serialização longa pode causar perda de frames e falhas visuais.
Não use o estado salvo para armazenar grandes volumes de dados, como bitmaps, nem estruturas de dados complexas que exigem serialização ou desserialização demorada. Em vez disso, armazene apenas tipos primitivos e objetos pequenos e simples,
como String. Use o estado salvo para armazenar uma quantidade mínima de dados necessários, como um ID, para recriar os dados necessários para restaurar a interface ao estado anterior caso os outros mecanismos de persistência falhem. A maioria
dos apps precisa implementar esses mecanismos para processar o encerramento de processo iniciado pelo sistema.
Dependendo dos casos de uso do seu app, talvez não seja necessário usar o estado salvo. Por exemplo, um navegador pode levar o usuário de volta à página da Web exata em que ele estava antes de sair. Caso sua atividade se comporte dessa maneira, você pode abandonar o uso do estado salvo e manter tudo localmente.
Além disso, ao abrir uma atividade proveniente de uma intent, o pacote de extras é entregue à atividade quando a configuração é mudada e quando o sistema restaura a atividade. Se dados de estado da interface, como uma consulta de pesquisa, tiverem sido transmitidos como um extra da intent quando a atividade foi iniciada, será possível usar o pacote de extras em vez do pacote de estado salvo. Para saber mais sobre os extras de intents, consulte Intents e filtros de intents.
Em qualquer um desses cenários, você ainda precisará usar um ViewModel para evitar
ciclos desnecessários de recarregamento do banco de dados durante uma mudança de configuração.
Nos casos em que os dados da interface a serem preservados são simples e leves, você pode usar APIs de estado salvo sozinhas para preservar seus dados de estado.
Conectar-se ao estado salvo usando SavedStateRegistry
A partir do Fragment 1.1.0 ou da dependência transitiva dele, Activity
1.0.0, os componentes de interface, como ComponentActivity, implementam
SavedStateRegistryOwner e fornecem um SavedStateRegistry que está
vinculado ao componente. SavedStateRegistry permite que os componentes se conectem ao
estado salvo para consumi-lo ou contribuir com ele. Por exemplo,
o módulo Saved State para ViewModel usa SavedStateRegistry para criar um
SavedStateHandle e fornecê-lo aos objetos ViewModel. Para recuperar
o SavedStateRegistry no proprietário do ciclo de vida, chame
savedStateRegistry.
Os componentes que contribuem para o estado salvo precisam implementar
SavedStateRegistry.SavedStateProvider, que define um único método
denominado saveState(). O método saveState() permite que seu componente
retorne um Bundle contendo qualquer estado desse componente que precisa ser salvo.
SavedStateRegistry chama esse método durante a fase de salvamento do
ciclo de vida do proprietário do ciclo de vida.
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)
}
}
Para registrar um SavedStateProvider, chame registerSavedStateProvider() no
SavedStateRegistry, transmitindo uma chave que se associe ao provedor e
aos dados dele. Os dados salvos anteriormente para o provedor podem
ser extraídos do estado salvo chamando consumeRestoredStateForKey()
no SavedStateRegistry, transmitindo a chave associada aos dados do
provedor.
Em uma ComponentActivity, você pode registrar um SavedStateProvider em
onCreate() depois de chamar super.onCreate(). Como alternativa, você poderá definir um
LifecycleObserver em um SavedStateRegistryOwner, que implementa
LifecycleOwner, e registrar o SavedStateProvider assim que o
evento ON_CREATE ocorrer. Ao usar um LifecycleObserver, é possível desacoplar o
registro e a extração do estado salvo anteriormente do
próprio SavedStateRegistryOwner.
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 SearchActivity : ComponentActivity() {
private var searchManager = SearchManager(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Set up your Compose UI here
setContent {
// ...
}
}
}
Usar a persistência local para lidar com o encerramento do processo para dados complexos ou grandes
O armazenamento local persistente, como um banco de dados ou DataStore, vai sobreviver enquanto seu aplicativo estiver instalado no dispositivo do usuário (a menos que o usuário limpe os dados do app). Esse armazenamento local sobrevive ao encerramento do processo do aplicativo iniciado pelo sistema, mas a recuperação dos dados pode ser cara, já que a leitura vai ocorrer do armazenamento local para a memória. Muitas vezes, esse armazenamento local persistente já pode fazer parte da arquitetura do aplicativo para que todos os dados que você não quer perder ao abrir e fechar o app sejam armazenados.
O ViewModel e o estado salvo usando rememberSerializable,
rememberSaveable ou SavedStateHandle não são soluções de armazenamento de longo prazo e,
assim, não substituem o armazenamento local, como um banco de dados. Em vez disso, você
precisa usar esses mecanismos para armazenar temporariamente somente o estado transitório da interface e
usar armazenamento persistente para outros dados do app. Consulte o Guia da arquitetura do app
para saber mais sobre como aproveitar o armazenamento local para manter os dados do modelo de app
a longo prazo, como nas reinicializações do dispositivo.
Como gerenciar o estado da interface: dividir para ter êxito
É possível salvar e restaurar o estado da interface de forma eficiente dividindo o trabalho entre os vários tipos de mecanismo de persistência. Na maioria dos casos, cada um desses mecanismos precisa armazenar um tipo diferente de dados usados no app, com base nas compensações de complexidade de dados, velocidade de acesso e ciclo de vida:
- Persistência local: armazena todos os dados do aplicativo que você não quer perder ao
abrir e fechar o app.
- Exemplo: uma coleção de objetos de música, que pode incluir arquivos de áudio e metadados.
ViewModel: armazena na memória todos os dados necessários para mostrar a interface associada, o estado da interface da tela.- Exemplo: os objetos de música da pesquisa mais recente e a consulta de pesquisa mais recente.
- Estado salvo (
rememberSerializable,rememberSaveableeSavedStateHandle): armazena um pequeno volume de dados necessários para recarregar o estado da interface caso o sistema pare e, em seguida, recrie a interface. Em vez de armazenar objetos complexos aqui, persista os objetos complexos no armazenamento local e armazene um ID exclusivo para esses objetos nas APIs de estado salvo.- Exemplo: armazenamento da consulta de pesquisa mais recente.
Por exemplo, considere um app que permite pesquisar na sua biblioteca de músicas. Veja como diferentes eventos devem ser tratados:
Quando o usuário adiciona uma música, o ViewModel delega imediatamente a persistência
desses dados no local. Se essa música recém-adicionada for algo que precisa ser mostrado na interface, também
será necessário atualizar os dados no objeto ViewModel para refletir a
adição. Não se esqueça de realizar todas as inserções do banco de dados fora da linha de execução principal.
Quando o usuário pesquisa uma música, todos os dados de música complexos carregados do
banco de dados precisam ser armazenados imediatamente no objeto ViewModel como parte
do estado da interface da tela.
Quando o app entra em segundo plano e o sistema salva o estado,
a consulta de pesquisa precisa ser armazenada usando APIs de estado salvo,
caso o processo seja recriado. Como as informações são necessárias para carregar os dados de aplicativo mantidos, armazene a consulta de pesquisa no SavedStateHandle do ViewModel ou use rememberSerializable ou rememberSaveable nos combináveis. Essas são todas as informações necessárias para carregar os dados e
fazer a interface voltar ao estado atual.
Restaurar estados complexos: como juntar as peças
Quando chega o momento do usuário retornar ao app, há dois cenários possíveis para recriar a interface:
- A interface é recriada depois que o sistema encerra o processo do aplicativo. O
sistema salva a consulta usando APIs de estado salvo. O
ViewModel(usandoSavedStateHandle) ou o combinável (usandorememberSerializableourememberSaveable) restaura automaticamente a consulta. Se o elemento combinável restaurar a consulta, ele vai transmiti-la aoViewModel. OViewModelverifica que não há resultados da pesquisa armazenados em cache e delega o carregamento dos resultados usando a consulta de pesquisa fornecida. - A interface é recriada após uma mudança de configuração. Como a instância
ViewModelnão foi destruída, oViewModeltem todas as informações armazenadas em cache na memória e não precisa consultar novamente o banco de dados.
Outros recursos
Para saber mais sobre como salvar estados da interface, consulte estes recursos.
- Documentação da camada de interface
- Como salvar o estado da interface no Compose
- Documentação do estado e do Jetpack Compose
Codelabs
Visualiza conteúdo
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Módulo Saved State para ViewModel
- Como gerenciar ciclos de vida com componentes que os reconhecem
- Visão geral do ViewModel