Módulo de estado guardado para ViewModel Parte de Android Jetpack.
Como 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
y no en actividades, por lo que usar onSaveInstanceState()
o rememberSaveable
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
A partir de Fragment 1.2.0 o su dependencia transitiva Activity 1.1.0, puedes aceptar un SavedStateHandle
como argumento de constructor de tu ViewModel
.
Kotlin
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
Java
public class SavedStateViewModel extends ViewModel { private SavedStateHandle state; public SavedStateViewModel(SavedStateHandle savedStateHandle) { state = savedStateHandle; } ... }
Luego, podrás recuperar una instancia de ViewModel
sin ninguna configuración adicional. La fábrica predeterminada de ViewModel
proporciona el SavedStateHandle
apropiado para tu 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); ... } ... }
Cuando proporcionas una instancia de ViewModelProvider.Factory
personalizada, puedes habilitar el uso de SavedStateHandle
extendiendo AbstractSavedStateViewModelFactory
.
Cómo trabajar con SavedStateHandle
La clase SavedStateHandle
es un mapa de claves-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:
contains(String key)
: comprueba si hay un valor para la clave dada.remove(String key)
: quita el valor de la clave dada.keys()
: muestra todas las claves incluidas enSavedStateHandle
.
Además, puedes recuperar valores de SavedStateHandle
con un contenedor de datos observables. Estos son los tipos compatibles:
LiveData
Recupera valores de SavedStateHandle
que se unen en un observable de LiveData
por medio de getLiveData()
.
Cuando se actualiza el valor de la clave, LiveData
recibe el valor nuevo. Con frecuencia, el valor se establece debido a las interacciones del usuario, como ingresar una búsqueda a fin de filtrar una lista de datos. Este valor actualizado se puede usar 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
Recupera valores de SavedStateHandle
que se unen en un observable de StateFlow
por medio de getStateFlow()
.
Cuando actualizas el valor de la clave, StateFlow
recibe el valor nuevo. Con frecuencia, puedes establecer el valor debido a las interacciones del usuario, como ingresar una búsqueda para filtrar una lista de datos. Luego, puedes transformar este valor actualizado mediante otros operadores de flujo.
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 } }
Compatibilidad con estados experimentales de Compose
El artefacto lifecycle-viewmodel-compose
proporciona la capa experimental
saveable
APIs que permiten la interoperabilidad entre SavedStateHandle
y Compose
Saver
para que cualquier State
que
puede guardar mediante rememberSaveable
con un Saver
personalizado también se pueden guardar con 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 admitidos
Los datos conservados en un SavedStateHandle
se guardan y se restablecen como un Bundle
, junto con el resto de las savedInstanceState
correspondientes a la actividad o al fragmento.
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
.
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 intent ACTION_IMAGE_CAPTURE
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.
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; } }
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
:
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 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 esta ruta para crear una instancia de un File
nuevo.
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; } } }
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.
Kotlin
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
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