Dans une application, l'état correspond à toute valeur susceptible de changer au fil du temps. C'est une définition très large qui recouvre aussi bien une base de données Room qu'une variable dans une classe.
Toutes les applications Android présentent des états à l'utilisateur. Voici quelques exemples d'états dans les applications Android :
- Un snackbar qui indique quand une connexion réseau ne peut être établie.
- Un article de blog et les commentaires associés.
- Des animations de boutons produisant un effet d'ondes en cas d'activation par l'utilisateur.
- Des autocollants qu'un utilisateur peut superposer à une image.
Jetpack Compose vous permet de préciser où et comment vous stockez et utilisez l'état dans une application Android. Ce guide se concentre sur la connexion entre l'état et les composables, ainsi que sur les API proposées par Jetpack Compose pour gérer plus facilement l'état.
État et composition
Compose est déclaratif. Le seul moyen de le mettre à jour est donc d'appeler le même composable avec de nouveaux arguments. Ces arguments sont des représentations de l'état de l'interface utilisateur. Chaque fois qu'un état est mis à jour, une recomposition se produit. Par conséquent, des éléments tels que TextField
ne sont pas automatiquement mis à jour comme dans les vues XML essentielles. Un composable doit être explicitement informé du nouvel état pour être mis à jour en conséquence.
@Composable private fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Hello!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField( value = "", onValueChange = { }, label = { Text("Name") } ) } }
Si vous exécutez cette commande et essayez de saisir du texte, vous verrez que rien ne se passe. En effet, le TextField
ne se met pas à jour. Il se met à jour lorsque son paramètre value
change. Cela est dû au fonctionnement de la composition et de la recomposition dans Compose.
Pour en savoir plus sur la première composition et la recomposition, consultez Approche dans Compose.
État dans les composables
Les fonctions modulables peuvent utiliser l'API remember
pour stocker un objet en mémoire. Une valeur calculée par remember
est stockée dans la composition lors de la première composition, et la valeur stockée est renvoyée lors de la recomposition.
remember
peut être utilisé pour stocker à la fois des objets modifiables et immuables.
mutableStateOf
crée un MutableState<T>
observable, qui est un type observable intégré à l'environnement d'exécution Compose.
interface MutableState<T> : State<T> {
override var value: T
}
Toute modification apportée à value
programme la recomposition de toutes les fonctions modulables qui lisent value
.
Il existe trois façons de déclarer un objet MutableState
dans un composable :
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }
Ces déclarations sont équivalentes et sont fournies sous forme de sucre syntaxique pour différentes utilisations de l'état. Dans le composable que vous écrivez, vous devez choisir la déclaration qui génère le code le plus lisible possible.
La syntaxe by
déléguée nécessite les importations suivantes :
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
Vous pouvez utiliser la valeur mémorisée comme paramètre pour d'autres composables ou même en tant que logique dans des instructions pour modifier les composables à afficher. Par exemple, si vous ne souhaitez pas afficher le message d'accueil si le nom est vide, utilisez l'état dans une instruction if
:
@Composable fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { var name by remember { mutableStateOf("") } if (name.isNotEmpty()) { Text( text = "Hello, $name!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) } OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Name") } ) } }
Bien que remember
vous aide à conserver l'état lors des recompositions, l'état n'est pas conservé en cas de modification de la configuration. Pour cela, vous devez utiliser rememberSaveable
. rememberSaveable
enregistre automatiquement toutes les valeurs susceptibles d'être enregistrées dans un Bundle
. Pour les autres valeurs, vous pouvez transmettre un objet Saver personnalisé.
Autres types d'états acceptés
Compose ne nécessite pas que vous utilisiez MutableState<T>
pour conserver l'état. Il est compatible avec d'autres types observables. Avant de lire un autre type observable dans Compose, vous devez le convertir en State<T>
afin que les composables puissent se recomposer automatiquement en cas de changement d'état.
Compose dispose de fonctions permettant de créer des State<T>
à partir de types observables courants utilisés dans les applications Android. Avant d'utiliser ces intégrations, ajoutez le ou les artefacts appropriés comme décrit ci-dessous :
Flow
:collectAsStateWithLifecycle()
collectAsStateWithLifecycle()
collecte les valeurs d'unFlow
en tenant compte du cycle de vie, ce qui permet d'économiser des ressources d'application. Il représente la dernière valeur émise à partir du conteneurState
Compose. Il est recommandé d'utiliser cette API pour collecter des flux sur les applications Android.La dépendance suivante est requise dans le fichier
build.gradle
(elle doit être 2.6.0-beta01 ou plus récente) :
Kotlin
dependencies {
...
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
}
Groovy
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.7"
}
-
collectAsState
est semblable àcollectAsStateWithLifecycle
, car il collecte également les valeurs d'unFlow
et les convertit enState
Compose.Utilisez
collectAsState
pour un code indépendant de la plate-forme à la place decollectAsStateWithLifecycle
(Android uniquement).collectAsState
étant disponible danscompose-runtime
, aucune dépendance supplémentaire n'est requise. -
observeAsState()
commence à observer cetteLiveData
et représente ses valeurs viaState
.La dépendance suivante est requise dans le fichier
build.gradle
:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.7.5")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.7.5"
}
-
subscribeAsState()
est une fonction d'extension qui transforme les flux réactifs de RxJava2 (par exemple,Single
,Observable
,Completable
) enState
Compose.La dépendance suivante est requise dans le fichier
build.gradle
:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.7.5")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.7.5"
}
-
subscribeAsState()
est une fonction d'extension qui transforme les flux réactifs de RxJava3 (par exemple,Single
,Observable
,Completable
) enState
Compose.La dépendance suivante est requise dans le fichier
build.gradle
:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.7.5")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.7.5"
}
Avec état et sans état
Un composable qui utilise remember
pour stocker un objet crée un état interne et devient un composable avec état. HelloContent
est un exemple de composable avec état, car il contient et modifie son état name
en interne. Cette fonctionnalité peut être utile lorsqu'un appelant n'a pas besoin de contrôler l'état et peut l'utiliser sans avoir à gérer l'état lui-même. Toutefois, les composables dotés d'un état interne ont tendance à être moins réutilisables et plus difficiles à tester.
Un composable sans état est un composable qui ne contient aucun état. Un moyen simple de réaliser une configuration sans état consiste à hisser un état.
Lorsque vous développez des composables réutilisables, vous souhaitez souvent présenter à la fois une version avec état et une version sans état du même composable. La version avec état est pratique pour les appelants qui ne se préoccupent pas de l'état, tandis que la version sans état est requise pour les appelants qui doivent contrôler ou hisser l'état.
Hisser un état
Le hissage d'état dans Compose est un modèle qui consiste à faire remonter un état vers l'appelant d'un composable pour obtenir un composable sans état. Le modèle général du hissage d'état dans Jetpack Compose consiste à remplacer la variable d'état par deux paramètres :
value: T
: valeur actuelle à afficheronValueChange: (T) -> Unit
: événement qui demande la valeur à modifier et oùT
est la nouvelle valeur proposée
Vous n'êtes cependant pas limité à onValueChange
. Si des événements plus spécifiques correspondent au composable, vous devez les définir à l'aide de lambdas.
L'état hissé selon cette méthode présente plusieurs propriétés importantes :
- Source fiable unique : en déplaçant l'état au lieu de le copier, nous garantissons qu'il n'y a qu'une seule source fiable. Cela permet d'éviter les bugs.
- Encapsulé : seuls les composables avec état peuvent modifier leur état. Le processus s'effectue entièrement en interne.
- Partageable : un état hissé peut être partagé avec plusieurs composables. Si vous souhaitez lire
name
dans un autre composable, le hissage vous le permet. - Interceptable : les appelants des composables sans état peuvent décider d'ignorer ou de modifier les événements avant de modifier l'état.
- Dissociation:l'état des composables sans état peut être stocké n'importe où. Par exemple, il est maintenant possible de déplacer
name
dans unViewModel
.
Dans l'exemple suivant, on extrait name
et onValueChange
de HelloContent
, puis on les déplace vers le haut de l'arborescence vers un composable HelloScreen
qui appelle HelloContent
.
@Composable fun HelloScreen() { var name by rememberSaveable { mutableStateOf("") } HelloContent(name = name, onNameChange = { name = it }) } @Composable fun HelloContent(name: String, onNameChange: (String) -> Unit) { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Hello, $name", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") }) } }
En hissant l'état de HelloContent
, il est plus facile de déduire le composable, de le réutiliser dans différentes situations et de le tester. HelloContent
est dissocié du mode de stockage de son état. La dissociation signifie que si vous modifiez ou remplacez HelloScreen
, vous n'avez pas besoin de modifier la façon dont HelloContent
est intégré.
Le modèle où l'état descend et les événements remontent est appelé flux de données unidirectionnel. Dans ce cas, l'état passe de HelloScreen
à HelloContent
et les événements passent de HelloContent
à HelloScreen
. En suivant un flux de données unidirectionnel, vous pouvez dissocier les composables qui affichent l'état dans l'interface utilisateur des parties de votre application qui stockent et modifient l'état.
Pour en savoir plus, consultez la page Où hisser l'état.
Restaurer l'état dans Compose
L'API rememberSaveable
se comporte de la même manière que remember
, car elle conserve l'état lors des recompositions, mais aussi lors de la recréation d'une activité ou d'un processus à l'aide du mécanisme d'enregistrement de l'état d'instance. Cela se produit, par exemple, lorsque l'écran est pivoté.
Comment stocker l'état
Tous les types de données ajoutés à Bundle
sont enregistrés automatiquement. Plusieurs options s'offrent à vous si vous souhaitez enregistrer un élément qui ne peut pas être ajouté à Bundle
.
Parcelize
La solution la plus simple consiste à ajouter l'annotation @Parcelize
à l'objet. L'objet peut être scindé et regroupé. Par exemple, ce code crée un type de données City
scindable qui est enregistré dans l'état.
@Parcelize data class City(val name: String, val country: String) : Parcelable @Composable fun CityScreen() { var selectedCity = rememberSaveable { mutableStateOf(City("Madrid", "Spain")) } }
MapSaver
Si, pour une raison quelconque, @Parcelize
ne convient pas, vous pouvez utiliser mapSaver
pour définir votre propre règle de conversion d'un objet en un ensemble de valeurs que le système peut enregistrer dans le Bundle
.
data class City(val name: String, val country: String) val CitySaver = run { val nameKey = "Name" val countryKey = "Country" mapSaver( save = { mapOf(nameKey to it.name, countryKey to it.country) }, restore = { City(it[nameKey] as String, it[countryKey] as String) } ) } @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
ListSaver
Pour éviter de devoir définir les clés de la carte, vous pouvez également utiliser listSaver
et utiliser ses index comme clés :
data class City(val name: String, val country: String) val CitySaver = listSaver<City, Any>( save = { listOf(it.name, it.country) }, restore = { City(it[0] as String, it[1] as String) } ) @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
Conteneurs d'état dans Compose
Un hissage d'état simple peut être géré dans les fonctions modulables. Si toutefois le nombre d'états à suivre est élevé ou si la logique d'exécution des fonctions modulables se produit, il est recommandé de déléguer les responsabilités logiques et d'état à d'autres classes : les conteneurs d'états.
Pour en savoir plus, consultez la documentation sur le hissage d'état dans Compose ou, plus généralement, la page Conteneurs d'états et état de l'interface utilisateur dans le guide sur l'architecture.
Relancer des calculs de mémorisation en cas de changement de touche
L'API remember
est souvent utilisée conjointement avec MutableState
:
var name by remember { mutableStateOf("") }
Ici, l'utilisation de la fonction remember
permet à la valeur MutableState
de survivre aux recompositions.
En général, remember
utilise un paramètre lambda calculation
. Lorsque remember
est exécuté pour la première fois, il appelle le lambda calculation
et stocke son résultat. Lors de la recomposition, remember
renvoie la valeur qui a été stockée pour la dernière fois.
Outre l'état de mise en cache, vous pouvez également utiliser remember
pour stocker tout objet ou résultat d'une opération de la composition dont l'initialisation ou le calcul est coûteux. Vous préférez peut-être ne pas avoir à répéter ce calcul à chaque recomposition.
La création de l'objet ShaderBrush
, qui est une opération coûteuse, est un bon exemple :
val brush = remember { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) }
remember
stocke la valeur jusqu'à ce qu'il quitte la composition. Cependant, il existe un moyen d'invalider la valeur mise en cache. L'API remember
utilise également un paramètre key
ou keys
. Si l'une de ces touches change, la prochaine fois que la fonction recomposera, remember
invalidera le cache et exécutera à nouveau le bloc lambda de calcul. Ce mécanisme vous permet de contrôler la durée de vie d'un objet dans la composition. Le calcul reste valide jusqu'à ce que les entrées changent, plutôt que jusqu'à ce que la valeur mémorisée quitte la composition.
Les exemples suivants montrent comment ce mécanisme fonctionne.
Dans cet extrait, un élément ShaderBrush
est créé et utilisé en tant que peinture d'arrière-plan d'un composable Box
. remember
stocke l'instance ShaderBrush
, car il est coûteux de la recréer, comme expliqué précédemment. remember
utilise avatarRes
comme paramètre key1
, qui est l'image de fond sélectionnée. Si avatarRes
change, le pinceau se recompose avec la nouvelle image et s'applique de nouveau à l'élément Box
. Cela peut se produire lorsque l'utilisateur sélectionne une autre image comme arrière-plan d'un sélecteur.
@Composable private fun BackgroundBanner( @DrawableRes avatarRes: Int, modifier: Modifier = Modifier, res: Resources = LocalContext.current.resources ) { val brush = remember(key1 = avatarRes) { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) } Box( modifier = modifier.background(brush) ) { /* ... */ } }
Dans l'extrait suivant, l'état est hissé dans une classe de conteneur d'état simple MyAppState
. Il expose une fonction rememberMyAppState
pour initialiser une instance de la classe à l'aide de remember
. L'exposition de ces fonctions pour créer une instance qui survit aux recompositions est un modèle courant dans Compose. La fonction rememberMyAppState
reçoit windowSizeClass
, qui sert de paramètre key
pour remember
. Si ce paramètre change, l'application doit recréer la classe de conteneur d'état simple avec la dernière valeur. Cela peut se produire si, par exemple, l'utilisateur fait pivoter l'appareil.
@Composable private fun rememberMyAppState( windowSizeClass: WindowSizeClass ): MyAppState { return remember(windowSizeClass) { MyAppState(windowSizeClass) } } @Stable class MyAppState( private val windowSizeClass: WindowSizeClass ) { /* ... */ }
Compose utilise l'implémentation de la classe equals pour décider si une touche a changé et invalider la valeur stockée.
Stocker l'état avec des touches au-delà de la recomposition
L'API rememberSaveable
est un wrapper lié à remember
qui peut stocker des données dans un Bundle
. Cette API permet à l'état de survivre non seulement en cas de recomposition, mais également de recréation d'activité et d'arrêt de processus initié par le système.
rememberSaveable
reçoit les paramètres input
de la même manière que remember
reçoit keys
. Le cache n'est pas valide lorsqu'une des entrées change. La prochaine fois que la fonction se recomposera, rememberSaveable
réexécutera le bloc lambda de calcul.
Dans l'exemple suivant, rememberSaveable
stocke userTypedQuery
jusqu'à ce que typedQuery
change :
var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) { mutableStateOf( TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length)) ) }
En savoir plus
Pour en savoir plus sur l'état et Jetpack Compose, consultez les ressources supplémentaires suivantes.
Exemples
Ateliers de programmation
Vidéos
- A Compose state of mind (Le développement d'applications selon Compose)
Blogs
- Effective state management for
TextField
in Compose (Gérer efficacement l'état de TextField dans Compose)
Recommandations personnalisées
- Remarque : Le texte du lien s'affiche lorsque JavaScript est désactivé
- Structurer votre interface utilisateur Compose
- Enregistrer l'état de l'UI dans Compose
- Effets secondaires dans Compose