1. Avant de commencer
Cet atelier de programmation décrit les concepts fondamentaux liés à l'utilisation de l'état dans Jetpack Compose. Il montre comment l'état de l'application détermine ce qui s'affiche dans l'UI, comment Compose met à jour l'UI lorsque l'état change en utilisant différentes API, comment optimiser la structure de nos fonctions composables et comment utiliser des ViewModels dans un environnement Compose.
Conditions préalables
- Bonne connaissance de la syntaxe Kotlin
- Connaissances de base de Compose (vous pouvez commencer par le tutoriel Jetpack Compose).
- Connaissances de base sur les composants d'architecture
ViewModel
.
Points abordés
- Comment envisager le rôle des états et des événements dans une UI Jetpack Compose
- Comment Compose utilise l'état pour afficher des éléments sur un écran
- Qu'est-ce que le hissage d'état ?
- Fonctionnement des fonctions composables avec état et sans état
- Comment Compose suit automatiquement les états avec l'API
State<T>
- Fonctionnement de la mémoire et de l'état interne dans une fonction composable, à l'aide des API
remember
etrememberSaveable
- Utilisation des listes et des états avec les API
mutableStateListOf
ettoMutableStateList
- Utilisation de
ViewModel
avec Compose
Ce dont vous avez besoin
Recommandé/Facultatif
- Consultez Raisonnement dans Compose.
- Suivez l'atelier de programmation Principes de base de Jetpack Compose avant celui-ci. Nous ferons un récapitulatif complet des états dans cet atelier de programmation.
Objectifs de l'atelier
Vous allez mettre en œuvre une application de bien-être simple :
L'application offre deux fonctionnalités principales :
- Un compteur pour suivre votre consommation d'eau.
- Une liste de tâches de bien-être à effectuer tout au long de la journée.
Pour obtenir de l'aide tout au long de cet atelier de programmation, reportez-vous au code suivant :
2. Configuration
Démarrer un nouveau projet Compose
- Pour démarrer un nouveau projet Compose, ouvrez Android Studio.
- Si vous êtes dans la fenêtre Bienvenue dans Android Studio, cliquez sur Démarrer un nouveau projet Android Studio. Si vous avez déjà ouvert un projet Android Studio, sélectionnez File > New > New Project (Fichier > Nouveau > Nouveau projet) dans la barre de menu.
- Pour un nouveau projet, sélectionnez Empty Activity (Activité vide) dans la liste des modèles disponibles.
- Cliquez sur Next (Suivant) et configurez votre projet en l'appelant BasicStateCodelab.
Veillez à sélectionner une minimumSdkVersion ayant au moins le niveau d'API 21, ce qui correspond au niveau d'API minimum accepté par Compose.
Lorsque vous sélectionnez le modèle Activité Compose vide, Android Studio configure les éléments suivants dans votre projet :
- Une classe
MainActivity
configurée avec une fonction composable qui affiche du texte à l'écran. - Le fichier
AndroidManifest.xml
, qui définit les autorisations, les composants et les ressources personnalisées de votre application. - Les fichiers
build.gradle.kts
etapp/build.gradle.kts
contiennent les options et les dépendances nécessaires à Compose.
Solution de l'atelier de programmation
Vous pouvez obtenir le code de solution pour BasicStateCodelab
avec GitHub :
$ git clone https://github.com/android/codelab-android-compose
Vous pouvez également télécharger le dépôt sous forme de fichier ZIP :
Vous trouverez le code de la solution dans le projet BasicStateCodelab
. Nous vous recommandons de suivre l'atelier de programmation étape par étape, à votre rythme, et de consulter la solution si vous le jugez nécessaire. Au cours de cet atelier de programmation, vous découvrirez des extraits de code que vous devez ajouter à votre projet.
3. L'état dans Compose
L'"état" d'une application est une valeur susceptible de changer au fil du temps. Cette définition très large recouvre aussi bien une base de données Room et qu'une variable dans une classe.
Toutes les applications Android présentent des états à l'utilisateur. Voici quelques exemples d'états que vous pouvez trouver dans les applications Android :
- Messages les plus récents reçus dans une application de chat.
- Photo de profil de l'utilisateur.
- Position de défilement dans une liste d'éléments.
Commençons à coder votre appli bien-être.
Par souci de simplicité, lors de cet atelier de programmation :
- Vous pouvez ajouter tous les fichiers Kotlin dans le package
com.codelabs.basicstatecodelab
racine du moduleapp
. Toutefois, dans une application de productivité, les fichiers doivent être structurés de manière logique dans des sous-packages. - Vous allez coder en dur toutes les chaînes intégrées dans des extraits. Dans une application réelle, elles doivent être ajoutées en tant que ressources de chaîne dans le fichier
strings.xml
et référencées à l'aide de l'APIstringResource
de Compose.
La première fonctionnalité que vous devez créer est un compteur de consommation d'eau pour évaluer le nombre de verres d'eau que vous buvez au cours de la journée.
Créez une fonction composable appelée WaterCounter
qui contient un composable Text
affichant le nombre de verres. Le nombre de verres doit être stocké dans une valeur appelée count
, que vous pouvez coder en dur pour le moment.
Créez un fichier WaterCounter.kt
avec la fonction composable WaterCounter
, comme ceci :
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
val count = 0
Text(
text = "You've had $count glasses.",
modifier = modifier.padding(16.dp)
)
}
Créons une fonction composable représentant l'intégralité de l'écran, qui comporte deux sections : le compteur de consommation d'eau et la liste des tâches de bien-être. Pour l'instant, nous nous contenterons d'ajouter notre compteur.
- Créez un fichier
WellnessScreen.kt
qui représente l'écran principal et appelez la fonctionWaterCounter
:
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
WaterCounter(modifier)
}
- Ouvrez
MainActivity.kt
. Supprimez les composablesGreeting
etDefaultPreview
. Appelez le nouveau composableWellnessScreen
dans le blocsetContent
de l'activité, comme suit :
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicStateCodelabTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
WellnessScreen()
}
}
}
}
}
- Si vous exécutez l'application maintenant, l'écran de base du compteur de consommation d'eau s'affiche, avec le nombre de verres d'eau codés en dur.
L'état de la fonction composable WaterCounter
correspond à la variable count
. Cependant, l'utilité d'un état statique est limitée, car il ne peut pas être modifié. Pour y remédier, vous allez ajouter un Button
afin d'augmenter le nombre de verres d'eau que vous buvez et d'effectuer le suivi de la quantité bue tout au long de la journée.
Toute action entraînant la modification de l'état est appelée un événement. Nous aborderons ce point plus en détail dans la section suivante.
4. Les événements dans Compose
Nous avons parlé de l'état comme d'une valeur qui change au fil du temps, comme les derniers messages reçus dans une application de chat. Mais pourquoi l'état est-il mis à jour ? Dans les applications Android, l'état est modifié en réponse à des événements.
Les événements sont des entrées générées depuis l'extérieur ou l'intérieur d'une application, par exemple :
- Un utilisateur interagissant avec l'interface utilisateur en appuyant sur un bouton.
- D'autres facteurs, tels que des capteurs envoyant une nouvelle valeur ou des réponses réseau.
Bien que l'état de l'application propose une description des éléments à afficher dans l'interface utilisateur, les événements constituent le mécanisme qui permet de modifier cet état, entraînant des changements dans l'UI.
Les événements informent une partie d'un programme qu'un changement est survenu. Dans toutes les applications Android, il existe une boucle centrale d'actualisation de l'UI qui se présente ainsi :
- Événement : un événement est généré par l'utilisateur ou une autre partie du programme.
- Modification d'état : un gestionnaire d'événements modifie l'état utilisé par l'UI.
- Affichage d'état : l'UI est actualisée pour afficher le nouvel état.
La gestion de l'état dans Compose permet de comprendre comment les états et les événements interagissent entre eux.
Ajoutez maintenant le bouton qui permettra aux utilisateurs de modifier l'état en ajoutant des verres d'eau.
Accédez à la fonction composable WaterCounter
pour ajouter Button
sous notre libellé Text
. Un Column
vous aidera à aligner verticalement le Text
avec les composables Button
. Vous pouvez déplacer la marge intérieure externe vers le composable Column
et ajouter une marge intérieure supplémentaire en haut de Button
pour la séparer du texte.
La fonction composable Button
reçoit une fonction lambda onClick
, soit l'événement qui se produit lorsque l'utilisateur clique sur le bouton. Vous verrez plus d'exemples de fonctions lambda ultérieurement.
Remplacez count
par var
au lieu de val
pour qu'il devienne modifiable.
import androidx.compose.material3.Button
import androidx.compose.foundation.layout.Column
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count = 0
Text("You've had $count glasses.")
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
Lorsque vous exécutez l'application et que vous cliquez sur le bouton, rien ne se passe. Définir une valeur différente pour la variable count
ne permettra pas à Compose de la détecter comme un changement d'état. Il ne va donc rien se passer. En effet, vous n'avez pas dit à Compose qu'il devrait redessiner l'écran (c'est-à-dire "recomposer" la fonction composable) lors du changement d'état. Vous allez résoudre ce problème à l'étape suivante.
5. La mémoire dans une fonction composable
Les applications de Compose transforment les données en UI en appelant des fonctions composables. Nous appelons "Composition" la description de l'UI créée par Compose lorsqu'elle exécute des composables. En cas de changement d'état, Compose réexécute les fonctions composables concernées avec le nouvel état en créant une UI mise à jour. Ce processus est appelé recomposition. Compose cherche également de quelles données chaque composable a besoin pour ne recomposer que les composants dont les données ont changé, et ignore ceux qui ne sont pas affectés.
Pour ce faire, Compose doit connaître l'état à suivre. Ainsi, lorsqu'il reçoit une mise à jour, il peut planifier la recomposition.
Compose dispose d'un système de suivi d'état spécial qui planifie des recompositions pour tous les composables qui lisent un état particulier. Compose peut ainsi être précis et recomposer uniquement les fonctions composables qui doivent être modifiées, et non l'ensemble de l'interface utilisateur. Pour ce faire, il effectue non seulement le suivi des "écritures" (c'est-à-dire des changements d'état), mais aussi des "lectures" de l'état.
Utilisez les types State
et MutableState
de Compose pour rendre l'état observable par Compose.
Compose effectue le suivi de chaque composable qui lit les propriétés d'état value
et déclenche une recomposition lorsque son élément value
change. Vous pouvez utiliser la fonction mutableStateOf
pour créer un élément MutableState
observable. Elle reçoit une valeur initiale en tant que paramètre encapsulé dans un objet State
, ce qui rend son élément value
observable.
Mettez à jour le composable WaterCounter
afin que count
utilise l'API mutableStateOf
avec 0
comme valeur initiale. Lorsque mutableStateOf
renvoie un type MutableState
, vous pouvez modifier son value
pour mettre à jour l'état, et Compose déclenche une recomposition des fonctions où son attribut value
est lu.
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
// Changes to count are now tracked by Compose
val count: MutableState<Int> = mutableStateOf(0)
Text("You've had ${count.value} glasses.")
Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
Comme indiqué précédemment, toute modification de count
programme une recomposition de toutes les fonctions composables qui lisent automatiquement les valeurs "value
" de count
. Dans ce cas, WaterCounter
est recomposé à chaque fois que l'utilisateur clique sur le bouton.
Si vous exécutez l'application maintenant, vous remarquerez que rien ne se passe pour le moment.
La programmation des recompositions fonctionne correctement. Toutefois, lorsqu'une recomposition se produit, la variable count
est réinitialisée à 0. Nous devons donc pouvoir conserver cette valeur lors des recompositions.
Pour ce faire, nous pouvons utiliser la fonction composable intégrée remember
. Une valeur calculée par remember
est stockée dans la composition lors de la composition initiale, et la valeur stockée est conservée lors des recompositions.
remember
et mutableStateOf
sont généralement utilisés conjointement dans des fonctions composables.
Vous pouvez l'écrire de plusieurs façons équivalentes, comme le montre la documentation sur les états dans Compose.
Modifiez WaterCounter
, en encadrant l'appel de mutableStateOf
avec la fonction composable remember
:
import androidx.compose.runtime.remember
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
val count: MutableState<Int> = remember { mutableStateOf(0) }
Text("You've had ${count.value} glasses.")
Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
Nous pourrions également simplifier l'utilisation de count
en utilisant la délégation des propriétés de Kotlin.
Vous pouvez utiliser le mot clé by pour définir count
en tant que variable. L'ajout des importations getter et setter de la propriété déléguée nous permet de lire et de modifier indirectement count
sans faire à chaque fois explicitement référence à la propriété value
de MutableState
.
WaterCounter
se présente maintenant comme suit :
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
Text("You've had $count glasses.")
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
Dans le composable que vous écrivez, vous devez choisir la syntaxe qui génère le code le plus lisible possible.
Examinons ce que nous avons accompli jusqu'à maintenant :
- Nous avons défini une variable à mémoriser au cours du temps, appelée
count
. - Nous avons créé un texte à afficher dans lequel nous indiquons à l'utilisateur le nombre mémorisé.
- Nous avons ajouté un bouton qui incrémente le nombre mémorisé à chaque clic de l'utilisateur.
Cette organisation forme une boucle de rétroaction avec le flux de données :
- L'état est présenté à l'utilisateur (le nombre actuel est affiché sous forme de texte).
- L'utilisateur génère des événements qui sont combinés à l'état existant pour générer un nouvel état (en cliquant sur le bouton, un événement est ajouté au décompte actuel).
Votre compteur est prêt et opérationnel.
6. L'UI basée sur l'état
Compose est un framework d’interface utilisateur déclaratif. Au lieu de supprimer les composants de l'UI ou de modifier leur visibilité à chaque changement d'état, nous décrivons l'état de l'UI dans des conditions spécifiques. En raison de l'appel d'une recomposition et de la mise à jour de l'UI, les composables peuvent finir par entrer dans la composition ou en sortir.
Cette approche permet d'éviter la mise à jour manuelle des vues, comme avec le système View. Elle est également moins sujette aux erreurs, car vous ne pouvez pas oublier de mettre à jour une vue en fonction d'un nouvel état : cette mise à jour est automatique.
Si une fonction composable est appelée lors de la composition initiale ou dans des recompositions, elle est présente dans la composition. Une fonction composable qui n'est pas appelée, par exemple parce qu'elle est appelée dans une instruction if et que la condition n'est pas remplie, est absente de la composition.
Pour en savoir plus sur le cycle de vie des composables, consultez la documentation.
La sortie de la composition est une arborescence qui décrit l'interface utilisateur.
Vous pouvez inspecter la mise en page générée par Compose à l'aide de l'outil d'inspection de la mise en page d'Android Studio, qui correspond à notre prochaine activité.
Pour illustrer ce point, modifiez votre code pour afficher l'interface utilisateur en fonction de l'état. Ouvrez WaterCounter
et affichez Text
si la valeur de count
est supérieure à 0 :
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
// This text is present if the button has been clicked
// at least once; absent otherwise
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
Exécutez l'application, puis ouvrez l'outil d'inspection de la mise en page d'Android Studio via Outils > Inspection de la mise en page.
Vous verrez un écran divisé, affichant l'arborescence des composants à gauche et un aperçu de l'application à droite.
Naviguez dans l'arborescence en appuyant sur l'élément racine BasicStateCodelabTheme
à gauche de l'écran. Affichez l'arborescence complète en cliquant sur le bouton Tout développer.
Cliquez sur un élément de l'écran à droite pour accéder à l'élément correspondant dans l'arborescence.
Si vous appuyez sur le bouton Ajouter dans l'application :
- Le nombre passe à 1, et l'état change.
- Une recomposition est appelée.
- L'écran est recomposé avec les nouveaux éléments.
Si vous examinez l'arborescence des composants à l'aide de l'outil d'inspection de la mise en page d'Android Studio, vous pouvez maintenant voir le composable Text
:
Il indique quels éléments sont présents dans l'interface utilisateur à un moment précis.
Différentes parties de l'UI peuvent dépendre du même état. Modifiez le Button
afin qu'il soit activé jusqu'à ce que count
atteigne 10 jours, puis qu'il se désactive au-delà (et dès que vous avez atteint votre objectif de la journée). Pour ce faire, utilisez le paramètre enabled
de Button
.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
...
Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
...
}
Exécutez l'application maintenant. Les modifications apportées à l'état count
déterminent si Text
doit être affiché ou non, et si Button
est activé ou désactivé.
7. La fonction Remember dans la composition
remember
stocke des objets dans la composition et les oublie si l'emplacement source où remember
est appelé n'est pas appelé à nouveau lors d'une recomposition.
Pour visualiser ce comportement, vous allez implémenter la fonctionnalité suivante dans l'application : lorsque l'utilisateur a bu au moins un verre d'eau, afficher une tâche de bien-être à réaliser par l'utilisateur et qu'il peut également fermer. Les composables doivent être petits et réutilisables. Créez donc un composable appelé WellnessTaskItem
qui affiche la tâche de bien-être en fonction d'une chaîne reçue sous la forme d'un paramètre, ainsi qu'un bouton Close (Fermer).
Créez un fichier WellnessTaskItem.kt
et ajoutez le code suivant. Vous utiliserez cette fonction composable plus tard dans l'atelier.
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.padding
@Composable
fun WellnessTaskItem(
taskName: String,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.weight(1f).padding(start = 16.dp),
text = taskName
)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
La fonction WellnessTaskItem
reçoit une description de la tâche et une fonction lambda onClose
(tout comme le composable Button
intégré reçoit un onClick
).
WellnessTaskItem
se déroule comme ceci :
Pour améliorer notre application et l'enrichir en fonctionnalités, mettez à jour WaterCounter
pour afficher WellnessTaskItem
lorsque count
est supérieur à 0.
Lorsque count
est supérieur à 0, définissez une variable showTask
qui détermine si la valeur WellnessTaskItem
doit être affichée ou non et initialisez-la sur "true".
Ajoutez une instruction if pour afficher WellnessTaskItem
si showTask
est défini sur "true". Utilisez les API que vous avez apprises dans les sections précédentes pour vous assurer que la valeur showTask
survit aux recompositions.
@Composable
fun WaterCounter() {
Column(modifier = Modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
var showTask by remember { mutableStateOf(true) }
if (showTask) {
WellnessTaskItem(
onClose = { },
taskName = "Have you taken your 15 minute walk today?"
)
}
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, enabled = count < 10) {
Text("Add one")
}
}
}
Utilisez la fonction lambda onClose
de WellnessTaskItem
. Ainsi, lorsque vous appuyez sur le bouton X, la variable showTask
devient false
, et la tâche ne s'affiche plus.
...
WellnessTaskItem(
onClose = { showTask = false },
taskName = "Have you taken your 15 minute walk today?"
)
...
Ajoutez ensuite un Button
avec le texte Effacer le compteur et placez-le à côté du Button
Ajouter. Un Row
permet d'aligner les deux boutons. Vous pouvez également ajouter une marge intérieure à Row
. Lorsque vous appuyez sur le bouton Effacer le compteur, la variable count
est réinitialisée.
Votre fonction composable WaterCounter
devrait se présenter comme suit :
import androidx.compose.foundation.layout.Row
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
var showTask by remember { mutableStateOf(true) }
if (showTask) {
WellnessTaskItem(
onClose = { showTask = false },
taskName = "Have you taken your 15 minute walk today?"
)
}
Text("You've had $count glasses.")
}
Row(Modifier.padding(top = 8.dp)) {
Button(onClick = { count++ }, enabled = count < 10) {
Text("Add one")
}
Button(
onClick = { count = 0 },
Modifier.padding(start = 8.dp)) {
Text("Clear water count")
}
}
}
}
Lorsque vous exécutez l'application, votre écran affiche l'état initial :
À droite s'affiche une version simplifiée de l'arborescence des composants, qui vous aidera à analyser ce qui se passe lors d'un changement d'état. count
et showTask
sont des valeurs mémorisées.
Vous pouvez maintenant suivre ces étapes dans l'application :
- Appuyez sur le bouton Ajouter. Cette opération incrémente
count
(ce qui entraîne une recomposition).WellnessTaskItem
et le compteurText
commencent à s'afficher.
- Appuyez sur le X du composant
WellnessTaskItem
. Cette action entraîne une nouvelle recomposition. La valeurshowTask
est maintenant "false", ce qui signifie queWellnessTaskItem
n'est plus affiché.
- Appuyez sur le bouton Ajouter (une autre recomposition).
showTask
se rappelle que vous avez ferméWellnessTaskItem
dans les recompositions suivantes si vous continuez à ajouter des verres.
- Appuyez sur le bouton Effacer le compteur pour remettre
count
à 0 et provoquer une recomposition. LeText
affichantcount
, ou tout code lié àWellnessTaskItem
ne sont pas appelés et quittent la composition.
showTask
a été oublié, car l'emplacement du code dans lequelshowTask
est appelé n'a pas été appelé. Vous êtes revenu à la première étape.
- Appuyez sur le bouton Ajouter pour définir
count
supérieur à 0 (recomposition).
- Le composable
WellnessTaskItem
s'affiche à nouveau, car la valeur précédente deshowTask
a été oubliée lorsqu'elle a quitté la composition ci-dessus.
Que se passe-t-il si nous souhaitons que showTask
persiste une fois que count
revient à 0, plus longtemps que ce que permet remember
(c'est-à-dire, même si l'emplacement du code remember
n'est pas appelé lors d'une recomposition) ? Nous allons voir comment corriger ces scénarios et découvrir d'autres exemples dans les sections suivantes.
Maintenant que vous savez comment l'interface utilisateur et les états sont réinitialisés lorsqu'ils quittent la composition, effacez votre code et revenez au WaterCounter
que vous aviez au début de cette section :
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
Text("Add one")
}
}
}
8. Restaurer l'état dans Compose
Exécutez l'application, ajoutez des verres d'eau au compteur, puis faites pivoter l'appareil. Assurez-vous que le paramètre "Rotation automatique" est activé sur votre appareil.
Étant donné que l'activité est recréée après une modification de configuration (dans ce cas, l'orientation), l'état enregistré est oublié : le compteur disparaît lorsqu'il revient à 0.
Il en va de même si vous changez de langue, alternez entre le mode sombre et le mode clair, et pour toute autre modification de configuration qui oblige Android à recréer l'activité en cours.
Bien que remember
vous aide à conserver l'état lors des recompositions, il n'est pas conservé lors des modifications de configuration. Vous devez alors utiliser rememberSaveable
au lieu de remember
.
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é. Pour en savoir plus sur la façon de Restaurer l'état dans Compose, consultez la documentation.
Dans WaterCounter
, remplacez remember
par rememberSaveable
:
import androidx.compose.runtime.saveable.rememberSaveable
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
...
var count by rememberSaveable { mutableStateOf(0) }
...
}
Exécutez l'application maintenant et essayez de modifier la configuration. Le compteur doit être enregistré correctement.
La recréation d'activité n'est qu'un des cas d'utilisation de rememberSaveable
. Nous étudierons un autre cas d'utilisation ultérieurement lorsque nous travaillerons avec les listes.
Utilisez remember
ou rememberSaveable
selon l'état de l'application et des besoins de l'expérience utilisateur.
9. Hisser un état
Un composable qui utilise remember
pour stocker un objet contient un état interne, ce qui en fait un composable avec état. Cette fonctionnalité est 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.
Les composables qui ne possèdent aucun état sont appelés composables sans état. Pour créer un composable sans état, rien de plus simple : il suffit d'utiliser le hissage d'é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 repose consiste à remplacer la variable d'état par deux paramètres :
- value: T : la valeur actuelle à afficher
- onValueChange: (T) -> Unit : un événement qui demande la modification de la valeur par une nouvelle valeur T.
où cette valeur représente tout état pouvant être modifié.
L'état hissé selon cette méthode présente plusieurs propriétés importantes :
- Référence unique : en déplaçant l'état au lieu de le dupliquer, nous conservons une source de référence unique. Cela contribue à éviter les bugs.
- Possibilité de partage : un état ainsi hissé peut être partagé avec plusieurs composables.
- Possibilité d'interception : 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 d'une fonction composable sans état peut être stocké n'importe où. Par exemple, dans un ViewModel.
Essayez donc pour WaterCounter
afin qu'il puisse bénéficier de tous les éléments ci-dessus.
Avec et sans état
Lorsque tous les états peuvent être extraits d'une fonction composable, la fonction composable obtenue est appelée sans état.
Refactorisez le composable WaterCounter
en le divisant en deux parties : le compteur avec état et le compteur sans état.
Le rôle de StatelessCounter
est d'afficher count
et d'appeler une fonction lorsque vous incrémentez count
. Pour ce faire, suivez le modèle ci-dessus et transmettez l'état count
(en tant que paramètre de la fonction composable) et une fonction lambda onIncrement
, qui est appelée lorsque l'état doit être incrémenté. StatelessCounter
se déroule comme ceci :
@Composable
fun StatelessCounter(count: Int, onIncrement: () -> Unit, modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
if (count > 0) {
Text("You've had $count glasses.")
}
Button(onClick = onIncrement, Modifier.padding(top = 8.dp), enabled = count < 10) {
Text("Add one")
}
}
}
StatefulCounter
est propriétaire de l'état. Cela signifie qu'il contient l'état count
et le modifie lors de l'appel de la fonction StatelessCounter
.
@Composable
fun StatefulCounter(modifier: Modifier = Modifier) {
var count by rememberSaveable { mutableStateOf(0) }
StatelessCounter(count, { count++ }, modifier)
}
Bien joué ! Vous avez hissé count
de StatelessCounter
à StatefulCounter
.
Vous pouvez connecter ceci à votre application et mettre à jour WellnessScreen
avec StatefulCounter
:
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
StatefulCounter(modifier)
}
Comme nous l'avons vu, le hissage d'état présente certains avantages. Nous allons examiner les variantes de ce code et en expliquer certaines : vous n'avez pas besoin de copier les extraits suivants dans votre application.
- Votre composable sans état peut désormais être réutilisé. Prenons l'exemple suivant.
Pour compter les verres d'eau et de jus, rappelez-vous des waterCount
et des juiceCount
, mais utilisez la même fonction composable StatelessCounter
pour afficher deux états distincts.
@Composable
fun StatefulCounter() {
var waterCount by remember { mutableStateOf(0) }
var juiceCount by remember { mutableStateOf(0) }
StatelessCounter(waterCount, { waterCount++ })
StatelessCounter(juiceCount, { juiceCount++ })
}
Si juiceCount
est modifié, StatefulCounter
est recomposé. Lors de la recomposition, Compose identifie les fonctions qui lisent juiceCount
et ne recompose que ces fonctions.
Lorsque l'utilisateur appuie pour incrémenter juiceCount
, StatefulCounter
se recompose ainsi que StatelessCounter
pour le compteur juiceCount
. Cependant, le StatelessCounter
qui lit waterCount
n'est pas recomposé.
- Votre fonction composable avec état peut fournir le même état pour plusieurs fonctions composables.
@Composable
fun StatefulCounter() {
var count by remember { mutableStateOf(0) }
StatelessCounter(count, { count++ })
AnotherStatelessMethod(count, { count *= 2 })
}
Dans ce cas, si le nombre est mis à jour par StatelessCounter
ou AnotherStatelessMethod
, tout est recomposé, comme attendu.
Comme l'état hissé peut être partagé, assurez-vous de transmettre uniquement l'état dont les composables ont besoin pour éviter des recompositions inutiles et pour améliorer leur réutilisation.
Pour en savoir plus sur l'état et le hissage d'état, consultez la documentation sur l'état dans Compose.
10. Utiliser des listes
Ensuite, ajoutez la deuxième fonctionnalité de votre application : la liste des tâches liées au bien-être. Vous pouvez effectuer deux actions avec les éléments de liste :
- Cocher les éléments de la liste pour marquer la tâche comme terminée.
- Supprimer les tâches de la liste qui ne vous intéressent pas.
Configuration
- Commencez par modifier l'élément de liste. Vous pouvez réutiliser le
WellnessTaskItem
de la section "La fonction Remember dans la composition" et le mettre à jour pour qu'il contienne l'élémentCheckbox
. Assurez-vous de hisser l'étatchecked
et le rappelonCheckedChange
pour transformer la fonction en fonction sans état.
Le composable WellnessTaskItem
pour cette section doit se présenter comme suit :
import androidx.compose.material3.Checkbox
@Composable
fun WellnessTaskItem(
taskName: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp),
text = taskName
)
Checkbox(
checked = checked,
onCheckedChange = onCheckedChange
)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
- Dans le même fichier, ajoutez une fonction composable
WellnessTaskItem
avec état qui définit une variable d'étatcheckedState
et la transmet à la méthode sans état du même nom. Ne vous préoccupez pas deonClose
pour l'instant, vous pouvez transmettre une fonction lambda vide.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@Composable
fun WellnessTaskItem(taskName: String, modifier: Modifier = Modifier) {
var checkedState by remember { mutableStateOf(false) }
WellnessTaskItem(
taskName = taskName,
checked = checkedState,
onCheckedChange = { newValue -> checkedState = newValue },
onClose = {}, // we will implement this later!
modifier = modifier,
)
}
- Créez un fichier
WellnessTask.kt
pour modéliser une tâche contenant un ID et un libellé. Définissez-la en tant que classe de données.
data class WellnessTask(val id: Int, val label: String)
- Pour la liste des tâches, créez un fichier nommé
WellnessTasksList.kt
et ajoutez une méthode qui génère de fausses données :
fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
Notez que dans une application réelle, vous récupérez vos données à partir de votre couche de données.
- Dans
WellnessTasksList.kt
, ajoutez une fonction composable qui va créer la liste. Définissez unLazyColumn
et des éléments à partir de la méthode de liste que vous avez créée. Si vous avez besoin d'aide, consultez la documentation sur les listes.
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.runtime.remember
@Composable
fun WellnessTasksList(
modifier: Modifier = Modifier,
list: List<WellnessTask> = remember { getWellnessTasks() }
) {
LazyColumn(
modifier = modifier
) {
items(list) { task ->
WellnessTaskItem(taskName = task.label)
}
}
}
- Ajoutez la liste à
WellnessScreen
. Utilisez unColumn
pour aligner verticalement la liste avec le compteur existant.
import androidx.compose.foundation.layout.Column
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList()
}
}
- Exécutez l'application et essayez par vous-même ! Vous devriez maintenant pouvoir vérifier les tâches, mais pas les supprimer. Vous en apprendrez plus sur la suppression lors d'une prochaine section.
Restaurer l'état de l'élément dans LazyList
Examinons maintenant de plus près certaines caractéristiques des composables WellnessTaskItem
.
checkedState
appartient à chaque composable WellnessTaskItem
de manière indépendante, comme une variable privée. Lorsque checkedState
change, seule cette instance de WellnessTaskItem
est recomposée, pas toutes les instances WellnessTaskItem
de LazyColumn
.
Procédez comme suit :
- Cochez n'importe quel élément en haut de cette liste (par exemple, les éléments 1 et 2).
- Faites défiler la liste jusqu'en bas pour les retirer de l'écran.
- Faites défiler la page jusqu'en haut pour afficher les éléments cochés précédemment.
- Ils sont décochés.
Comme vous l'avez vu dans une section précédente, un problème subsiste : lorsqu'un élément quitte la composition, l'état mémorisé est oublié. Les éléments se trouvant sur un LazyColumn
quittent la composition lorsque vous les faites défiler. Ils ne sont plus visibles.
Comment résoudre ce problème ? Utilisez de nouveau rememberSaveable
. L'état survivra à l'activité ou à la recréation de processus à l'aide du mécanisme d'enregistrement de l'état d'instance. Grâce à la façon dont rememberSaveable
fonctionne conjointement avec LazyList
, vos éléments peuvent également survivre lorsque vous quittez la composition.
Il vous suffit de remplacer remember
par rememberSaveable
dans votre WellnessTaskItem
avec état. Tout simplement :
import androidx.compose.runtime.saveable.rememberSaveable
var checkedState by rememberSaveable { mutableStateOf(false) }
Modèles courants dans Compose
Remarquez l'implémentation de LazyColumn
:
@Composable
fun LazyColumn(
...
state: LazyListState = rememberLazyListState(),
...
La fonction composable rememberLazyListState
crée un état initial pour la liste à l'aide de rememberSaveable
. Lorsque l'activité est recréée, l'état de défilement est maintenu sans code supplémentaire.
De nombreuses applications doivent réagir et écouter la position de défilement, les modifications de mise en page des éléments et d'autres événements liés à l'état de la liste. Les composants inactifs, comme LazyColumn
ou LazyRow
, sont compatibles avec ce cas d'utilisation grâce au hissage de LazyListState
. Pour en savoir plus sur ce modèle, consultez la documentation sur l'état dans les listes.
Le paramètre d'état associé à une valeur par défaut fournie par une fonction rememberX
publique constitue un modèle courant dans les fonctions composables intégrées. Vous en trouvez un autre exemple dans BottomSheetScaffold
, qui hisse l'état à l'aide de rememberBottomSheetScaffoldState
.
11. MutableList observable
Ensuite, pour ajouter le comportement de suppression d'une tâche de notre liste, vous devez d'abord créer une liste modifiable.
Pour ce faire, vous ne pouvez pas utiliser d'objets modifiables tels que ArrayList<T>
ou mutableListOf,
. Ces types n'informeront pas Compose que les éléments de liste ont changé et ne planifieront pas une recomposition de l'interface utilisateur. Vous avez besoin d'une autre API.
Vous devez créer une instance de MutableList
observable par Compose. Cette structure permet à Compose de suivre les modifications afin de recomposer l'interface utilisateur lors de l'ajout ou de la suppression d'éléments de liste.
Commencez par définir notre MutableList
observable. La fonction d'extension toMutableStateList()
permet de créer un élément MutableList
observable à partir d'un Collection
initial modifiable ou immuable, comme List
.
Vous pouvez également utiliser la méthode de fabrique mutableStateListOf
pour créer la MutableList
observable, puis ajouter les éléments pour votre état initial.
- Ouvrir le fichier
WellnessScreen.kt
. Déplacez la méthodegetWellnessTasks
vers ce fichier pour pouvoir l'utiliser. Pour créer la liste, commencez par appelergetWellnessTasks()
, puis utilisez la fonction d'extensiontoMutableStateList
que vous avez apprise précédemment.
import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
StatefulCounter()
val list = remember { getWellnessTasks().toMutableStateList() }
WellnessTasksList(list = list, onCloseTask = { task -> list.remove(task) })
}
}
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
- Modifiez la fonction composable
WellnessTasksList
en supprimant la valeur par défaut de la liste, car celle-ci est hissée au niveau de l'écran. Ajoutez un nouveau paramètre de fonction lambdaonCloseTask
(et recevez unWellnessTask
à supprimer). TransmettezonCloseTask
auWellnessTaskItem
.
Vous devez encore apporter une modification. La méthode items
reçoit un paramètre key
. Par défaut, l'état de chaque élément pointe vers sa position dans la liste.
Dans une liste modifiable, cela cause des problèmes en cas de modifications au niveau de l'ensemble de données, car les éléments qui changent de position perdent efficacement tout état mémorisé.
Vous pouvez facilement résoudre ce problème en utilisant l'id
de chaque WellnessTaskItem
comme clé pour chaque élément.
Pour en savoir plus sur les clés d'élément dans une liste, consultez la documentation.
WellnessTasksList
se présentera comme suit :
@Composable
fun WellnessTasksList(
list: List<WellnessTask>,
onCloseTask: (WellnessTask) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(modifier = modifier) {
items(
items = list,
key = { task -> task.id }
) { task ->
WellnessTaskItem(taskName = task.label, onClose = { onCloseTask(task) })
}
}
}
- Modifiez
WellnessTaskItem
: ajoutez la fonction lambdaonClose
en tant que paramètre auWellnessTaskItem
avec état et appelez-la.
@Composable
fun WellnessTaskItem(
taskName: String, onClose: () -> Unit, modifier: Modifier = Modifier
) {
var checkedState by rememberSaveable { mutableStateOf(false) }
WellnessTaskItem(
taskName = taskName,
checked = checkedState,
onCheckedChange = { newValue -> checkedState = newValue },
onClose = onClose,
modifier = modifier,
)
}
Bien joué ! Votre fonctionnalité est maintenant terminée, et la suppression d'un élément de liste fonctionne.
Si vous cliquez sur X dans chaque ligne, les événements remontent jusqu'à la liste qui détient l'état, ce qui supprime l'élément de la liste et oblige Compose à recomposer l'écran.
Si vous essayez d'utiliser rememberSaveable()
pour stocker la liste dans WellnessScreen
, une exception d'exécution est générée :
Cette erreur vous indique que vous devez fournir un enregistrement personnalisé. Cependant, vous ne devez pas utiliser rememberSaveable
pour stocker de grandes quantités de données ou des structures de données complexes qui nécessitent une longue sérialisation ou désérialisation.
Des règles similaires s'appliquent lors de l'utilisation de l'élément onSaveInstanceState
de l'activité. Pour en savoir plus, consultez Enregistrer les états de l'interface utilisateur. Pour ce faire, vous avez besoin d'un autre mécanisme de stockage. Pour en savoir plus sur les différentes options de conservation de l'état de l'interface utilisateur, consultez la documentation.
Nous allons ensuite examiner le rôle de ViewModel en tant que titulaire de l'état de l'application.
12. L'état dans ViewModel
L'écran, ou état de l'interface utilisateur, indique ce qui doit s'afficher à l'écran (p. ex. la liste des tâches). Cet état est généralement connecté à d'autres couches de la hiérarchie, car il contient des données d'application.
Alors que l'état de l'interface utilisateur décrit le contenu à afficher à l'écran, la logique d'une application décrit son comportement et doit réagir aux changements d'état. Il existe deux types de logique : le comportement de l'interface utilisateur ou logique de l'interface utilisateur, et la logique métier.
- La logique de l'interface utilisateur concerne l'affichage des changements d'état à l'écran (p. ex. la logique de navigation ou l'affichage de snackbars).
- La logique métier concerne ce qu'il faut faire avec les changements d'état (p. ex. effectuer un paiement ou stocker les préférences d'un utilisateur). Cette logique est généralement placée dans les couches métier ou la couche de données, jamais dans la couche d'UI.
Les ViewModels fournissent l'état de l'interface utilisateur et l'accès à la logique métier située dans les autres couches de l'application. De plus, les ViewModels résistent aux modifications de configuration. Leur durée de vie est donc plus longue que la composition. Ils peuvent suivre le cycle de vie de l'hôte du contenu Compose, c'est-à-dire les activités, les fragments ou la destination d'un graphique de navigation si vous utilisez la navigation Compose.
Pour en savoir plus sur l'architecture et la couche d'interface utilisateur, consultez la documentation sur la couche d'interface utilisateur.
Migrer la liste et supprimer la méthode
Bien que les étapes précédentes vous aient indiqué la façon de gérer l'état directement dans les fonctions composables, il est recommandé de séparer la logique de l'UI et la logique métier de l'état de l'UI et de les migrer vers un ViewModel.
Nous allons migrer l'état de l'interface utilisateur (la liste) vers votre ViewModel, puis nous allons commencer à extraire la logique métier.
- Créez un fichier
WellnessViewModel.kt
pour ajouter votre classe ViewModel.
Déplacez votre "source de données" getWellnessTasks()
vers WellnessViewModel
.
Définissez une variable _tasks
interne en utilisant toMutableStateList
comme précédemment et présentez tasks
sous forme de liste pour qu'il soit impossible de la modifier en dehors de ViewModel.
Implémentez une fonction remove
simple qui délègue la fonction de suppression intégrée de la liste.
import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.ViewModel
class WellnessViewModel : ViewModel() {
private val _tasks = getWellnessTasks().toMutableStateList()
val tasks: List<WellnessTask>
get() = _tasks
fun remove(item: WellnessTask) {
_tasks.remove(item)
}
}
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
- Nous pouvons accéder à ce ViewModel à partir de n'importe quel composable en appelant la fonction
viewModel()
.
Pour utiliser cette fonction, ouvrez le fichier app/build.gradle.kts
, ajoutez la bibliothèque suivante et synchronisez les nouvelles dépendances dans Android Studio :
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:{latest_version}")
Utilisez la version 2.6.2
lorsque vous travaillez avec Android Studio Giraffe. Sinon, cliquez ici pour consulter la dernière version de la bibliothèque.
- Ouvrez
WellnessScreen
. Instanciez le ViewModelwellnessViewModel
en appelantviewModel()
en tant que paramètre du composable Écran, de façon à ce qu'il puisse être remplacé lors du test de ce composable et hissé si nécessaire. Fournissez àWellnessTasksList
la liste des tâches et supprimez la fonction de la fonction lambdaonCloseTask
.
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun WellnessScreen(
modifier: Modifier = Modifier,
wellnessViewModel: WellnessViewModel = viewModel()
) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList(
list = wellnessViewModel.tasks,
onCloseTask = { task -> wellnessViewModel.remove(task) })
}
}
viewModel()
renvoie un ViewModel
existant ou en crée un dans le champ d'application donné. L'instance ViewModel est conservée tant que le champ d'application est actif. Par exemple, si le composable est utilisé dans une activité, viewModel()
renvoie la même instance jusqu'à la fin de l'activité ou la fermeture du processus.
Et voilà ! Vous avez intégré ViewModel à une partie de la logique d'état et métier avec votre écran. Étant donné que l'état est conservé en dehors de la composition et stocké par le ViewModel, les modifications de la liste survivront aux modifications de configuration.
ViewModel ne conserve pas automatiquement l'état de l'application dans tous les scénarios (p. ex. une fin de processus initiée par le système). Pour en savoir plus sur la persistance de l'état de l'interface utilisateur de votre application, consultez la documentation.
Migrer l'état coché
La dernière refactorisation consiste à migrer l'état coché et la logique vers le ViewModel. Ainsi, le code devient plus facile à utiliser et à tester, et tous les états sont gérés par le ViewModel.
- Commencez par modifier la classe de modèle
WellnessTask
afin qu'elle puisse stocker l'état coché et définir la valeur "false" comme valeur par défaut.
data class WellnessTask(val id: Int, val label: String, var checked: Boolean = false)
- Dans ViewModel, implémentez une méthode
changeTaskChecked
qui reçoit une tâche à modifier avec une nouvelle valeur pour l'état coché.
class WellnessViewModel : ViewModel() {
...
fun changeTaskChecked(item: WellnessTask, checked: Boolean) =
_tasks.find { it.id == item.id }?.let { task ->
task.checked = checked
}
}
- Dans
WellnessScreen
, indiquez le comportement duonCheckedTask
de la liste en appelant la méthodechangeTaskChecked
du ViewModel. Les fonctions devraient désormais se présenter comme ceci :
@Composable
fun WellnessScreen(
modifier: Modifier = Modifier,
wellnessViewModel: WellnessViewModel = viewModel()
) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList(
list = wellnessViewModel.tasks,
onCheckedTask = { task, checked ->
wellnessViewModel.changeTaskChecked(task, checked)
},
onCloseTask = { task ->
wellnessViewModel.remove(task)
}
)
}
}
- Ouvrez
WellnessTasksList
et ajoutez le paramètre de la fonction lambdaonCheckedTask
afin de le transmettre auWellnessTaskItem.
.
@Composable
fun WellnessTasksList(
list: List<WellnessTask>,
onCheckedTask: (WellnessTask, Boolean) -> Unit,
onCloseTask: (WellnessTask) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier
) {
items(
items = list,
key = { task -> task.id }
) { task ->
WellnessTaskItem(
taskName = task.label,
checked = task.checked,
onCheckedChange = { checked -> onCheckedTask(task, checked) },
onClose = { onCloseTask(task) }
)
}
}
}
- Nettoyez le fichier
WellnessTaskItem.kt
. Nous n'avons plus besoin d'une méthode avec état, car l'état CheckBox sera hissé au niveau de la liste. Le fichier contient uniquement cette fonction composable :
@Composable
fun WellnessTaskItem(
taskName: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp),
text = taskName
)
Checkbox(
checked = checked,
onCheckedChange = onCheckedChange
)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
- Exécutez l'application et essayez de cocher une tâche, n'importe laquelle. Vous remarquerez qu'il n'est pas encore possible de cocher une tâche.
En effet, le suivi effectué par Compose pour les MutableList
concerne l'ajout et la suppression d'éléments. C'est la raison pour laquelle la suppression fonctionne. Toutefois, les modifications apportées aux valeurs des éléments de ligne (checkedState
ici) ne sont pas prises en compte, sauf si vous lui demandez de les suivre également.
Vous disposez de deux options pour corriger ces erreurs :
- Modifiez notre classe de données
WellnessTask
de sorte quecheckedState
devienneMutableState<Boolean>
au lieu deBoolean
, ce qui oblige Compose à suivre une modification d'élément. - Copiez l'élément que vous allez modifier, supprimez-le de la liste, puis ajoutez-le de nouveau à la liste. Ainsi, Compose pourra suivre la modification apportée à la liste.
Ces deux approches présentent des avantages et des inconvénients. Par exemple, selon l'implémentation de la liste que vous utilisez, la suppression et la lecture de l'élément peuvent se révéler complexe.
Imaginons que vous souhaitiez éviter les opérations de liste potentiellement complexes et rendre checkedState
observable, car il est plus efficace et spécifique à Compose.
Votre nouvelle WellnessTask
pourrait se présenter comme suit :
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
data class WellnessTask(val id: Int, val label: String, val checked: MutableState<Boolean> = mutableStateOf(false))
Comme nous l'avons vu précédemment, vous pouvez utiliser la délégation des propriétés pour simplifier l'utilisation de la variable checked
dans ce cas.
Remplacez WellnessTask
par une classe plutôt que par une classe de données. Faites en sorte que WellnessTask
reçoive une variable initialChecked
avec la valeur par défaut false
dans le constructeur. Ensuite, nous pouvons initialiser la variable checked
avec la méthode de fabrique mutableStateOf
et en utilisant initialChecked
comme valeur par défaut.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
class WellnessTask(
val id: Int,
val label: String,
initialChecked: Boolean = false
) {
var checked by mutableStateOf(initialChecked)
}
Et voilà ! Cette solution fonctionne, et toutes les modifications survivent à la recomposition et aux modifications de configuration.
Tests
Maintenant que la logique métier est refactorisée dans ViewModel au lieu d'être couplée dans des fonctions composables, les tests unitaires sont beaucoup plus simples.
Vous pouvez utiliser des tests d'instrumentation pour vous assurer que votre code Compose et l'état de l'interface utilisateur fonctionnent correctement. Vous pouvez suivre l'atelier de programmation Tester dans Compose pour apprendre à tester votre interface utilisateur Compose.
13. Félicitations
Bien joué ! Vous avez terminé cet atelier de programmation et appris toutes les API de base pour utiliser les états dans une application Jetpack Compose.
Vous avez appris comment fonctionnent l'état et les événements afin d'extraire des composables sans état dans Compose. Vous avez également vu comment Compose utilise les changements d'état pour apporter des modifications à l'UI.
Et maintenant ?
Consultez les autres ateliers de programmation du parcours Compose.
Applications exemples
- JetNews présente les bonnes pratiques expliquées dans cet atelier de programmation.
Plus de documentation
- Raisonnement dans Compose
- États et Jetpack Compose
- Flux de données unidirectionnel dans Jetpack Compose
- Restaurer l'état dans Compose
- Présentation de ViewModel
- Compose et autres bibliothèques