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 l'API 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 de
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 ViewModel objets reçoivent un SavedStateHandle
objet 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é. Dans les scénarios initiés par le système, il l'est.
Configuration
Pour utiliser SavedStateHandle, acceptez-le comme argument de constructeur dans 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
viewModelFactory DSL.
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 après l'arrêt 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 leSavedStateHandle.
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 observable StateFlow. Selon que vous devez modifier directement la valeur ou non, vous pouvez choisir entre un flux en lecture seule ou mutable :
getStateFlow(): utilisez cette option si vous n'avez besoin que de lire l'état. Lorsque vous mettez à jour la valeur de la clé ailleurs dans leSavedStateHandle, le StateFlow reçoit la nouvelle valeur. Cette option est idéale 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 la.valueduMutableStateFlowrenvoyé met automatiquement à jour leSavedStateHandlesous-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 } }
Prise en charge de la sérialisation KotlinX
Pour un état d'interface utilisateur complexe, 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 le SavedStateHandle. Cela préserve l'état de votre ViewModel jusqu'à l'arrêt du processus, de sorte que votre interface utilisateur Compose peut restaurer son état de manière transparente 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 l'état de Compose
Si votre état repose sur les API Saver de Compose plutôt que sur 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 rendre la
classe parcelable en ajoutant l'annotation Kotlin @Parcelize ou en
implémentant Parcelable directement.
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 de
le 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
Afficher le contenu
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