Módulo de estado guardado para ViewModel   Parte de Android Jetpack.

Según se mencionó en Cómo guardar estados de IU, ViewModel objetos pueden controlar cambios de configuración, por lo que no necesitas preocuparte por el estado en rotaciones y otros casos. Sin embargo, si necesitas controlar el cierre del proceso que inició el sistema, te recomendamos que uses la API de SavedStateHandle como respaldo.

Por lo general, se almacena o se hace referencia al estado de la IU en ViewModel objetos, por lo que usar rememberSaveable en Compose requiere código estándar que el módulo de estado guardado pueda manejar por ti.

Cuando se usa este módulo, los objetos ViewModel reciben un SavedStateHandle objeto a través de su constructor. Este objeto es un mapa de par clave-valor que te permite escribir y recuperar objetos en el estado guardado y desde este. Estos valores se conservan después de que el sistema finaliza el proceso y quedan disponibles a través del mismo objeto.

El estado guardado está vinculado a la pila de tareas. Si la pila de tareas desaparece, también desaparece el estado guardado. Esto puede ocurrir cuando fuerzas la detención de una app, la quitas del menú Recientes o reinicias el dispositivo. En esos casos, la pila de tareas desaparece y no puedes restablecer la información en estado guardado. En las situaciones de descarte del estado de la IU iniciada por el usuario, no se restablece el estado guardado. En situaciones iniciadas por el sistema, sí.

Configuración

Para usar SavedStateHandle, acéptalo como un argumento de constructor en tu ViewModel.

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

Luego, podrás recuperar una instancia de ViewModel dentro de tus elementos componibles sin ninguna configuración adicional. La fábrica ViewModel predeterminada proporciona el SavedStateHandle adecuado a tu ViewModel.

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

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

Cuando proporcionas una instancia de ViewModelProvider.Factory personalizada, puedes habilitar el uso de SavedStateHandle con CreationExtras y el viewModelFactory DSL.

Cómo trabajar con SavedStateHandle

La clase SavedStateHandle es un mapa de par clave-valor que te permite escribir y recuperar datos hacia el estado guardado y desde este mediante los métodos set() y get().

Mediante SavedStateHandle, se retiene el valor de la búsqueda luego del cierre del proceso, lo que asegura que el usuario vea el mismo conjunto de datos filtrados antes y después de la recreación sin necesidad de que la actividad o el fragmento guarden, restablezcan o reenvíen el valor a ViewModel de forma manual.

SavedStateHandle también tiene otros métodos que quizás esperas cuando interactúas con un mapa de par clave-valor:

Además, puedes recuperar valores de SavedStateHandle con un contenedor de datos observables. La lista de tipos compatibles incluye lo siguiente:

StateFlow

Puedes recuperar valores de SavedStateHandle envueltos en un objeto observable StateFlow. Según si necesitas mutar el valor directamente, puedes elegir entre una transmisión de solo lectura o mutable:

  • getStateFlow(): Úsalo si solo necesitas leer el estado. Cuando actualizas el valor de la clave en otro lugar de SavedStateHandle, StateFlow recibe el valor nuevo. Esto es ideal cuando quieres exponer una transmisión de solo lectura y transformarla con operadores de Flow.
  • getMutableStateFlow(): Úsalo si necesitas acceso de lectura y escritura. Si actualizas el .value del MutableStateFlow que se muestra, se actualiza automáticamente el SavedStateHandle subyacente, lo que te evita tener que configurar la clave de forma manual.

Con frecuencia, actualizas estos valores debido a las interacciones del usuario, como ingresar una búsqueda para filtrar una lista de datos.

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

Compatibilidad con la serialización de KotlinX

Para el estado complejo de la IU, puedes usar el delegado de propiedad saved junto con la serialización de KotlinX. Este delegado te permite conservar las clases de datos @Serializable personalizadas directamente en SavedStateHandle. Esto conserva el estado de tu ViewModel hasta el cierre del proceso, por lo que tu IU de Compose puede restablecer su estado sin problemas cuando se vuelve a crear.

Para usarlo, anota tu clase de datos con @Serializable y usa el delegado saved en tu 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)
    }
}

Compatibilidad con estados de Compose

Si tu estado depende de las APIs de Compose's Saver en lugar de la serialización de KotlinX, el artefacto lifecycle-viewmodel-compose proporciona el delegado saveable. Esto permite la interoperabilidad entre SavedStateHandle y Saver de Compose para que cualquier State que puedas guardar mediante rememberSaveable con un Saver personalizado también se pueda guardar con 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 admitidos

Los datos conservados en un SavedStateHandle se guardan y se restablecen como un Bundle, junto con el resto de las savedInstanceState de tu app.

Tipos admitidos de forma directa

De forma predeterminada, puedes llamar a set() y get() en un SavedStateHandle para los mismos tipos de datos que un Bundle, como se muestra a continuación:

Compatibilidad con tipo/clase Compatibilidad con arreglos
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+)

Si la clase no extiende una de las de la lista anterior, considera hacer la clase parcelable. Para ello, agrega la anotación @Parcelize de Kotlin o implementa Parcelable directamente.

Cómo guardar clases no parcelables

Si una clase no implementa Parcelable o Serializable y no se puede modificar para que implemente una de esas interfaces, entonces no es posible guardar directamente una instancia de esa clase en un SavedStateHandle.

Desde Lifecycle 2.3.0-alpha03, SavedStateHandle te permite guardar cualquier objeto, ya que proporciona tu propia lógica para guardar y restablecer el objeto como un Bundle mediante el método setSavedStateProvider(). SavedStateRegistry.SavedStateProvider es una interfaz que define un único método saveState() que muestra un Bundle que contiene el estado que quieres guardar. Cuando SavedStateHandle está listo para guardar su estado, llama a saveState() a fin de recuperar el Bundle de SavedStateProvider y guarda el Bundle para la clave asociada.

Considera un ejemplo de una app que solicita una imagen de la app de cámara mediante el ACTION_IMAGE_CAPTURE intent pasando un archivo temporal para el lugar donde la cámara debe almacenar la imagen. El TempFileViewModel encapsula la lógica para crear ese archivo temporal.

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

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

A fin de garantizar que el archivo temporal no se pierda si el proceso de la actividad finaliza y luego se restablece, TempFileViewModel puede usar SavedStateHandle para conservar sus datos. A fin de permitir que TempFileViewModel guarde sus datos, implementa SavedStateProvider y establécelo como un proveedor en el SavedStateHandle de el 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 restablecer los datos de File cuando el usuario vuelva, recupera el temp_file Bundle del SavedStateHandle. Este es el mismo Bundle proporcionado por saveTempFile() que contiene la ruta de acceso absoluta. Luego, se puede usar esta ruta para crear una instancia de un nuevo 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
      }
    }
}

SavedStateHandle en las pruebas

Para probar un ViewModel que tome un SavedStateHandle como dependencia, crea una instancia nueva de SavedStateHandle con los valores de prueba que requiere y pásala a la instancia de ViewModel que estás probando.

class MyViewModelTest {

    private lateinit var viewModel: MyViewModel

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

Recursos adicionales

Para obtener más información sobre el módulo de estado guardado para ViewModel, consulta los siguientes recursos.

Codelabs

Contenido de Views