Salvar estados da interface

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:

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 o SavedStateHandle não for usado. O ViewModel 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, o ViewModel 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