L'état dans Jetpack Compose

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 et rememberSaveable
  • Utilisation des listes et des états avec les API mutableStateListOf et toMutableStateList
  • Utilisation de ViewModel avec Compose

Ce dont vous avez besoin

Recommandé/Facultatif

Objectifs de l'atelier

Vous allez mettre en œuvre une application de bien-être simple :

775940a48311302b.png

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

  1. Pour démarrer un nouveau projet Compose, ouvrez Android Studio.
  2. 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.
  3. Pour un nouveau projet, sélectionnez Empty Activity (Activité vide) dans la liste des modèles disponibles.

Nouveau projet

  1. 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 et app/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 module app. 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'API stringResource 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.

  1. Créez un fichier WellnessScreen.kt qui représente l'écran principal et appelez la fonction WaterCounter :
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   WaterCounter(modifier)
}
  1. Ouvrez MainActivity.kt. Supprimez les composables Greeting et DefaultPreview. Appelez le nouveau composable WellnessScreen dans le bloc setContent 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()
               }
           }
       }
   }
}
  1. 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.

7ed1e6fbd94bff04.jpeg

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 :

f415ca9336d83142.png

  • É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.

e4dfc3bef967e0a1.gif

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.

e4dfc3bef967e0a1.gif

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.

a9d78ead2c8362b6.gif

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.

7d3509d136280b6c.png

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.

677bc0a178670de8.png

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 :

1f8e05f6497ec35f.png

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é.

1a8f4095e384ba01.gif

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 :

6e8b72a529e8dedd.png

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 :

Arborescence de composants représentant l'état initial de l'application, qui affiche 0.

À 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 compteur Text commencent à s'afficher.

Arborescence de composants montrant le changement d'état. Lorsque l'utilisateur clique sur le bouton "Ajouter", le texte s'affiche avec un conseil et le texte comportant le décompte de verres d'eau apparaît.

865af0485f205c28.png

  • Appuyez sur le X du composant WellnessTaskItem. Cette action entraîne une nouvelle recomposition. La valeur showTask est maintenant "false", ce qui signifie que WellnessTaskItem n'est plus affiché.

Arborescence de composants montrant que le composable d'une tâche disparaît lorsque l'utilisateur clique sur le bouton Fermer.

82b5dadce9cca927.png

  • 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. Le Text affichant count, ou tout code lié à WellnessTaskItem ne sont pas appelés et quittent la composition.

ae993e6ddc0d654a.png

  • showTask a été oublié, car l'emplacement du code dans lequel showTask 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).

7624eed0848a145c.png

  • Le composable WellnessTaskItem s'affiche à nouveau, car la valeur précédente de showTask 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.

2c1134ad78e4b68a.gif

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.

bf2e1634eff47697.gif

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.

  1. 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++ })
}

8211bd9e0a4c5db2.png

Si juiceCount est modifié, StatefulCounter est recomposé. Lors de la recomposition, Compose identifie les fonctions qui lisent juiceCount et ne recompose que ces fonctions.

2cb0dcdbe75dcfbf.png

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é.

7fe6ee3d2886abd0.png

  1. 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

  1. 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ément Checkbox. Assurez-vous de hisser l'état checked et le rappel onCheckedChange pour transformer la fonction en fonction sans état.

a0f8724cfd33cb10.png

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")
        }
    }
}
  1. Dans le même fichier, ajoutez une fonction composable WellnessTaskItem avec état qui définit une variable d'état checkedState et la transmet à la méthode sans état du même nom. Ne vous préoccupez pas de onClose 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,
   )
}
  1. 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)
  1. 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.

  1. Dans WellnessTasksList.kt, ajoutez une fonction composable qui va créer la liste. Définissez un LazyColumn 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)
        }
    }
}
  1. Ajoutez la liste à WellnessScreen. Utilisez un Column 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()
   }
}
  1. 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.

f9cbc49c960fd24c.gif

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 :

  1. Cochez n'importe quel élément en haut de cette liste (par exemple, les éléments 1 et 2).
  2. Faites défiler la liste jusqu'en bas pour les retirer de l'écran.
  3. Faites défiler la page jusqu'en haut pour afficher les éléments cochés précédemment.
  4. 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.

a68b5473354d92df.gif

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) }

85796fb49cf5dd16.gif

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.

  1. Ouvrir le fichier WellnessScreen.kt. Déplacez la méthode getWellnessTasks vers ce fichier pour pouvoir l'utiliser. Pour créer la liste, commencez par appeler getWellnessTasks(), puis utilisez la fonction d'extension toMutableStateList 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") }
  1. 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 lambda onCloseTask (et recevez un WellnessTask à supprimer). Transmettez onCloseTask au WellnessTaskItem.

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) })
       }
   }
}
  1. Modifiez WellnessTaskItem : ajoutez la fonction lambda onClose en tant que paramètre au WellnessTaskItem 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.

47f4a64c7e9a5083.png

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.

  1. 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") }
  1. 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.

  1. Ouvrez WellnessScreen. Instanciez le ViewModel wellnessViewModel en appelant viewModel() 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 lambda onCloseTask.
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.

  1. 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)
  1. 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
       }
}
  1. Dans WellnessScreen, indiquez le comportement du onCheckedTask de la liste en appelant la méthode changeTaskChecked 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)
           }
       )
   }
}
  1. Ouvrez WellnessTasksList et ajoutez le paramètre de la fonction lambda onCheckedTask afin de le transmettre au WellnessTaskItem..
@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) }
           )
       }
   }
}
  1. 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")
       }
   }
}
  1. 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.

1d08ebcade1b9302.gif

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 que checkedState devienne MutableState<Boolean> au lieu de Boolean, 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.

e7cc030cd7e8b66f.gif

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

API de référence