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
, et non dans des activités. Par conséquent, l'utilisation d'onSaveInstanceState()
ou de rememberSaveable
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
À partir de la version Fragment 1.2.0 ou de sa dépendance transitive Activity 1.1.0, vous pouvez accepter un objet SavedStateHandle
comme argument de constructeur de votre ViewModel
.
Kotlin
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
Java
public class SavedStateViewModel extends ViewModel { private SavedStateHandle state; public SavedStateViewModel(SavedStateHandle savedStateHandle) { state = savedStateHandle; } ... }
Vous pouvez ensuite récupérer une instance de votre ViewModel
sans configuration supplémentaire. La fabrique ViewModel
par défaut fournit le SavedStateHandle
approprié à votre 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); ... } ... }
Lorsque vous fournissez une instance ViewModelProvider.Factory
personnalisée, vous pouvez activer l'utilisation de SavedStateHandle
en étendant AbstractSavedStateViewModelFactory
.
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 :
LiveData
Récupérez les valeurs SavedStateHandle
qui sont encapsulées dans un objet LiveData
observable à l'aide de getLiveData()
.
Lorsque la valeur de la clé est mise à jour, LiveData
reçoit la nouvelle valeur. Le plus souvent, cette valeur est définie en raison des interactions des utilisateurs, comme la saisie d'une requête pour filtrer une liste de données. Cette valeur mise à jour peut ensuite être utilisée pour transformer 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
Récupérez les valeurs SavedStateHandle
qui sont encapsulées dans un objet StateFlow
observable à l'aide de getStateFlow()
.
Lorsque vous mettez à jour la valeur de la clé, StateFlow
reçoit la nouvelle valeur. La plupart du temps, vous pouvez définir la valeur en fonction des interactions de l'utilisateur, comme la saisie d'une requête pour filtrer une liste de données. Vous pouvez ensuite transformer cette valeur mise à jour à l'aide d'autres opérateurs de flux.
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 } }
Prise en charge expérimentale de State par Compose
L'artefact lifecycle-viewmodel-compose
fournit la version expérimentale
saveable
API permettant l'interopérabilité entre SavedStateHandle
et Compose
Saver
afin que toute State
que vous
économie possible via rememberSaveable
avec un Saver
personnalisé peuvent également être enregistrés avec 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 } } }
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 l'activité ou le fragment.
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.
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; } }
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
:
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; } } }
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
.
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; } } }
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.
Kotlin
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
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