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 dans les 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 perdurer 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 quelle API et quelles sont les bonnes pratiques pour gérer les valeurs mémorisées et conservées dans Compose.
Choisissez la bonne durée de vie
Dans Compose, vous pouvez utiliser plusieurs fonctions pour conserver l'état entre les 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 à la recréation d'activité ? |
❌ |
✅ La même instance ( |
✅ Un objet équivalent ( |
Les valeurs sont-elles conservées après l'arrêt du processus ? |
❌ |
❌ |
✅ |
Types de données acceptés |
Tous |
Ne doit pas référencer d'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 mémorisé, ce qui signifie qu'il est stocké par Compose pour être réutilisé ultérieurement par le composable. Lorsqu'un composable est recomposé, il exécute à nouveau son code, mais tous les appels à remember renvoient leurs valeurs 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émorisation positionnelle. Lorsque les valeurs mémorisées sont mémorisées pour être utilisées dans les 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 de nouveau pour être déplacée vers un autre emplacement sans utiliser le composable key ou MovableContent), ou lorsqu'elles sont appelées avec des paramètres key différents.
Parmi les choix disponibles, remember a la durée de vie la plus courte et oublie les valeurs le plus tôt parmi les quatre fonctions de mémorisation décrites sur cette page.
Il est donc particulièrement adapté aux cas suivants :
- Créer des objets d'état interne, tels que la position de défilement ou l'état d'animation
- Éviter de recréer des objets coûteux à chaque recomposition
Toutefois, vous devez éviter :
- Stocker une entrée 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 d'un processus initié par le système.
rememberSaveable et rememberSerializable
rememberSaveable et rememberSerializable s'appuient sur remember. Elles ont la durée de vie la plus longue des fonctions de mémorisation décrites dans ce guide.
En plus de mémoriser les objets de manière positionnelle lors des recompositions, il peut également enregistrer des valeurs afin qu'elles puissent être restaurées lors des recréations d'activités, y compris à partir des modifications de configuration et de l'arrêt du processus (lorsque le système arrête le processus de votre application lorsqu'elle est 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 accepte automatiquement la persistance des types complexes 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.
rememberSaveable et rememberSerializable sont donc parfaits pour stocker l'état associé aux entrées utilisateur, y compris la saisie dans un champ de texte, la position de défilement, les états d'activation/de désactivation, 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émoriser 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émorisées en les sérialisant dans un Bundle. Cela a deux conséquences :
- Les valeurs que vous mémorisez doivent pouvoir être représentées 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 précédemment par la composition.
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. et les convertit automatiquement en types compatibles. Voici un exemple de Saver pour une classe Size. Elle est implémentée 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'API retain se situe entre remember et rememberSaveable/rememberSerializable en termes de durée de mémorisation de ses valeurs. Il porte un nom différent, car les valeurs conservées ont également un cycle de vie différent de celui de leurs équivalents mémorisés.
Lorsqu'une valeur est conservée, elle est à la fois mémorisé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 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 rememberSaveable, retain peut 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'un changement 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 contre ViewModel
À la base, retain et ViewModel offrent des fonctionnalités similaires dans leur capacité la plus couramment utilisée à conserver les instances d'objets lors des modifications de configuration. Le choix entre retain et ViewModel dépend du type de valeur que vous conservez, de la manière dont elle doit être limitée et de la nécessité ou non de fonctionnalités supplémentaires.
Les ViewModel sont des objets qui encapsulent généralement la communication entre l'UI de votre application et les couches de données. 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 supprimées lorsque le contenu est définitivement supprimé de la composition (par exemple, lors d'un changement de configuration, cela signifie qu'une valeur conservée est supprimée si la hiérarchie de l'interface utilisateur est recréée et que la valeur conservée n'a pas été utilisé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 compatibilité intégrée avec les coroutines pour lancer des tâches en arrière-plan. ViewModel est donc l'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 de votre projet, et éventuellement capturer et conserver l'état de l'UI essentiel à la mission, qui doit être conservé lors des modifications de configuration dans ViewModel et survivre à l'arrêt du processus.
retain est idéal pour les objets limités à des instances de composables spécifiques et qui ne nécessitent pas d'être réutilisés ou partagés entre des composables frères. ViewModel est un bon endroit pour stocker l'état de l'UI et effectuer des tâches en arrière-plan, tandis que retain est un bon candidat pour stocker des objets pour la plomberie de l'UI, comme les caches, l'outil de suivi des impressions et l'analyse de données, les dépendances sur les AndroidView et d'autres objets qui interagissent avec l'OS Android ou gèrent des bibliothèques tierces telles que les sociétés de traitement des paiements 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 compatibilité avec les coroutines et l'état enregistré ne soit pas proposée d'emblée, retain peut servir de composant de base pour le cycle de vie de ces éléments semblables à ViewModel avec ces fonctionnalités intégrées. La conception d'un tel composant n'entre pas dans le cadre de ce guide.
|
|
|
|---|---|---|
Définition du champ d'application |
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 dans un autre emplacement agit toujours sur une nouvelle instance. |
Les |
Destruction |
Lorsqu'il quitte définitivement 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 |
|
|
Combinez retain et rememberSaveable ou rememberSerializable
Parfois, un objet doit avoir une durée de vie hybride de 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 du 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é importante.
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 portée ou à la durée de vie du singleton 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 de sauvegarde ne puissent pas être manipulées par les données de conservation, car cela évite un scénario dans lequel une mise à jour des données de sauvegarde est tentée alors que 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é.
Consultez l'exemple complet (RetainAndSaveSample.kt) pour voir un exemple complet d'implémentation de ce modèle.
Mémorisation positionnelle et mises en page adaptatives
Les applications Android peuvent prendre en charge de nombreux facteurs de forme, y compris les téléphones, les 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 détaillée en liste sur deux colonnes, mais naviguer entre une liste et une page d'informations lorsqu'elle est présentée sur un écran de téléphone plus petit.
Comme les valeurs mémorisées et conservées sont mémorisées de manière positionnelle, elles ne sont réutilisées que si elles apparaissent au même point dans la hiérarchie de la composition. Lorsque 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 (de Jetpack Navigation 3), ce n'est pas un problème, et votre état sera conservé 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 effectuant l'une des opérations 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 les objets dans la hiérarchie de composition.
- Utilisez
MovableContentpour déplacer correctement les composables avec état. Les instances deMovableContentpeuvent déplacer les valeurs mémorisées et conservées de leur ancien emplacement vers le nouvel emplacement.
Se souvenir des fonctions d'usine
Bien que les UI Compose soient constituées de fonctions composables, de nombreux objets entrent en jeu dans 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 de clé. Cela permet aux utilisateurs 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 :
- Ajoutez le préfixe
rememberau nom de la fonction. 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, vous pouvez utiliser 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 ne sont peut-être pas pertinentes pour l'utilisation. N'oubliez pas que l'endroit où votre état est créé n'est pas forcément 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) } ) }