Módulo Saved State para ViewModel (Views)   Parte do Android Jetpack.

Conceitos e implementação do Jetpack Compose

Conforme mencionado em Como salvar estados de interface, os objetos ViewModel podem processar mudanças de configuração. Então, você não precisa se preocupar com o estado em rotações ou outros casos. No entanto, se você precisa lidar com encerramentos de processo iniciados pelo sistema, é recomendável usar a API SavedStateHandle como backup.

O estado da interface geralmente é armazenado ou referenciado em ViewModel objetos, não em atividades. O uso de onSaveInstanceState() exige o uso de um código boilerplate que o módulo Saved State pode processar para você.

Ao usar esse módulo, ViewModel objetos recebem um SavedStateHandle objeto pelo construtor. Esse objeto é um mapa de chave-valor que permite gravar e acessar objetos de e para o estado salvo. Esses valores persistem depois que o processo é encerrado pelo sistema e permanecem disponíveis pelo mesmo objeto.

O estado salvo fica vinculado à pilha de tarefas. Portanto, se ela desaparece, o estado também desaparece. Isso pode ocorrer ao forçar o fechamento ou remover o app do menu "Recentes" ou ao reiniciar o dispositivo. Nesses casos, a pilha de tarefas desaparece e não é possível restaurar as informações do estado salvo. Em cenários em que o estado da interface iniciado pelo usuário é dispensado, o estado salvo não é restaurado. Em cenários iniciados pelo sistema, ele é.

Configuração

A partir do Fragment 1.2.0 ou da respectiva dependência transitiva Activity 1.1.0, é possível aceitar um SavedStateHandle como argumento de construtor para seu ViewModel.

Kotlin

class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }

Java

public class SavedStateViewModel extends ViewModel {
    private SavedStateHandle state;

    public SavedStateViewModel(SavedStateHandle savedStateHandle) {
        state = savedStateHandle;
    }

    ...
}

Assim, você pode extrair uma instância do ViewModel sem qualquer outra configuração. A fábrica ViewModel padrão fornece o SavedStateHandle apropriado para seu ViewModel.

Kotlin

class MainFragment : Fragment() {
    val vm: SavedStateViewModel by viewModels()

    ...
}

Java

class MainFragment extends Fragment {
    private SavedStateViewModel vm;

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        vm = new ViewModelProvider(this).get(SavedStateViewModel.class);

        ...

    }

    ...
}

Ao fornecer uma instância ViewModelProvider.Factory personalizada, é possível ativar o uso de SavedStateHandle estendendo AbstractSavedStateViewModelFactory.

Como trabalhar com SavedStateHandle

A classe SavedStateHandle é um mapa de chave-valor que permite gravar e acessar dados de e para o estado salvo, usando os métodos set() e get().

Usando SavedStateHandle, o valor da consulta é retido após o encerramento do processo, garantindo que o usuário veja o mesmo conjunto de dados filtrados antes e depois da recriação sem que a atividade ou o fragmento precise salvar, restaurar e encaminhar manualmente esse valor de volta para ViewModel.

SavedStateHandle também tem outros métodos que podem ser esperados ao interagir com um mapa de chave-valor:

Além disso, é possível extrair valores de SavedStateHandle usando um detentor de dados observáveis. Veja a lista de tipos com suporte:

LiveData

Extraia valores de SavedStateHandle que são encapsulados em um LiveData observável usando getLiveData(). Quando o valor da chave é atualizado, o LiveData recebe o novo valor. Na maioria das vezes, o valor é definido devido a interações do usuário, como a inserção de uma consulta para filtrar uma lista de dados. Esse valor atualizado pode ser usado para transformar LiveData.

Kotlin

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    val filteredData: LiveData<List<String>> =
        savedStateHandle.getLiveData<String>("query").switchMap { query ->
        repository.getFilteredData(query)
    }

    fun setQuery(query: String) {
        savedStateHandle["query"] = query
    }
}

Java

public class SavedStateViewModel extends ViewModel {
    private SavedStateHandle savedStateHandle;
    public LiveData<List<String>> filteredData;
    public SavedStateViewModel(SavedStateHandle savedStateHandle) {
        this.savedStateHandle = savedStateHandle;
        LiveData<String> queryLiveData = savedStateHandle.getLiveData("query");
        filteredData = Transformations.switchMap(queryLiveData, query -> {
            return repository.getFilteredData(query);
        });
    }

    public void setQuery(String query) {
        savedStateHandle.set("query", query);
    }
}

Tipos compatíveis

Dados mantidos em um SavedStateHandle são salvos e restaurados como um Bundle, com o restante do savedInstanceState para a atividade ou fragmento.

Salvar classes não comparáveis

Se uma classe não implementar Parcelable ou Serializable e não puder ser modificada para implementar uma dessas interfaces, não será possível salvar diretamente uma instância dessa classe em um SavedStateHandle.

