Module Saved State pour ViewModel Inclus dans Android Jetpack.
Comme indiqué dans la section Enregistrer des états d'interface utilisateur, les objets ViewModel peuvent gérer les modifications de configuration. Vous n'avez donc pas à vous soucier de l'état dans les cas de rotation ou autres. Toutefois, si vous devez gérer l'arrêt du processus initié par le système, vous pouvez utiliser SavedStateHandle comme sauvegarde.
L'état de l'interface utilisateur est généralement stocké ou référencé dans des objets ViewModel. Par conséquent, l'utilisation d'rememberSaveable dans Compose nécessite un code récurrent que le module d'état enregistré peut gérer pour vous.
Lorsque vous utilisez ce module, les objets ViewModel reçoivent un objet SavedStateHandle via son constructeur. Cet objet est un mappage clé-valeur qui vous permet d'écrire et de récupérer des objets depuis et vers l'état enregistré. Ces valeurs sont conservées après la fin du processus par le système et restent disponibles via le même objet.
L'état enregistré est lié à votre pile de tâches. Si celle-ci disparaît, l'état enregistré aussi. Cela peut se produire lors de l'arrêt forcé d'une application, de sa suppression du menu "Applications récentes" ou du redémarrage de l'appareil. Dans ce cas, la pile de tâches disparaît et vous ne pouvez pas restaurer les informations de l'état enregistré. Dans les scénarios d'arrêt de l'état de l'interface utilisateur déclenché par l'utilisateur, l'état enregistré n'est pas restauré. C'est le cas pour les scénarios initiés par le système.
Configuration
Pour utiliser SavedStateHandle, acceptez-le comme argument de constructeur pour votre ViewModel.
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
Vous pouvez ensuite récupérer une instance de votre ViewModel dans vos composables sans configuration supplémentaire. La fabrique ViewModel par défaut fournit le SavedStateHandle approprié à votre ViewModel.
class MyViewModel : ViewModel() { /*...*/ } // import androidx.lifecycle.viewmodel.compose.viewModel @Composable fun MyScreen( viewModel: MyViewModel = viewModel() ) { // use viewModel here }
Lorsque vous fournissez une instance ViewModelProvider.Factory personnalisée, vous pouvez activer l'utilisation de SavedStateHandle à l'aide de CreationExtras et du DSL viewModelFactory.
Utiliser SavedStateHandle
La classe SavedStateHandle est un mappage clé-valeur qui vous permet d'écrire et de récupérer des données depuis et vers l'état enregistré via les méthodes set() et get().
En utilisant SavedStateHandle, la valeur de la requête est conservée jusqu'à la fin du processus. Ainsi, l'utilisateur voit le même ensemble de données filtrées avant et après la recréation, sans que l'activité ou le fragment n'aient besoin d'enregistrer, de restaurer ni de transmettre manuellement cette valeur au ViewModel.
SavedStateHandle propose également d'autres méthodes auxquelles vous vous attendez peut-être pour interagir avec un mappage clé/valeur :
contains(String key): vérifie s'il existe une valeur pour la clé donnée.remove(String key): supprime la valeur de la clé donnée.keys(): renvoie toutes les clés contenues dans l'élémentSavedStateHandle.
Vous pouvez également récupérer les valeurs de SavedStateHandle à l'aide d'un conteneur de données observable. Voici la liste des types acceptés :
StateFlow
Vous pouvez récupérer les valeurs de SavedStateHandle encapsulées dans un objet StateFlow observable. Selon que vous devez ou non modifier directement la valeur, vous pouvez choisir entre un flux en lecture seule ou un flux modifiable :
getStateFlow(): utilisez cette option si vous n'avez besoin que de lire l'état. Lorsque vous mettez à jour la valeur de la clé ailleurs dansSavedStateHandle, StateFlow reçoit la nouvelle valeur. C'est idéal lorsque vous souhaitez exposer un flux en lecture seule et le transformer à l'aide d'opérateurs Flow.getMutableStateFlow(): utilisez cette option si vous avez besoin d'un accès en lecture et en écriture. La mise à jour de l'élément.valuede l'élémentMutableStateFlowrenvoyé met automatiquement à jour l'élémentSavedStateHandlesous-jacent, ce qui vous évite d'avoir à définir manuellement la clé.
Le plus souvent, vous mettez à jour ces valeurs en raison des interactions des utilisateurs, comme la saisie d'une requête pour filtrer une liste de données.
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 } }
Compatibilité avec KotlinX Serialization
Pour les états d'UI complexes, vous pouvez utiliser le délégué de propriété saved avec la sérialisation KotlinX. Ce délégué vous permet de conserver les classes de données @Serializable personnalisées directement dans SavedStateHandle. Cela permet de préserver l'état de votre ViewModel en cas d'arrêt du processus, afin que votre UI Compose puisse restaurer son état de manière fluide lors de la recréation.
Pour l'utiliser, annotez votre classe de données avec @Serializable et utilisez le délégué saved dans votre 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) } }
Prise en charge de State par Compose
Si votre état repose sur les API Saver de Compose au lieu de la sérialisation KotlinX, l'artefact lifecycle-viewmodel-compose fournit le délégué saveable. Cela permet l'interopérabilité entre SavedStateHandle et le Saver de Compose afin que tout State que vous pouvez enregistrer via rememberSaveable avec un Saver personnalisé puisse également être enregistré avec SavedStateHandle.
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { var filteredData: List<String> by savedStateHandle.saveable { mutableStateOf(emptyList()) } fun setQuery(query: String) { withMutableSnapshot { filteredData += query } } }
Types pris en charge
Les données conservées dans un SavedStateHandle sont enregistrées et restaurées en tant que Bundle, avec le reste de savedInstanceState pour votre application.
Types directement pris en charge
Par défaut, vous pouvez appeler set() et get() sur un SavedStateHandle pour les mêmes types de données qu'un Bundle, comme indiqué ci-dessous :
| Prise en charge du type/de la classe | Prise en charge des tableaux |
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 classe n'étend pas l'un des éléments de la liste ci-dessus, vous pouvez la rendre parcelable en ajoutant l'annotation Kotlin @Parcelize ou en implémentant directement Parcelable.
Enregistrer des classes non parcelables
Si une classe n'implémente pas Parcelable ou Serializable et ne peut pas être modifiée pour implémenter l'une de ces interfaces, il n'est pas possible d'enregistrer directement une instance de cette classe dans un SavedStateHandle.
À partir du cycle de vie 2.3.0-alpha03, SavedStateHandle vous permet d'enregistrer un objet en fournissant votre propre logique d'enregistrement et de restauration en tant qu'objet Bundle à l'aide de la méthode setSavedStateProvider().
SavedStateRegistry.SavedStateProvider est une interface qui définit une seule méthode saveState() qui renvoie un Bundle contenant l'état que vous souhaitez enregistrer. Lorsque SavedStateHandle est prêt à enregistrer son état, il appelle saveState() pour récupérer le Bundle à partir du SavedStateProvider et enregistre le Bundle pour la clé associée.
Prenons l'exemple d'une application qui demande une image à l'appli Appareil photo via l'intent ACTION_IMAGE_CAPTURE, en transmettant un fichier temporaire dans lequel l'appareil photo doit stocker l'image. TempFileViewModel encapsule la logique de création de ce fichier temporaire.
class TempFileViewModel : ViewModel() { private var tempFile: File? = null fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
Pour éviter de perdre le fichier temporaire si le processus de l'activité est arrêté, puis restauré, TempFileViewModel peut utiliser SavedStateHandle pour conserver ses données. Pour autoriser TempFileViewModel à enregistrer ses données, implémentez SavedStateProvider et définissez-le comme fournisseur sur le SavedStateHandle du 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 } } }
Pour restaurer les données File lorsque l'utilisateur revient, récupérez le temp_file Bundle à partir du SavedStateHandle. Il s'agit de la même valeur Bundle fournie par saveTempFile() qui contient le chemin absolu. Le chemin absolu peut ensuite être utilisé pour instancier un nouveau 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 } } }
SavedStateHandler dans les tests
Pour tester un ViewModel qui utilise SavedStateHandle comme dépendance, créez une instance de SavedStateHandle avec les valeurs de test requises et transmettez-la à l'instance ViewModel que vous testez.
class MyViewModelTest { private lateinit var viewModel: MyViewModel @Before fun setup() { val savedState = SavedStateHandle(mapOf("someIdArg" to testId)) viewModel = MyViewModel(savedState = savedState) } }
Ressources supplémentaires
Pour en savoir plus sur le module Saved State pour ViewModel, consultez les ressources suivantes.
Ateliers de programmation
Contenu des vues
Recommandations personnalisées
- Remarque : Le texte du lien s'affiche lorsque JavaScript est désactivé
- Enregistrer les états de l'interface utilisateur
- Utiliser les objets de données observables
- Créer des ViewModels avec des dépendances