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.

O estado da interface geralmente é armazenado ou referenciado em ViewModel objetos. O uso de rememberSaveable no Compose exige 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

Para usar SavedStateHandle, aceite-o como um argumento de construtor para seu ViewModel.

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

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

class MyViewModel : ViewModel() { /*...*/ }

// import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun MyScreen(
    viewModel: MyViewModel = viewModel()
) {
    // use viewModel here
}

Ao fornecer uma instância ViewModelProvider.Factory personalizada, é possível ativar o uso de SavedStateHandle usando CreationExtras e a viewModelFactory DSL.

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. A lista de tipos com suporte inclui o seguinte:

StateFlow

É possível extrair valores de SavedStateHandle encapsulados em um observável StateFlow. Dependendo se você precisa mutar o valor diretamente, é possível escolher entre um fluxo somente leitura ou mutável:

  • getStateFlow(): use esse método se precisar apenas ler o estado. Quando você atualiza o valor da chave em outro lugar no SavedStateHandle, o StateFlow recebe o novo valor. Isso é ideal quando você quer expor um fluxo somente leitura e transformá-lo usando operadores de fluxo.
  • getMutableStateFlow(): use esse método se precisar de acesso de leitura e gravação. A atualização do .value do MutableStateFlow retornado atualiza automaticamente o SavedStateHandle subjacente, evitando que você precise definir a chave manualmente.

Na maioria das vezes, esses valores são atualizados devido a interações do usuário, como a inserção de uma consulta para filtrar uma lista de dados.

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

    // Use getMutableStateFlow to read and write the query directly
    private val _query = savedStateHandle.getMutableStateFlow("query", "")
    val query: StateFlow = _query.asStateFlow()

    // Use getStateFlow if you only need a read-only stream to react to changes
    val filteredData: StateFlow<List> =
        query.flatMapLatest {
            repository.getFilteredData(it)
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )

    fun setQuery(newQuery: String) {
        // Updating the MutableStateFlow automatically updates the SavedStateHandle
        _query.value = newQuery
    }
}

Suporte à serialização do KotlinX

Para estados complexos da interface, você pode usar o delegado de propriedade saved com a serialização do KotlinX. Esse delegado permite persistir classes de dados @Serializable personalizadas diretamente no SavedStateHandle. Isso preserva o estado do ViewModel durante o encerramento do processo, para que a interface do Compose possa restaurar o estado perfeitamente após a recriação.

Para usar, anote sua classe de dados com @Serializable e use o delegado saved no ViewModel:

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
// Ensure you have the savedstate-ktx dependency
import androidx.savedstate.serialization.saved
import kotlinx.serialization.Serializable

@Serializable
data class UserFilterState(
    val searchQuery: String,
    val minAge: Int,
    val includeInactive: Boolean
)

class FilterViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {

    // The state is automatically serialized to a Bundle on process death,
    // and deserialized upon recreation.
    var filterState by savedStateHandle.saved {
        UserFilterState(searchQuery = "", minAge = 18, includeInactive = false)
    }

    fun updateQuery(newQuery: String) {
        // Mutating the property automatically updates the underlying SavedStateHandle
        filterState = filterState.copy(searchQuery = newQuery)
    }
}

Suporte ao estado do Compose

Se o estado depender das APIs Saver do Compose em vez da serialização do KotlinX, o artefato lifecycle-viewmodel-compose vai fornecer o delegado saveable. Isso permite a interoperabilidade entre SavedStateHandle e Saver do Compose para que qualquer State que você possa salvar usando rememberSaveable com um Saver personalizado também possa ser salvo com SavedStateHandle.

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

    var 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 do seu app.

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, considere tornar a classe parcelável 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 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.

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

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

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 de ViewModel:

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
        }
    }
}

Para restaurar os dados de File quando o usuário retornar, acesse o temp_file Bundle 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.

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
      }
    }
}

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.

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

Conteúdo de visualizações