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í.
Para el estado que se usa en la lógica empresarial, mantenla en un ViewModel y guárdala conSavedStateHandle. Para el estado que se usa en
la lógica de la IU, usa rememberSaveable en Compose.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.
Activity solo guarda los datos que se escriben cuando se detiene el
host (por ejemplo, cuando la app se envía a segundo plano).SavedStateHandle
Las escrituras en SavedStateHandle mientras se detiene Activity no se guardan, a menos que Activity reciba onStart seguido de onStop nuevamente (por ejemplo, cuando la app se pone en primer plano y luego vuelve a segundo plano).SavedStateHandle también tiene otros métodos que quizás esperas cuando interactúas con un mapa de par clave-valor:
contains(String key): Verifica si hay un valor para la clave determinada.remove(String key): Quita el valor de la clave determinada.keys(): Muestra todas las claves contenidas enSavedStateHandle.
Además, puedes recuperar valores de SavedStateHandle con un contenedor de datos observables. La lista de tipos compatibles incluye lo siguiente:
get() y set() básicos. En Compose, el estado se captura solo cuando la aplicación pasa a segundo plano. Eso significa que, si bien puedes seguir actualizando los contenedores de datos observables de un SavedStateHandle mientras la app está en segundo plano, todas las actualizaciones de estado podrían perderse si se finaliza el proceso de la app antes de volver a convertirse en primer plano.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 deSavedStateHandle, 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.valuedelMutableStateFlowque se muestra, se actualiza automáticamente elSavedStateHandlesubyacente, 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.
getMutableStateFlow en SavedStateHandle en
Lifecycle versión 2.9.0.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) } }
saved se evalúa de forma diferida. No llama a la expresión lambda de inicialización ni guarda nada en SavedStateHandle hasta que se accede a la propiedad por primera vez.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
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Cómo guardar estados de la IU
- Cómo trabajar con objetos de datos observables
- Cómo crear ViewModels con dependencias