Dans Jetpack Compose, les fonctions composables conservent souvent l'état à l'aide de la fonction remember. Les valeurs mémorisées peuvent être réutilisées lors des recompositions, comme
expliqué dans États et Jetpack Compose.
Bien que remember serve d'outil pour conserver les valeurs lors des recompositions, l'état doit souvent durer au-delà de la durée de vie d'une composition. Cette page explique la
différence entre les API remember, retain, rememberSaveable,
et rememberSerializable, quand choisir chaque API et quelles sont les
bonnes pratiques pour gérer les valeurs mémorisées et conservées dans Compose.
Choisir la durée de vie appropriée
Dans Compose, vous pouvez utiliser plusieurs fonctions pour conserver l'état lors des
compositions et au-delà : remember, retain, rememberSaveable et
rememberSerializable. Ces fonctions diffèrent par leur durée de vie et leur sémantique, et chacune est adaptée au stockage de types d'état spécifiques. Les différences sont décrites dans le tableau suivant :
|
|
|
|
|---|---|---|---|
Les valeurs survivent-elles aux recompositions ? |
✅ |
✅ |
✅ |
Les valeurs survivent-elles aux recréations d'activité ? |
❌ |
✅ La même instance ( |
✅ Un objet équivalent ( |
Les valeurs survivent-elles à l'arrêt du processus ? |
❌ |
❌ |
✅ |
Types de données acceptés |
Tous |
Ne doit pas faire référence à des objets qui seraient divulgués si l'activité était détruite |
Doit être sérialisable |
Cas d'utilisation |
|
|
|
remember
remember est le moyen le plus courant de stocker l'état dans Compose. Lorsque remember est
appelé pour la première fois, le calcul donné est exécuté et est
mémorisé, ce qui signifie qu'il est stocké par Compose pour une utilisation ultérieure par le
composable. Lorsqu'un composable est recomposé, il exécute à nouveau son code, mais tous les appels à remember renvoient leurs valeurs à partir de la composition précédente au lieu d'exécuter à nouveau le calcul.
Chaque instance d'une fonction composable possède son propre ensemble de valeurs mémorisées, appelé mémoïsation positionnelle. Lorsque les valeurs mémorisées sont mémoïsées pour être utilisées lors des recompositions, elles sont liées à leur position dans la hiérarchie de composition. Si un composable est utilisé à différents emplacements, chaque instance de la hiérarchie de composition possède son propre ensemble de valeurs mémorisées.
Lorsqu'une valeur mémorisée n'est plus utilisée, elle est oubliée et son enregistrement est supprimé. Les valeurs mémorisées sont oubliées lorsqu'elles sont supprimées de la hiérarchie de composition (y compris lorsqu'une valeur est supprimée et ajoutée à nouveau pour être déplacée vers un autre emplacement sans utiliser le composable key ou MovableContent), ou lorsqu'elles sont appelées avec différents paramètres key.
Parmi les choix disponibles, remember a la durée de vie la plus courte et oublie les valeurs le plus tôt des quatre fonctions de mémoïsation décrites sur cette page.
Il est donc le mieux adapté pour :
- Créer des objets d'état internes, tels que la position de défilement ou l'état d'animation
- Éviter la recréation coûteuse d'objets lors de chaque recomposition
Toutefois, vous devez éviter :
- de stocker des entrées utilisateur avec
remember, car les objets mémorisés sont oubliés lors des modifications de configuration de l'activité et de l'arrêt du processus initié par le système.
rememberSaveable et rememberSerializable
rememberSaveable et rememberSerializable s'appuient sur remember. Ils ont la durée de vie la plus longue des fonctions de mémoïsation abordées dans ce guide.
En plus de mémoïser les objets de manière positionnelle lors des recompositions, ils peuvent également enregistrer des valeurs afin qu'elles puissent être restaurées lors des recréations d'activité, y compris lors des modifications de configuration et de l'arrêt du processus (lorsque le système arrête le processus de votre application en arrière-plan, généralement pour libérer de la mémoire pour les applications au premier plan ou si l'utilisateur révoque les autorisations de votre application pendant son exécution).
rememberSerializable fonctionne de la même manière que rememberSaveable, mais il est automatiquement compatible avec la persistance des types complexes qui sont sérialisables avec la bibliothèque kotlinx.serialization. Choisissez rememberSerializable si votre type est (ou peut être) marqué avec @Serializable, et rememberSaveable dans tous les autres cas.
Cela fait de rememberSaveable et rememberSerializable des candidats parfaits pour stocker l'état associé à l'entrée utilisateur, y compris l'entrée de champ de texte, la position de défilement, les états de bouton à bascule, etc. Vous devez enregistrer cet état pour vous assurer que l'utilisateur ne perd jamais sa place. En général, vous devez utiliser rememberSaveable ou rememberSerializable pour mémoïser tout état que votre application ne peut pas récupérer à partir d'une autre source de données persistante, telle qu'une base de données.
Notez que rememberSaveable et rememberSerializable enregistrent leurs valeurs mémoïsées en les sérialisant dans un Bundle. Cela a deux conséquences :
- Les valeurs que vous mémoïsez doivent être représentables par un ou plusieurs des types de données suivants : primitives (y compris
Int,Long,Float,Double),Stringou tableaux de l'un de ces types. - Lorsqu'une valeur enregistrée est restaurée, il s'agit d'une nouvelle instance égale à
(
==), mais pas de la même référence (===) que celle utilisée par la composition auparavant.
Pour stocker des types de données plus complexes sans utiliser kotlinx.serialization, vous pouvez implémenter un Saver personnalisé pour sérialiser et désérialiser votre objet dans des types de données compatibles. Notez que Compose comprend les types de données courants tels que State, List, Map, Set, etc. prêts à l'emploi, et les convertit automatiquement en types compatibles à votre place. Voici un exemple de Saver pour une classe Size. Il est implémenté en regroupant toutes les propriétés de Size dans une liste à l'aide de listSaver.
data class Size(val x: Int, val y: Int) { object Saver : androidx.compose.runtime.saveable.Saver<Size, Any> by listSaver( save = { listOf(it.x, it.y) }, restore = { Size(it[0], it[1]) } ) } @Composable fun rememberSize(x: Int, y: Int) { rememberSaveable(x, y, saver = Size.Saver) { Size(x, y) } }
retain
L'retain API se situe entre remember et
rememberSaveable/rememberSerializable en termes de durée de mémoïsation de ses
valeurs. Son nom est différent, car les valeurs conservées ont également un cycle de vie différent de celui de leurs homologues mémorisés.
Lorsqu'une valeur est conservée, elle est à la fois mémoïsée de manière positionnelle et enregistrée dans une
structure de données secondaire dont la durée de vie est distincte et liée à celle de l'application. Une valeur conservée peut survivre aux modifications de configuration sans être sérialisée, mais elle ne peut pas survivre à l'arrêt du processus. Si une valeur n'est pas utilisée après la recréation de la hiérarchie de composition, la valeur conservée est retirée (ce qui équivaut à être oubliée pour retain).
En échange de ce cycle de vie plus court que celui de rememberSaveable, retain est en mesure de conserver des valeurs qui ne peuvent pas être sérialisées, comme les expressions lambda, les flux et les objets volumineux tels que les bitmaps. Par exemple, vous pouvez utiliser retain pour gérer un lecteur multimédia (tel qu'ExoPlayer) afin d'éviter les interruptions de la lecture multimédia lors d'une modification de configuration.
@Composable fun MediaPlayer() { // Use the application context to avoid a memory leak val applicationContext = LocalContext.current.applicationContext val exoPlayer = retain { ExoPlayer.Builder(applicationContext).apply { /* ... */ }.build() } // ... }
retain et ViewModel
Dans leur forme la plus élémentaire, retain et ViewModel offrent des fonctionnalités similaires dans leur capacité la plus couramment utilisée à conserver des instances d'objet lors des modifications de configuration. Le choix d'utiliser retain ou ViewModel dépend du type de valeur que vous conservez, de la manière dont elle doit être limitée et si vous avez besoin de fonctionnalités supplémentaires.
Les ViewModel sont des objets qui encapsulent généralement la communication entre l'UI et les couches de données de votre application. Ils vous permettent de déplacer la logique hors de vos
fonctions composables, ce qui améliore la testabilité. Les ViewModel sont gérés en tant que
singletons dans un ViewModelStore et ont une durée de vie différente de celle des valeurs conservées. Alors qu'un ViewModel reste actif jusqu'à ce que son ViewModelStore soit
détruit, les valeurs conservées sont retirées lorsque le contenu est définitivement supprimé
de la composition (par exemple, pour une modification de configuration, cela signifie qu'
une valeur conservée est retirée si la hiérarchie de l'UI est recréée et que la valeur conservée
n'a pas été consommée après la recréation de la composition).
ViewModel inclut également des intégrations prêtes à l'emploi pour l'injection de dépendances avec Dagger et Hilt, l'intégration avec SavedState et la prise en charge intégrée des coroutines pour le lancement de tâches en arrière-plan. Cela fait de ViewModel un endroit idéal pour lancer des tâches en arrière-plan et des requêtes réseau, interagir avec d'autres sources de données dans votre projet, et éventuellement capturer et conserver l'état de l'UI essentiel qui doit être conservé lors des modifications de configuration dans le ViewModel et survivre à l'arrêt du processus.
retain est mieux adapté aux objets limités à des instances composables spécifiques et qui ne nécessitent pas de réutilisation ni de partage entre des composables frères. Alors que ViewModel est un bon endroit pour stocker l'état de l'UI et effectuer des tâches en arrière-plan, retain est un bon candidat pour stocker des objets pour l'infrastructure de l'UI, tels que les caches, le suivi des impressions et les analyses, les dépendances sur les AndroidView et d'autres objets qui interagissent avec le système d'exploitation Android ou gèrent des bibliothèques tierces telles que les processeurs de paiement ou la publicité.
Pour les utilisateurs avancés qui conçoivent des modèles d'architecture d'application personnalisés en dehors des recommandations de l'architecture d'application Android moderne, retain peut également être utilisé pour créer une API interne de type ViewModel. Bien que la prise en charge des coroutines et de l'état enregistré ne soit pas proposée prête à l'emploi, retain peut servir de bloc de construction pour le cycle de vie de ces composants de type ViewModel avec ces fonctionnalités intégrées. Les spécificités de la conception d'un tel composant ne sont pas abordées dans ce guide.
|
|
|
|---|---|---|
Limitation |
Aucune valeur partagée ; chaque valeur est conservée et associée à un point spécifique de la hiérarchie de composition. La conservation du même type à un autre emplacement agit toujours sur une nouvelle instance. |
|
Destruction |
Lors du retrait définitif de la hiérarchie de composition |
Lorsque le |
Fonctionnalités supplémentaires |
Peut recevoir des rappels lorsque l'objet se trouve ou non dans la hiérarchie de composition |
|
Propriétaire : |
|
|
Cas d'utilisation |
|
|
Combiner retain et rememberSaveable ou rememberSerializable
Parfois, un objet doit avoir une durée de vie hybride à la fois retained et rememberSaveable ou rememberSerializable. Cela peut indiquer que votre
objet doit être un ViewModel, qui peut prendre en charge l'état enregistré, comme décrit dans
le guide Module Saved State pour ViewModel.
Il est possible d'utiliser retain et rememberSaveable ou rememberSerializable simultanément. La combinaison correcte des deux cycles de vie ajoute une complexité significative.
Nous vous recommandons d'utiliser ce modèle dans le cadre de modèles d'architecture plus avancés et personnalisés, et uniquement lorsque toutes les conditions suivantes sont remplies :
- Vous définissez un objet composé d'un mélange de valeurs qui doivent être conservées ou enregistrées (par exemple, un objet qui suit une entrée utilisateur et un cache en mémoire qui ne peut pas être écrit sur le disque).
- Votre état est limité à un composable et ne convient pas à la limitation singleton ou à la durée de vie de
ViewModel.
Dans ce cas, nous vous recommandons de diviser votre classe en trois parties : les données enregistrées, les données conservées et un objet "médiateur" qui n'a pas d'état propre et qui délègue aux objets conservés et enregistrés pour mettre à jour l'état en conséquence. Ce modèle se présente comme suit :
@Composable fun rememberAndRetain(): CombinedRememberRetained { val saveData = rememberSerializable(serializer = serializer<ExtractedSaveData>()) { ExtractedSaveData() } val retainData = retain { ExtractedRetainData() } return remember(saveData, retainData) { CombinedRememberRetained(saveData, retainData) } } @Serializable data class ExtractedSaveData( // All values that should persist process death should be managed by this class. var savedData: AnotherSerializableType = defaultValue() ) class ExtractedRetainData { // All values that should be retained should appear in this class. // It's possible to manage a CoroutineScope using RetainObserver. // See the full sample for details. var retainedData = Any() } class CombinedRememberRetained( private val saveData: ExtractedSaveData, private val retainData: ExtractedRetainData, ) { fun doAction() { // Manipulate the retained and saved state as needed. } }
En séparant l'état par durée de vie, la séparation des responsabilités et du stockage devient très explicite. Il est intentionnel que les données d'enregistrement ne puissent pas être manipulées par les données de conservation, car cela évite un scénario dans lequel une tentative de mise à jour des données d'enregistrement est effectuée lorsque le bundle savedInstanceState a déjà été capturé et ne peut pas être mis à jour. Il permet également de tester des scénarios de recréation en testant vos constructeurs sans appeler Compose ni simuler une recréation d'activité.
Pour obtenir un exemple complet de la mise en œuvre de ce modèle, consultez l'exemple complet (RetainAndSaveSample.kt) pour un exemple complet de
comment ce modèle peut être mis en œuvre.
Mémoïsation positionnelle et mises en page adaptatives
Les applications Android peuvent être compatibles avec de nombreux facteurs de forme, y compris les téléphones, les appareils pliables, les tablettes et les ordinateurs de bureau. Les applications doivent souvent passer d'un facteur de forme à un autre à l'aide de mises en page adaptatives. Par exemple, une application exécutée sur une tablette peut afficher une vue de liste-détails à deux colonnes, mais peut passer d'une liste à une page de détails lorsqu'elle est présentée sur un écran de téléphone plus petit.
Étant donné que les valeurs mémorisées et conservées sont mémoïsées de manière positionnelle, elles ne sont réutilisées que si elles apparaissent au même point dans la hiérarchie de composition. À mesure que vos mises en page s'adaptent à différents facteurs de forme, elles peuvent modifier la structure de votre hiérarchie de composition et entraîner l'oubli de valeurs.
Pour les composants prêts à l'emploi tels que ListDetailPaneScaffold et NavDisplay (à partir de Jetpack Navigation 3), cela ne pose pas de problème et votre état persiste lors des modifications de mise en page. Pour les composants personnalisés qui s'adaptent aux facteurs de forme, assurez-vous que l'état n'est pas affecté par les modifications de mise en page en procédant de l'une des manières suivantes :
- Assurez-vous que les composables avec état sont toujours appelés au même endroit dans la hiérarchie de composition. Implémentez des mises en page adaptatives en modifiant la logique de mise en page au lieu de déplacer des objets dans la hiérarchie de composition.
- Utilisez
MovableContentpour déplacer les composables avec état de manière fluide. Les instances deMovableContentpeuvent déplacer les valeurs mémorisées et conservées de leur ancien emplacement vers leur nouvel emplacement.
Mémoriser les fonctions de fabrique
Bien que les UI Compose soient composées de fonctions composables, de nombreux objets sont utilisés pour la création et l'organisation d'une composition. L'exemple le plus courant est celui des objets composables complexes qui définissent leur propre état, comme LazyList,
qui accepte un LazyListState.
Lorsque vous définissez des objets axés sur Compose, nous vous recommandons de créer une fonction remember pour définir le comportement de mémorisation prévu, y compris la durée de vie et les entrées clés. Cela permet aux consommateurs de votre état de créer en toute confiance des instances dans la hiérarchie de composition qui survivront et seront invalidées comme prévu. Lorsque vous définissez une fonction de fabrique composable, suivez ces consignes :
- Préfixez le nom de la fonction avec
remember. Si l'implémentation de la fonction dépend de l'objetretainedet que l'API n'évoluera jamais pour s'appuyer sur une autre variante deremember, utilisez plutôt le préfixeretainà la place. - Utilisez
rememberSaveableourememberSerializablesi la persistance de l'état est choisie et qu'il est possible d'écrire une implémentationSavercorrecte. - Évitez les effets secondaires ou l'initialisation de valeurs basées sur des
CompositionLocalqui pourraient ne pas être pertinentes pour l'utilisation. N'oubliez pas que l'endroit où votre état est créé n'est peut-être pas celui où il est consommé.
@Composable fun rememberImageState( imageUri: String, initialZoom: Float = 1f, initialPanX: Int = 0, initialPanY: Int = 0 ): ImageState { return rememberSaveable(imageUri, saver = ImageState.Saver) { ImageState( imageUri, initialZoom, initialPanX, initialPanY ) } } data class ImageState( val imageUri: String, val zoom: Float, val panX: Int, val panY: Int ) { object Saver : androidx.compose.runtime.saveable.Saver<ImageState, Any> by listSaver( save = { listOf(it.imageUri, it.zoom, it.panX, it.panY) }, restore = { ImageState(it[0] as String, it[1] as Float, it[2] as Int, it[3] as Int) } ) }