No Lifecycle 2.3.0-alpha03 e versões mais recentes, SavedStateHandle permite que você salve qualquer objeto fornecendo sua lógica para salvar e restaurar o objeto como um Bundle usando o método setSavedStateProvider(). SavedStateRegistry.SavedStateProvider é uma interface que define um único método saveState() que retorna um Bundle contendo o estado que você quer salvar. Quando SavedStateHandle está pronto para salvar o estado, ele chama saveState() para extrair o Bundle do SavedStateProvider e salvar o Bundle para a chave associada.

Imagine um exemplo de app que solicita uma imagem do app de câmera via a ACTION_IMAGE_CAPTURE intent, transmitindo um arquivo temporário para onde a câmera vai armazenar a imagem. O TempFileViewModel encapsula a lógica para criar esse arquivo temporário.

Kotlin

class TempFileViewModel : ViewModel() {
    private var tempFile: File? = null

    fun createOrGetTempFile(): File {
        return tempFile ?: File.createTempFile("temp", null).also {
            tempFile = it
        }
    }
}

Java

class TempFileViewModel extends ViewModel {
    private File tempFile = null;

    public TempFileViewModel() {
    }

    @NonNull
    public File createOrGetTempFile() {
        if (tempFile == null) {
            tempFile = File.createTempFile("temp", null);
        }
        return tempFile;
    }
}

Para garantir que o arquivo temporário não seja perdido se o processo da atividade for encerrado e depois restaurado, TempFileViewModel pode usar SavedStateHandle para manter os dados. Para permitir que TempFileViewModel salve os dados, implemente SavedStateProvider e defina-o como um provedor no SavedStateHandle do ViewModel:

Kotlin

private fun File.saveTempFile() = bundleOf("path", absolutePath)

class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    private var tempFile: File? = null
    init {
        savedStateHandle.setSavedStateProvider("temp_file") { // saveState()
            if (tempFile != null) {
                tempFile.saveTempFile()
            } else {
                Bundle()
            }
        }
    }

    fun createOrGetTempFile(): File {
        return tempFile ?: File.createTempFile("temp", null).also {
            tempFile = it
        }
    }
}

Java

class TempFileViewModel extends ViewModel {
    private File tempFile = null;

    public TempFileViewModel(SavedStateHandle savedStateHandle) {
        savedStateHandle.setSavedStateProvider("temp_file",
            new TempFileSavedStateProvider());
    }
    @NonNull
    public File createOrGetTempFile() {
        if (tempFile == null) {
            tempFile = File.createTempFile("temp", null);
        }
        return tempFile;
    }

    private class TempFileSavedStateProvider implements SavedStateRegistry.SavedStateProvider {
        @NonNull
        @Override
        public Bundle saveState() {
            Bundle bundle = new Bundle();
            if (tempFile != null) {
                bundle.putString("path", tempFile.getAbsolutePath());
            }
            return bundle;
        }
    }
}

Para restaurar os dados de File quando o usuário retornar, acesse o Bundle temp_file do SavedStateHandle. Esse é o mesmo Bundle fornecido pelo saveTempFile() que contém o caminho absoluto. O caminho absoluto pode ser usado para instanciar um novo File.

Kotlin

private fun File.saveTempFile() = bundleOf("path", absolutePath)

private fun Bundle.restoreTempFile() = if (containsKey("path")) {
    File(getString("path"))
} else {
    null
}

class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    private var tempFile: File? = null
    init {
        val tempFileBundle = savedStateHandle.get<Bundle>("temp_file")
        if (tempFileBundle != null) {
            tempFile = tempFileBundle.restoreTempFile()
        }
        savedStateHandle.setSavedStateProvider("temp_file") { // saveState()
            if (tempFile != null) {
                tempFile.saveTempFile()
            } else {
                Bundle()
            }
        }
    }

    fun createOrGetTempFile(): File {
      return tempFile ?: File.createTempFile("temp", null).also {
          tempFile = it
      }
    }
}

Java

class TempFileViewModel extends ViewModel {
    private File tempFile = null;

    public TempFileViewModel(SavedStateHandle savedStateHandle) {
        Bundle tempFileBundle = savedStateHandle.get("temp_file");
        if (tempFileBundle != null) {
            tempFile = TempFileSavedStateProvider.restoreTempFile(tempFileBundle);
        }
        savedStateHandle.setSavedStateProvider("temp_file", new TempFileSavedStateProvider());
    }

    @NonNull
    public File createOrGetTempFile() {
        if (tempFile == null) {
            tempFile = File.createTempFile("temp", null);
        }
        return tempFile;
    }

    private class TempFileSavedStateProvider implements SavedStateRegistry.SavedStateProvider {
        @NonNull
        @Override
        public Bundle saveState() {
            Bundle bundle = new Bundle();
            if (tempFile != null) {
                bundle.putString("path", tempFile.getAbsolutePath());
            }
            return bundle;
        }

        @Nullable
        private static File restoreTempFile(Bundle bundle) {
            if (bundle.containsKey("path") {
                return File(bundle.getString("path"));
            }
            return null;
        }
    }
}