Preservar e restaurar o estado da IU de uma atividade em tempo hábil quando o sistema inicia a destruição do aplicativo ou da atividade é uma parte crucial da experiência do usuário. Nesses casos, o usuário espera que o estado da IU permaneça o mesmo, mas o sistema destrói a atividade e qualquer estado armazenado nela.
Para resolver a diferença entre a expectativa do usuário e o comportamento do sistema, use uma
combinação de objetos ViewModel
,
o método onSaveInstanceState()
e/ou armazenamento local para persistir o estado da IU em tais
transições de instância de
atividade e aplicativo. A decisão de como combinar essas opções depende da
complexidade dos dados de IU, dos casos de uso do seu app e da consideração da
velocidade de recuperação contra o uso da memória.
Não importa a abordagem adotada, o app precisa atender às expectativas dos usuários em relação ao estado da IU e ter uma IU fluída e rápida para evitar o atraso no carregamento de dados nela, especialmente em casos de mudanças frequentes na configuração, como a rotação. Na maioria dos casos, você precisa usar o ViewModel e o onSaveInstanceState().
Esta página discute as expectativas do usuário sobre o estado da IU, as opções disponíveis para preservação do estado, as compensações e as limitações de cada uma.
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 automaticamente o que é esperado pelo usuário. Em outros, o sistema faz o oposto do que o usuário espera.
Dispensar o estado da IU iniciado pelo usuário
O usuário espera que, ao iniciar uma atividade, o estado transitório da IU dessa atividade permaneça o mesmo até que o usuário dispense completamente a atividade. É possível dispensar completamente uma atividade:
- pressionando o botão "Voltar";
- deslizando a atividade para fora da tela "Visão geral" (Recentes);
- navegando para cima a partir da atividade;
- fechando o app na tela Configurações;
- realizando algum tipo de ação "de conclusão" (que seja apoiada por 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, com 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 navegador o leve à página exata que estava visualizando antes de sair do navegador usando o botão "Voltar".
Dispensar o estado da IU iniciada pelo sistema
O usuário espera que o estado da IU 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 IU 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 mudar o comportamento padrão para mudanças de configuração, mas isso não é recomendado. Consulte Gerenciar mudanças de configuração para ver mais detalhes.
O usuário também espera que o estado da IU da atividade permaneça o mesmo caso ele alterne temporariamente para outro app e depois volte para o seu. Por exemplo, o usuário realiza uma pesquisa e depois pressiona o botão home ou atende a uma chamada telefônica. Quando ele retorna à atividade de pesquisa, espera encontrar a palavra-chave e os resultados exibidos ali, exatamente como antes.
Nesse cenário, seu app é colocado em segundo plano para que o sistema faça 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 instância de atividade é destruída, com 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 do processo, consulte Processos e ciclo de vida do app.
Opções para preservar o estado da IU
Quando as expectativas do usuário sobre o estado da IU forem diferentes do comportamento padrão do sistema, salve e restaure o estado da IU do usuário para garantir que a destruição iniciada pelo sistema seja transparente para o usuário.
Cada uma das opções para preservar o estado da IU 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 | Serializado em disco | 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 por espaço em disco ou custo / tempo de recuperação do recurso de rede |
Tempo de leitura/gravação | Rápido (somente acesso à memória) | Lento (requer serialização/desserialização e acesso ao disco) | 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 à IU enquanto o usuário está usando ativamente o aplicativo. Ele permite acesso rápido aos dados da IU e ajuda a evitar a 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 saber como implementar um ViewModel, consulte o Guia do ViewModel.
O ViewModel retém os dados na memória, o que significa que é mais barato recuperá-los 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 da configuração.
Os ViewModels são automaticamente destruídos pelo sistema quando o usuário sai
da sua atividade ou do seu fragmento ou se você chama finish()
, o que significa que o estado
será 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 o encerramento do
processo iniciado pelo sistema. É por isso que você precisa usar objetos ViewModel em combinação
com onSaveInstanceState()
(ou alguma outra persistência de disco), armazenando
identificadores em savedInstanceState
para ajudar a ver modelos que recarregam os dados após
o encerramento do sistema.
Se você já tiver uma solução na memória para armazenar o estado da IU nas mudanças de configuração, talvez não seja necessário usar o ViewModel.
Usar onSaveInstanceState() como backup para lidar com o encerramento do processo iniciado pelo sistema
O callback onSaveInstanceState()
armazenará os dados necessários para atualizar o estado de um controlador de IU, como
uma atividade ou um fragmento, se o sistema destruir e depois recriar esse
controlador. Para aprender a implementar o estado de instância salvo, consulte
Como salvar e restaurar o estado da atividade no
Guia do ciclo de vida da atividade.
Os pacotes de estado de instância salvos persistem após mudanças de configuração e
a desativação do processo, mas são limitados pela quantidade de armazenamento e velocidade porque
onSavedInstanceState()
serializa dados para o disco. A serialização poderá consumir
muita memória se os objetos que estão sendo serializados forem complicados. Como
esse processo acontece na linha de execução principal durante uma mudança de configuração,
a serialização longa pode causar queda de frames e falhas visuais.
Não use o armazenamento onSavedInstanceState()
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
. Assim, use onSaveInstanceState()
para armazenar uma quantidade mínima de dados necessários, como um ID, para recriar
os dados necessários para restaurar a IU ao estado anterior, caso ocorra
falha nos outros mecanismos de persistência. A maioria dos apps precisa implementar
onSaveInstanceState()
para lidar com o encerramento do processo iniciado pelo sistema.
Dependendo dos casos de uso do seu app, talvez não seja necessário usar
onSaveInstanceState()
. Por exemplo, um navegador pode levar o usuário
de volta à página da Web exata que ele estava vendo antes de sair do
navegador. Caso sua atividade se comporte dessa maneira, você pode abandonar o uso de
onSaveInstanceState()
e, em vez disso, persistir tudo localmente.
Além disso, ao abrir uma atividade proveniente de uma intent, o pacote de extras é
entregue à atividade quando a configuração é alterada e quando o
sistema restaura a atividade. Se dados de estado da IU,
como uma consulta de pesquisa, forem transmitidos como uma intent extra quando a
atividade for iniciada, será possível usar o pacote extra em vez
do pacote onSaveInstanceState()
. Para saber mais sobre as intents
extras, consulte Intents e filtros de intents.
Em qualquer um desses cenários, você ainda usaria um
ViewModel
para evitar ciclos
desnecessários de atualizações de dados do banco de dados durante uma mudança de configuração.
Nos casos em que os dados da IU a serem preservados são simples e leves, você
pode usar onSaveInstanceState()
sozinho para preservar seus dados de estado.
Conectar-se ao estado salvo usando SavedStateRegistry
A partir do Fragment 1.1.0 ou
da respectiva dependência transitiva Activity 1.0.0, os controladores de IU,
como Activity
ou Fragment
, implementam
SavedStateRegistryOwner
e fornecem um SavedStateRegistry
que esteja vinculado ao controlador. SavedStateRegistry
permite que os componentes
se conectem ao estado salvo do controlador de IU 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 IU, 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 que precisa ser salvo desse componente.
SavedStateRegistry
chama esse método durante a fase de salvamento do
ciclo de vida do 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 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 recuperados 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ê pode
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
recuperaçã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 lidar com o encerramento do 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). Embora esse armazenamento local sobreviva à atividade iniciada pelo sistema e ao encerramento do processo do aplicativo, pode ser caro recuperá-lo porque ele precisará ser lido 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 temporariamente somente o estado transitório da IU e usar armazenamento persistente para outros dados do app. Consulte o Guia da arquitetura do app para ver mais detalhes sobre como aproveitar o armazenamento local para manter os dados do modelo de app a longo prazo (por exemplo, nas reinicializações do dispositivo).
Como gerenciar o estado da IU: dividir para ter êxito
É possível salvar e restaurar o estado da IU 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, velocidade de acesso e ciclo de vida:
- Persistência local: armazena todos os dados 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 o controlador de IU associado.- Exemplo: os objetos de música da pesquisa mais recente e a consulta de pesquisa mais recente.
onSaveInstanceState()
: armazena um pequeno volume de dados necessários para atualizar com facilidade o estado da atividade caso o sistema pare e, em seguida, recria o controlador de IU. Em vez de armazenar objetos complexos aqui, persista os objetos complexos no armazenamento local e aloque um código exclusivo para esses objetos emonSaveInstanceState()
.- Exemplo: armazenamento da consulta de pesquisa mais recente.
Por exemplo, considere uma atividade 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 IU, você também terá que atualizar os dados
no objeto ViewModel
para refletir a adição da música. Lembre-se 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, qualquer dado de música complexo carregado do
banco de dados para o controlador de IU precisa ser imediatamente armazenado no
objeto ViewModel
. Você também precisa salvar a consulta de pesquisa no
objeto ViewModel
.
Quando a atividade entra em segundo plano,
o sistema chama onSaveInstanceState()
.
Você precisa salvar a consulta de pesquisa no pacote onSaveInstanceState()
.
Esse pequeno volume de dados é muito fácil de salvar. Essa também é toda a informação de que você
precisa para recuperar a atividade para o estado atual dela.
Como restaurar estados complexos: juntando as peças
Quando chegar o momento do usuário retornar à atividade, há dois cenários possíveis para recriá-la:
- A atividade é recriada após ter sido parada pelo sistema. A
atividade tem a consulta salva em um pacote
onSaveInstanceState()
e precisa transferir a consulta para oViewModel
. OViewModel
vê 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. A atividade tem
a consulta salva em um pacote
onSaveInstanceState()
, e oViewModel
já tem os resultados da pesquisa armazenados em cache. A consulta é transferida do pacoteonSaveInstanceState()
para oViewModel
, que determina que já carregou os dados necessários e que não precisa consultar novamente o banco de dados.
Outros recursos
Para saber mais sobre como salvar estados da IU, consulte os seguintes recursos.