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

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.

Em geral, o estado da interface é armazenado ou referenciado em objetos ViewModel, não em atividades. O uso de onSaveInstanceState() ou rememberSaveable exige um 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 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. Já em cenários iniciados pelo sistema, o estado é salvo.

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 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 recuperar 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);
    }
}

StateFlow

Extraia 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 a SavedStateHandle e a Saver do Compose para que qualquer State que você possa salvar usando rememberSaveable com um Saver personalizado também possa ser salvo com SavedStateHandle.

Kotlin

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

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

    fun setQuery(query: String) {
        withMutableSnapshot {
            filteredData += query
        }
    }
}

Tipos com suporte

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 com suporte direto

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

Suporte para tipo/classe Suporte para 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, parcele-a 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.

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 pela intent ACTION_IMAGE_CAPTURE, 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;
        }
    }
}

Usar o SavedStateHandle em testes

Para testar um ViewModel que usa um SavedStateHandle como dependência, crie uma nova instância de SavedStateHandle com os valores de teste necessários e o transmita para a instância do ViewModel que você está testando.

Kotlin

class MyViewModelTest {

    private lateinit var viewModel: MyViewModel

    @Before
    fun setup() {
        val savedState = SavedStateHandle(mapOf("someIdArg" to testId))
        viewModel = MyViewModel(savedState = savedState)
    }
}

Outros recursos

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

Codelabs