Como salvar estados da IU

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. 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 da 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 ação "de conclusão" (que é apoiada por Activity.finish()).

A suposição do usuário nesses casos de dispensa completa é que ele tenha saído permanentemente da atividade e, se reabri-la, 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, 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 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 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 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 o 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, passando uma chave para associar aos dados do provedor, bem como o provedor. Os dados salvos anteriormente para o provedor podem ser recuperados do estado salvo chamando consumeRestoredStateForKey() no SavedStateRegistry, transmitindo a chave associada ao 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 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 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 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 código 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 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 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.

Outros recursos

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

Blogs