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

Conforme mencionado em Como salvar estados de IU, 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ê precisar lidar com a interrupção do processo iniciada pelo sistema, é recomendável usar onSaveInstanceState() como backup.

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

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

Configurar

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 recuperar uma instância do ViewModel sem nenhuma 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 recuperar dados de e para o estado salvo usando os métodos set() e get().

Usando SavedStateHandle, o valor da consulta é retido após a interrupção 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 recuperar valores de SavedStateHandle usando um detentor de dados observáveis. Veja a lista de tipos com suporte:

LiveData

Recuperar 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 UserViewModelJava(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);
    }
}

StateFlow

Recuperar valores de SavedStateHandle que são encapsulados em um StateFlow observável usando getStateFlow(). Quando você atualiza o valor da chave, o StateFlow recebe o novo valor. Na maioria das vezes, é possível definir o valor devido a interações do usuário, por exemplo, a inserção de uma consulta para filtrar uma lista de dados. Em seguida, você pode transformar esse valor atualizado usando outros operadores do Flow.

Kotlin

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

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

Suporte para o estado experimental do Compose

O artefato lifecycle-viewmodel-compose fornece as APIs experimentais saveable que permitem a interoperabilidade entre o SavedStateHandle e o Saver do Compose. Assim, qualquer State que você pode salvar por meio de rememberSaveable com um Saver personalizado também pode ser salvo com SavedStateHandle.

Kotlin

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {

    val filteredData: List<String> by savedStateHandle.saveable {
        mutableStateOf>(emptyList())
    }

    fun setQuery(query: String) {
        withMutableSnapshot {
            filteredData = 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 o fragmento.

Tipos diretamente compatíveis

Por padrão, você pode chamar set() e get() em um SavedStateHandle para os mesmos tipos de dados que um Bundle, como mostrado abaixo:

Compatibilidade com tipo/classe Compatibilidade com a matriz
double double[]
int int[]
long long[]
String String[]
byte byte[]
char char[]
CharSequence CharSequence[]
float float[]
Parcelable Parcelable[]
Serializable Serializable[]
short short[]
SparseArray
Binder
Bundle
ArrayList
Size (only in API 21+)
SizeF (only in API 21+)

Se a classe não estender um desses itens na lista acima, torne-a parcelada adicionando a anotação @Parcelize do Kotlin ou implementando Parcelable diretamente.

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.

A partir do Lifecycle 2.3.0-alpha03, SavedStateHandle permite que você salve qualquer objeto fornecendo sua lógica para salvar e restaurar seu 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 estiver pronto para salvar o estado, chamará saveState() para recuperar 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 por meio da intent ACTION_IMAGE_CAPTURE, transmitindo um arquivo temporário para onde a câmera 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 poderá 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, recupere 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;
        }
    }
}

Outros recursos

Para mais informações sobre o módulo Saved State para ViewModel, consulte os recursos a seguir.

Codelabs