Módulo de estado guardado para ViewModel Parte de Android Jetpack.
Según se mencionó en Cómo guardar estados de IU, los objetos ViewModel pueden manejar 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 objetos ViewModel, 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 objeto SavedStateHandle mediante su constructor. Este objeto es un mapa de 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í se restablece.
Configuración
Para usar SavedStateHandle, acéptalo como un argumento de constructor para tu ViewModel.
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
Luego, puedes recuperar una instancia de tu ViewModel dentro de tus elementos componibles sin ninguna configuración adicional. La fábrica predeterminada de ViewModel 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 ViewModelProvider.Factory personalizada, puedes habilitar el uso de SavedStateHandle con CreationExtras y el DSL viewModelFactory.
Cómo trabajar con SavedStateHandle
La clase SavedStateHandle es un mapa de pares 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 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(): Devuelve todas las claves incluidas enSavedStateHandle.
Además, puedes recuperar valores de SavedStateHandle con un contenedor de datos observables. La lista de tipos admitidos incluye lo siguiente:
StateFlow
Puedes recuperar valores de SavedStateHandle unidos en un observable de StateFlow. Según si necesitas mutar el valor directamente, puedes elegir entre un flujo de solo lectura o mutable:
getStateFlow(): Usa esta opción 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 un flujo de solo lectura y transformarlo con operadores de Flow.getMutableStateFlow(): Usa esta opción si necesitas acceso de lectura y escritura. Cuando actualizas el.valuedelMutableStateFlowque se muestra, se actualiza automáticamente elSavedStateHandlesubyacente, lo que te evita tener que establecer 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 de la IU complejo, puedes usar el delegado de propiedades saved junto con la serialización de KotlinX. Este delegado te permite conservar clases de datos de @Serializable personalizadas directamente en SavedStateHandle. Esto conserva el estado de tu ViewModel durante 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 el estado de Compose
Si tu estado se basa en las APIs de Saver de Compose 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 a través de 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 que se conservan en un SavedStateHandle se guardan y restablecen como un Bundle, junto con el resto de los 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 hacerla 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.
A partir de Lifecycle 2.3.0-alpha03, SavedStateHandle te permite guardar cualquier objeto proporcionando tu propia lógica para guardar y restablecer tu objeto como un Bundle con el método setSavedStateProvider().
SavedStateRegistry.SavedStateProvider es una interfaz que define un solo método saveState() que devuelve un Bundle que contiene el estado que deseas guardar. Cuando SavedStateHandle está listo para guardar su estado, llama a saveState() para recuperar el Bundle del SavedStateProvider y guarda el Bundle para la clave asociada.
Considera el ejemplo de una app que solicita una imagen de la app de cámara a través del intent ACTION_IMAGE_CAPTURE y pasa un archivo temporal para que la cámara almacene la imagen. 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 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 Bundle del temp_file de SavedStateHandle. Este es el mismo Bundle proporcionado por saveTempFile() que contiene la ruta de acceso absoluta. Luego, se puede usar la ruta de acceso absoluta para crear una instancia de un File nuevo.
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 vistas
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