Este guia discute as expectativas do usuário sobre o estado da interface e as opções disponíveis para preservá-lo.
É essencial salvar e restaurar o estado da interface de uma atividade rapidamente após o sistema destruir atividades ou aplicativos, já que isso proporciona 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 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
. - Estados de instância salvos nos seguintes contextos:
- Jetpack Compose:
rememberSaveable
. - Visualizações: API
onSaveInstanceState()
. - ViewModels:
SavedStateHandle
.
- Jetpack Compose:
- Armazenamento local para manter o estado da interface durante transições de apps e atividades.
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 atividade seja apagado ou preservado. Em alguns casos, o sistema faz o que é esperado pelo usuário automaticamente. Em outros, o sistema faz o oposto do que o usuário espera.
Dispensa do estado da interface iniciada pelo usuário
O usuário espera que, ao iniciar uma atividade, o estado transitório da interface permaneça o mesmo até que ele dispense totalmente a atividade. Confira como um usuário pode dispensar totalmente uma atividade:
- Deslizando a atividade 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 atividade e, se reabri-la, a expectativa é que ela seja iniciada em um estado limpo. O comportamento do sistema para esses cenários de dispensa é o que o usuário espera: a instância da atividade será destruída e removida da memória, bem como qualquer estado armazenado nela e qualquer registro de estado salvo associado à atividade.
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 interface iniciada pelo sistema
O usuário espera que o estado da interface de uma atividade 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 quando ocorre essa mudança na configuração, excluindo permanentemente qualquer estado da interface armazenado na instância da atividade. Para saber mais sobre as configurações do dispositivo, consulte a página de referência de configuração. É 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 por conta própria para saber mais.
O usuário também espera que o estado da interface da atividade permaneça o mesmo caso ele acesse outro app temporariamente e depois volte para o seu. Por exemplo, o usuário faz uma pesquisa na sua atividade de pesquisa e depois pressiona o botão home ou atende a uma ligação. Quando ele retorna à atividade de pesquisa, espera encontrar a palavra-chave e os resultados ali, exatamente como antes.
Nesse cenário, o app é colocado em segundo plano para que o sistema faça o melhor possível para manter o processo na memória. No entanto, o sistema pode destruir o processo do aplicativo enquanto o usuário interage com outros apps. Nesse caso, a instância de atividade é destruída, bem como qualquer estado armazenado nela. Quando o usuário reinicia o app, a atividade está inesperadamente em um estado limpo. Para saber mais sobre o encerramento de processos, 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 de instância salvo | Armazenamento persistente | |
---|---|---|---|
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 atividade/onFinish() | 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 uma atividade ou outro proprietário do ciclo de vida. Ele permanece na memória durante uma mudança de configuração, e o sistema o associa automaticamente à nova instância de atividade resultante da mudança.
Os ViewModels são destruídos automaticamente pelo sistema quando o usuário sai
da sua atividade ou do seu fragmento, ou ainda se você chama finish()
, o que significa que o estado
é apagado conforme o usuário espera nessas situações.
Ao contrário do estado de instância salvo, os ViewModels são destruídos durante um encerramento de processo
iniciado pelo sistema. Para recarregar dados após um encerramento de processo iniciada 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
onSaveInstanceState()
no sistema de visualização ou rememberSaveable
no Jetpack
Compose. 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 de instância salvo como backup para processar encerramentos de processo iniciados pelo sistema
O callback onSaveInstanceState()
no sistema de visualização,
rememberSaveable
no Jetpack Compose e SavedStateHandle
nos
ViewModels armazenam os dados necessários para recarregar o estado de um controlador de interface, como uma
atividade ou um fragmento, quando o sistema destrói e recria esse
controlador. Para aprender a implementar o estado de instância salvo usando
onSaveInstanceState
, consulte Como salvar e restaurar o estado da atividade no
Guia do ciclo de vida da atividade.
Os pacotes de estado de instância 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 de instância 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 de instância 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 de instância 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 de instância 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 do estado de instância 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 do estado de instância 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 controladores de interface, como Activity
ou Fragment
, implementam
SavedStateRegistryOwner
e fornecem um SavedStateRegistry
que está
vinculado ao controlador. SavedStateRegistry
permite que os componentes se conectem ao
estado salvo do controlador de interface 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 controlador de interface, chame
getSavedStateRegistry()
.
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
controlador de interface.
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 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 Activity
ou um Fragment
, 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
.
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); ... }
Usar a persistência local para processar encerramentos de processo para dados complexos ou grandes
O armazenamento local persistente, como um banco de dados ou preferências compartilhadas, 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 à atividade iniciada pelo sistema e ao encerramento do processo do aplicativo, 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 a atividade sejam armazenados.
O ViewModel e o estado de instância salvo 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 apenas estados transitórios da interface temporariamente 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 na atividade, com base nas compensações de complexidade de dados, na velocidade de acesso e no ciclo de vida:
- Persistência local: armazena todos os dados do aplicativo que você não quer perder ao
abrir e fechar a atividade.
- 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 de instância salvo: armazena um pequeno volume de dados necessários para recarregar
o estado da interface caso o sistema pare e, em seguida, recria a interface. Em vez de armazenar
objetos complexos aqui, mantenha os objetos complexos no armazenamento local e aloque
um ID exclusivo para esses objetos nas APIs de estado de instância salvo.
- Exemplo: armazenamento da consulta de pesquisa mais recente.
Por exemplo, considere uma atividade que permite pesquisar na sua biblioteca de músicas. Confira como diferentes eventos precisam ser processados:
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 a atividade entra em segundo plano e o sistema chama as APIs de estado
de instância salvo, a consulta de pesquisa precisa ser armazenada no estado de instância 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. 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 à atividade, há dois cenários possíveis para recriá-la:
- A atividade é recriada após ter sido interrompida pelo sistema. O
sistema salva a consulta em um pacote de estados de instância armazenado, e a interface
precisa transmitir a consulta para o
ViewModel
se oSavedStateHandle
não for usado. OViewModel
verifica que não há resultados da pesquisa armazenados em cache e delega o carregamento dos resultados usando a consulta de pesquisa fornecida. - A atividade é criada após uma mudança de configuração. Como a instância
ViewModel
não foi destruída, oViewModel
tem 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.
Blogs
- ViewModels: exemplo simples (em inglês)
- ViewModels: persistência,
onSaveInstanceState()
, restauração do estado da interface e Loaders (em inglês) - Codelab de componentes compatíveis com ciclo de vida do Android
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