Como salvar estados da IU

Preservar e restaurar o estado da IU de uma atividade em tempo hábil na destruição do aplicativo ou da atividade iniciada pelo sistema é 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 preencher a lacuna 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. Decidir 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 de memória.

Independentemente da abordagem adotada, verifique se o app atende às expectativas dos usuários em relação ao estado da IU e fornece uma IU suave e rápida, evitando atraso no carregamento de dados na IU, especialmente após mudanças frequentes na configuração, como rotação. Na maioria dos casos, você precisa usar ViewModel e 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.

Dispensa do 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;
  • excluindo o app na tela "Configurações";
  • realizando algum tipo de atividade de conclusão (com Activity.finish()).

A suposição do usuário nesses casos de dispensa completa é que ele tenha saído permanentemente da atividade e, se reabrir a atividade, a expectativa é que ela seja iniciada com um estado limpo. O comportamento subjacente do sistema para esses cenários de dispensa corresponde à expectativa do usuário: a instância da atividade será destruída e removida da memória, juntamente 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".

Dispensa do 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 rotação ou alternar 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, embora não recomendado, modificar o comportamento padrão para mudanças de configuração. Consulte Gerenciar alterações de configuração para ver mais detalhes.

O usuário também espera que o estado da IU da atividade permaneça o mesmo se ele alternar temporariamente para outro aplicativo e depois voltar 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 não está interagindo com outros apps. Nesse caso, a instância de atividade é destruída, juntamente 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 de um app.

Opções para preservar o estado da IU

Quando as expectativas do usuário sobre o estado da IU não corresponderem ao 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 à dispensa da atividade completa do usuário/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 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 modelos de visualização a atualizar 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 saber como 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 nas mudanças de configuração e na desativação do processo, mas são limitados pela quantidade de armazenamento e velocidade porque onSavedInstanceState() serializa dados em disco. A serialização pode 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 pode causar queda de frames e falhas visuais se demorar muito.

Não use o onSavedInstanceState() para armazenar grandes quantidades 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 uma 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. Se sua atividade se comportar dessa maneira, você pode abandonar o uso de onSaveInstanceState() e, em vez disso, persistir tudo localmente.

Além disso, ao abrir uma atividade a partir de um intent, o pacote de extras é entregue à atividade quando a configuração é mudada e quando o sistema restaura a atividade. Se dados de estado da IU, como uma consulta de pesquisa, forem transmitidos como um intent extra quando a atividade for iniciada, você poderá usar o pacote extra em vez do pacote onSaveInstanceState(). Para saber mais sobre os 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ção 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.

Observação: agora você pode fornecer acesso a um estado salvo em objetos ViewModel com o módulo Saved State para ViewModel (atualmente na versão Alfa). Esse estado salvo pode ser acessado por meio de um objeto chamado SavedStateHandle. Você pode ver como ele é usado no codelab de componentes que reconhecem o ciclo de vida do Android (link em inglês).

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 armazenar todos os dados que você não quer perder se abrir e fechar a atividade.

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 para a arquitetura do app para ver mais detalhes sobre como aproveitar o armazenamento local para manter os dados do modelo de aplicativo a longo prazo (por exemplo, nas reinicializações do dispositivo).

Como gerenciar o estado da IU: dividir e conquistar

É 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 exibir 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 ID exclusivo para esses objetos em onSaveInstanceState().
    • 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 a partir 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 em si 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: remontagem de peças

Quando chegar o momento de o 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 o ViewModel. O ViewModel 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 o ViewModel já tem os resultados da pesquisa armazenados em cache. A consulta é transferida do pacote onSaveInstanceState() para o ViewModel, que determina que já carregou os dados necessários e que não precisa consultar novamente o banco de dados.

Observação: quando uma atividade é criada pela primeira vez, o pacote onSaveInstanceState() não contém dados, e o objeto ViewModel está vazio. Quando você cria o objeto ViewModel, uma consulta vazia é transferida, o que diz ao objeto ViewModel que não há dados para carregar ainda. Portanto, a atividade começa em um estado vazio.

Outros recursos

Para saber mais sobre como salvar estados da IU, consulte os seguintes recursos.

Blogs (links em inglês)