1. Introduction
Dans cet atelier de programmation, vous allez découvrir la notion d'état et la façon dont Jetpack Compose peut l'utiliser et la manipuler.
Avant de nous lancer, il est utile de définir précisément cette notion d'état. En pratique, on entend par "état" toute valeur pouvant évoluer au fil du temps dans une application. C'est une définition très large qui recouvre aussi bien une base de données Room qu'une variable dans une classe.
Toutes les applications Android présentent des états à l'utilisateur. Voici quelques exemples d'états dans les applications Android :
- Un snackbar qui indique quand une connexion réseau ne peut pas être établie
- Un article de blog et les commentaires associés
- Des animations de boutons produisant un effet d'ondes en cas d'activation par l'utilisateur
- Des autocollants qu'un utilisateur peut superposer à une image
Dans cet atelier de programmation, vous apprendrez comment envisager et exploiter les états lorsque vous utilisez Jetpack Compose. Pour ce faire, nous allons créer une application de liste de tâches. À la fin de cet atelier de programmation, vous aurez créé une UI avec état affichant une liste interactive de tâches.
Dans la section suivante, vous découvrirez le flux de données unidirectionnel. Ce système de conception est essentiel pour comprendre comment afficher et gérer les états dans Compose.
Points abordés
- Définition d'un flux de données unidirectionnel
- Comment envisager le rôle des états et des événements dans une UI
- Comment utiliser les éléments
ViewModel
etLiveData
du composant d'architecture dans Compose pour gérer les états - Comment Compose utilise les états pour afficher des éléments sur un écran
- Quand déplacer un état vers un appelant
- Comment utiliser un état interne dans Compose
- Comment utiliser
State<T>
pour intégrer un état dans Compose
Prérequis
- Android Studio Bumblebee
- Connaissance de Kotlin
- Il peut être utile de suivre l'atelier de programmation sur les principes de base de Jetpack Compose avant cet atelier de programmation.
- Connaissances de base de Compose (annotation
@Composable
, entre autres) - Connaissances de base sur la mise en page dans Compose (lignes et colonnes, par exemple)
- Connaissances de base sur les modificateurs (par exemple, Modifier.padding)
- Connaissances de base sur les composants d'architecture
ViewModel
etLiveData
.
Objectifs de l'atelier
- Application de listes de tâches interactive utilisant un flux de données unidirectionnel dans Compose
2. Configuration
Pour télécharger l'application exemple, vous pouvez :
Vous pouvez également cloner le dépôt GitHub à partir de la ligne de commande à l'aide de la commande suivante :
git clone https://github.com/googlecodelabs/android-compose-codelabs.git cd android-compose-codelabs/StateCodelab
À tout moment, vous pouvez exécuter l'un ou l'autre de ces modules dans Android Studio en modifiant la configuration d'exécution dans la barre d'outils.
Ouvrir le projet dans Android Studio
- Dans la fenêtre "Welcome to Android Studio" (Bienvenue dans Android Studio), sélectionnez
Open an existing Project (Ouvrir un projet existant).
- Sélectionnez le dossier
[Download Location]/StateCodelab
. (conseil : assurez-vous de sélectionner le répertoireStateCodelab
contenantbuild.gradle
.) - Une fois qu'Android Studio a importé le projet, vérifiez que vous pouvez exécuter les modules
start
etfinished
.
Analyser le code de départ
Le code de départ contient quatre packages :
examples
: exemples d'activités permettant d'explorer les concepts liés au flux de données unidirectionnel. Vous n'aurez pas à intervenir sur ce package.ui
: contient les thèmes générés automatiquement par Android Studio lors du démarrage d'un nouveau projet Compose. Vous n'aurez pas à intervenir sur ce package.util
: contient du code utile au projet. Vous n'aurez pas à intervenir sur ce package.todo
: package contenant le code de l'écran de liste de tâches que nous allons créer. C'est dans ce package que vous allez apporter des modifications.
Cet atelier de programmation porte essentiellement sur les fichiers du package todo
. Le module start
contient plusieurs fichiers avec lesquels vous devez vous familiariser.
Fichiers fournis dans le package todo
Data.kt
: structures de données utilisées pour représenter un élémentTodoItem
.TodoComponents.kt
: composables réutilisables que vous utiliserez pour créer l'écran de liste de tâches. Vous n'aurez pas à intervenir sur ce fichier.
Fichiers que vous modifierez dans le package todo
TodoActivity.kt
: activité Android utilisant Compose pour afficher un écran de liste de tâches une fois que vous aurez terminé cet atelier de programmation.TodoViewModel.kt
: un élémentViewModel
que vous allez intégrer avec Compose pour créer l'écran de liste de tâches. Vous l'associerez à Compose et l'étendrez pour ajouter des fonctionnalités dans le cadre de cet atelier de programmation.TodoScreen.kt
: implémentation dans Compose d'un écran de liste de tâches que vous allez créer dans cet atelier de programmation.
3. Comprendre ce qu'est un flux de données unidirectionnel
L'actualisation en continu de l'UI
Avant d'accéder à l'application de liste de tâches, nous allons explorer les concepts associés au flux de données unidirectionnel à l'aide du système Android View.
Comment s'effectuent les modifications d'état ? Dans l'introduction, nous avons parlé de l'état comme d'une valeur qui change au fil du temps. Ceci ne décrit qu'un aspect du concept d'état dans une application Android.
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 en dehors de notre application, comme lorsque l'utilisateur appuie sur un bouton appelant un OnClickListener
, quand un EditText
appelle un élément afterTextChanged
ou lorsqu'un accéléromètre envoie une nouvelle valeur.
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.
État non structuré
Avant d'aborder le cas de Compose, examinons les événements et les états dans le système Android View. Pour créer un état "Hello World", nous allons créer un élément Activity
permettant à l'utilisateur de saisir son nom.
Une façon de programmer cela consiste à demander au rappel d'événement de définir directement l'état dans TextView. Le code, qui utilise ViewBinding
, peut alors se présenter comme suit :
HelloCodelabActivity**.kt**
class HelloCodelabActivity : AppCompatActivity() {
private lateinit var binding: ActivityHelloCodelabBinding
var name = ""
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
binding.textInput.doAfterTextChanged {text ->
name = text.toString()
updateHello()
}
}
private fun updateHello() {
binding.helloText.text = "Hello, $name"
}
}
Ce code fonctionne correctement et convient parfaitement pour un petit exemple comme celui-ci. Toutefois, il devient plus difficile à gérer si l'UI devient plus complexe.
Si vous ajoutez un certain nombre d'événements et d'états à une activité ainsi créée, plusieurs problèmes peuvent survenir :
- Tests : étant donné que l'état de l'UI est intégré à
Views
, il peut être difficile de tester ce code. - Modifications partielles de l'état : lorsque l'écran comporte beaucoup d'autres événements, il est facile d'oublier d'actualiser une partie de l'état en réponse à un événement. L'UI présentée à l'utilisateur peut alors être incohérente ou incorrecte.
- Modifications partielles de l'UI : puisque nous mettons à jour manuellement l'UI après chaque changement d'état, le risque d'oubli est élevé. L'utilisateur peut alors voir des données obsolètes dans l'UI actualisée de manière aléatoire.
- Complexité du code : il est difficile d'extraire une partie de la logique lors du codage avec ce système. Le code est alors souvent plus difficile à lire et à interpréter.
Utiliser le flux de données unidirectionnel
Pour résoudre ces problèmes d'états non structurés, nous avons créé les composants d'architecture Android qui contiennent les éléments ViewModel
et LiveData
.
Une classe ViewModel
vous permet d'extraire un état de votre UI et de définir des événements que l'UI peut appeler pour actualiser cet état. Examinons ce qui se produit quand la même activité est codée avec un élément ViewModel
.
HelloCodelabActivity.kt
class HelloCodelabViewModel: ViewModel() {
// LiveData holds state which is observed by the UI
// (state flows down from ViewModel)
private val _name = MutableLiveData("")
val name: LiveData<String> = _name
// onNameChanged is an event we're defining that the UI can invoke
// (events flow up from UI)
fun onNameChanged(newName: String) {
_name.value = newName
}
}
class HelloCodeLabActivityWithViewModel : AppCompatActivity() {
private val helloViewModel by viewModels<HelloCodelabViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
binding.textInput.doAfterTextChanged {
helloViewModel.onNameChanged(it.toString())
}
helloViewModel.name.observe(this) { name ->
binding.helloText.text = "Hello, $name"
}
}
}
Dans cet exemple, nous avons déplacé l'état de Activity
vers ViewModel
. Dans un ViewModel, l'état est représenté par LiveData
. Un LiveData
est un conteneur d'état observable, ce qui signifie qu'il permet à quiconque d'observer les modifications apportées à l'état. Ensuite, dans l'UI, nous utilisons la méthode observe
pour actualiser l'UI chaque fois que l'état change.
L'élément ViewModel
expose également un événement : onNameChanged
. Cet événement est appelé par l'UI en réponse à des événements utilisateur, comme ce qui se produit ici chaque fois que le texte de EditText
change.
Revenons à la boucle d'actualisation de l'UI dont nous avons parlé précédemment, pour voir comment ViewModel
interagit avec les événements et l'état.
- Événement :
onNameChanged
est appelé par l'UI lorsque le texte saisi est modifié. - Modification d'état :
onNameChanged
traite les données, puis définit l'état de_name
. - Affichage d'état : les observateurs de
name
sont appelés, ce qui informe l'UI des changements d'état.
En structurant notre code de cette manière, nous pouvons envisager une "montée" des événements vers ViewModel
. Ensuite, en réponse aux événements, l'élément ViewModel
effectue un traitement, voire une modification de l'état. Une fois l'état actualisé, il "redescend" jusqu'à l'élément Activity
.
Ce modèle est appelé flux de données unidirectionnel. Un flux de données unidirectionnel est un système dans lequel l'état descend et les événements remontent. En structurant notre code de cette manière, nous obtenons certains avantages :
- Facilité des tests : en dissociant l'état de l'UI qui l'affiche, il est plus facile de tester ViewModel et l'activité.
- Encapsulation de l'état : comme l'état peut uniquement être actualisé à un seul endroit (
ViewModel
), il y aura moins de risque d'introduire un bug d'actualisation lorsque l'UI deviendra plus complexe. - Cohérence de l'UI : toutes les modifications d'état sont immédiatement reflétées dans l'UI grâce à l'utilisation de conteneurs d'état observables.
Bien que cette approche ajoute un peu de code, elle permet de gérer plus facilement et plus efficacement les états et les événements complexes à l'aide d'un flux de données unidirectionnel.
Dans la section suivante, nous verrons comment utiliser ce flux de données unidirectionnel dans Compose.
4. Compose et les éléments ViewModel
Dans la section précédente, nous avons exploré le flux de données unidirectionnel dans le système Android View à l'aide de ViewModel
et LiveData
. Nous allons à présent passer à Compose et découvrir comment utiliser le flux de données unidirectionnel dans Compose à l'aide d'éléments ViewModels
.
À la fin de cette section, vous aurez créé cet écran :
Découvrez les composables TodoScreen
Le code que vous avez téléchargé contient plusieurs composables que vous utiliserez et modifierez tout au long de cet atelier.
Ouvrez TodoScreen.kt
et examinez le composable TodoScreen
existant :
TodoScreen.kt
@Composable
fun TodoScreen(
items: List<TodoItem>,
onAddItem: (TodoItem) -> Unit,
onRemoveItem: (TodoItem) -> Unit
) {
/* ... */
}
Pour afficher ce composable, activez le volet de prévisualisation d'Android Studio en cliquant sur l'icône de division d'écran située en haut à droite .
Ce composable affiche une liste de tâches modifiable, mais n'a pas d'état propre. N'oubliez pas que l'état est une valeur susceptible de changer, or aucun des arguments de TodoScreen ne peut être modifié.
items
: liste d'éléments immuables à afficher à l'écranonAddItem
: événement activé quand l'utilisateur demande l'ajout d'une tâcheonRemoveItem
: événement activé quand l'utilisateur demande la suppression d'une tâche
En fait, ce composable peut être sans état. Il affiche uniquement la liste des éléments qui ont été transmis et ne permet pas de la modifier directement. Au lieu de cela, il peut recevoir deux événements onRemoveItem
et onAddItem
qui peuvent demander des modifications.
On peut alors se poser une question : s'il est sans état, comment peut-il afficher une liste modifiable ? Pour ce faire, il utilise une technique appelée hissage d'état. Le hissage d'état est le système qui consiste à faire monter un état pour rendre un composant sans état. Les composants sans état sont plus faciles à tester, ont souvent moins de bugs et offrent davantage de possibilités de réutilisation.
La combinaison de ces paramètres permet à l'appelant de hisser l'état hors de ce composable. Pour voir comment cela fonctionne, nous allons analyser la boucle d'actualisation de l'UI de ce composable.
- Événement : lorsque l'utilisateur demande l'ajout ou la suppression d'un élément,
TodoScreen
appelleonAddItem
ouonRemoveItem
. - Modification d'état : l'appelant de
TodoScreen
peut répondre à ces événements en actualisant l'état - Affichage d'état : lorsque l'état est actualisé,
TodoScreen
est appelé de nouveau avec le nouveauitems
pour que ces éléments s'affichent à l'écran.
L'appelant doit déterminer où et comment conserver la valeur de cet état. Il peut stocker des éléments items
selon la méthode la plus pratique, par exemple en mémoire, ou les lire depuis une base de données Room. TodoScreen
est complètement dissocié de la méthode choisie pour assurer la gestion de l'état.
Définir le composable TodoActivityScreen
Ouvrez TodoViewModel.kt
, puis trouvez un élément ViewModel
existant qui définit une variable d'état et deux événements.
TodoViewModel.kt
class TodoViewModel : ViewModel() {
// state: todoItems
private var _todoItems = MutableLiveData(listOf<TodoItem>())
val todoItems: LiveData<List<TodoItem>> = _todoItems
// event: addItem
fun addItem(item: TodoItem) {
/* ... */
}
// event: removeItem
fun removeItem(item: TodoItem) {
/* ... */
}
}
Nous souhaitons utiliser cet élément ViewModel
pour hisser l'état hors de TodoScreen
. Lorsque ce sera fait, nous aurons créé un système de flux de données unidirectionnel semblable à ceci :
Pour démarrer l'intégration de TodoScreen
dans TodoActivity
, ouvrez TodoActivity.kt
et définissez une nouvelle fonction @Composable
TodoActivityScreen(todoViewModel: TodoViewModel)
, puis appelez-la à partir de setContent
dans onCreate
.
Dans la suite de cette section, nous allons créer TodoActivityScreen
une étape à la fois. Vous pouvez commencer par appeler TodoScreen
avec un état et des événements factices comme ci-après :
TodoActivity.kt
import androidx.compose.runtime.Composable
class TodoActivity : AppCompatActivity() {
private val todoViewModel by viewModels<TodoViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
StateCodelabTheme {
Surface {
TodoActivityScreen(todoViewModel)
}
}
}
}
}
@Composable
private fun TodoActivityScreen(todoViewModel: TodoViewModel) {
val items = listOf<TodoItem>() // in the next steps we'll complete this
TodoScreen(
items = items,
onAddItem = { }, // in the next steps we'll complete this
onRemoveItem = { } // in the next steps we'll complete this
)
}
Ce composable peut servir de pont entre l'état stocké dans ViewModel et le composable TodoScreen
déjà défini dans le projet. Vous pouvez modifier TodoScreen
pour prendre l'élément ViewModel
directement, mais TodoScreen
ne sera pas aussi facile à réutiliser. En optant pour des paramètres plus simples tels que List<TodoItem>
, vous évitez d'associer TodoScreen
à l'emplacement spécifique où l'état est hissé.
Si vous exécutez l'application maintenant, vous verrez qu'elle affiche un bouton, mais cliquer dessus n'a aucun effet. C'est dû au fait que nous n'avons pas encore connecté ViewModel
à TodoScreen
.
Faire circuler les événements vers le haut
Maintenant que nous avons tous les éléments nécessaires (un ViewModel
, un composable TodoActivityScreen
pour faire office de pont et TodoScreen
), connectons-les pour afficher une liste dynamique à l'aide d'un flux de données unidirectionnel.
Dans TodoActivityScreen
, transmettez addItem
et removeItem
à partir de ViewModel
.
TodoActivity.kt
@Composable
private fun TodoActivityScreen(todoViewModel: TodoViewModel) {
val items = listOf<TodoItem>()
TodoScreen(
items = items,
onAddItem = { todoViewModel.addItem(it) },
onRemoveItem = { todoViewModel.removeItem(it) }
)
}
Lorsque TodoScreen
appelle onAddItem
ou onRemoveItem
, nous pouvons transmettre l'appel à l'événement approprié sur notre ViewModel
.
Faire redescendre l'état
Nous avons configuré les événements de notre flux de données unidirectionnel. Nous devons maintenant faire redescendre l'état.
Modifiez TodoActivityScreen
pour observer l'élément LiveData
de todoItems
à l'aide de observeAsState
:
TodoActivity.kt
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@Composable
private fun TodoActivityScreen(todoViewModel: TodoViewModel) {
val items: List<TodoItem> by todoViewModel.todoItems.observeAsState(listOf())
TodoScreen(
items = items,
onAddItem = { todoViewModel.addItem(it) },
onRemoveItem = { todoViewModel.removeItem(it) }
)
}
Cette ligne observe l'élément LiveData
et permet d'utiliser directement la valeur actuelle en tant que List<TodoItem>
.
C'est une ligne qui contient beaucoup d'éléments que nous pouvons analyser un à un :
val items: List<TodoItem>
déclare une variableitems
de typeList<TodoItem>
.todoViewModel.todoItems
est un élémentLiveData<List<TodoItem>
duViewModel
..observeAsState
observe un élémentLiveData<T>
et le convertit en objetState<T>
afin que Compose puisse réagir aux changements de valeur.listOf()
est une valeur initiale permettant d'éviter d'éventuels résultatsnull
avant l'initialisation deLiveData
. Si cette valeur n'était pas transmise, les élémentsitems
correspondraient àList<TodoItem>?
, qui peut avoir une valeur nulle.by
est la syntaxe de délégation de propriété dans Kotlin. Elle nous permet de désencapsuler automatiquement leState<List<TodoItem>>
deobserveAsState
pour en faire unList<TodoItem>
standard.
Exécuter à nouveau l'application
Exécutez de nouveau l'application pour afficher une liste s'actualisant de façon dynamique. Si vous cliquez sur le bouton situé en bas, des tâches sont ajoutées. Si vous cliquez sur une tâche, celle-ci est supprimée.
Dans cette section, nous avons vu comment concevoir un système de flux de données unidirectionnel dans Compose à l'aide de ViewModels
. Nous avons également vu comment utiliser un composable sans état permettant d'afficher une UI avec état à l'aide d'une technique appelée hissage d'état. Nous avons également continué à envisager les UI dynamiques en termes d'état et d'événements.
Dans la section suivante, nous allons apprendre à ajouter de la mémoire aux fonctions modulables.
5. Mémoire dans Compose
Maintenant que nous avons vu comment utiliser Compose avec ViewModel pour créer un flux de données unidirectionnel, nous allons voir comment Compose peut interagir avec un état de façon interne.
Dans la section précédente, vous avez vu comment Compose actualise l'écran en appelant à nouveau des composables. Ce processus est appelé recomposition. Nous avons pu afficher une liste dynamique en appelant à nouveau TodoScreen
.
Dans cette section, nous verrons comment créer des composables avec état.
Dans cette section, nous allons apprendre comment ajouter de la mémoire à une fonction modulable. Cette étape importante nous sera nécessaire pour ajouter un état dans Compose à la section suivante.
Design déjanté
Maquette du concepteur
Dans cette section, un nouveau concepteur de votre équipe vous a envoyé un projet de maquette inspiré d'une tendance en vogue, le design déjanté. Le principe de base du design déjanté consiste à adopter une conception éprouvée, puis à y ajouter des modifications d'apparence aléatoire afin de lui donner un côté intéressant.
Dans cette conception, la couche alpha de chaque icône reçoit une valeur aléatoire comprise entre 0,3 et 0,9.
Ajout d'un nombre aléatoire à un composable
Pour commencer, ouvrez TodoScreen.kt
et trouvez le composable TodoRow
. Ce composable peut décrire une seule ligne de la liste des tâches.
Définissez un nouvel élément val iconAlpha
avec la valeur randomTint()
. Cela correspond à une valeur flottante comprise entre 0,3 et 0,9, comme l'a demandé notre concepteur. Définissez ensuite la teinte de l'icône.
TodoScreen.kt
import androidx.compose.material.LocalContentColor
@Composable
fun TodoRow(todo: TodoItem, onItemClicked: (TodoItem) -> Unit, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.clickable { onItemClicked(todo) }
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(todo.task)
val iconAlpha = randomTint()
Icon(
imageVector = todo.icon.imageVector,
tint = LocalContentColor.current.copy(alpha = iconAlpha),
contentDescription = stringResource(id = todo.icon.contentDescription)
)
}
}
Si vous vérifiez à nouveau l'aperçu, vous constaterez que l'icône a désormais une teinte aléatoire.
Découvrir la recomposition
Exécutez à nouveau l'application pour tester le nouveau design déjanté. Vous remarquerez immédiatement que les teintes ne cessent de changer. Le concepteur reconnaît que malgré l'indéniable beauté du hasard, l'effet n'est pas idéal.
Application dont les icônes changent de teinte en cas de modification de la liste
Que voit-on ici ? Il s'avère que le processus de recomposition appelle randomTint
pour chaque ligne à l'écran pour tout changement dans la liste.
La recomposition désigne le processus qui consiste à appeler de nouveau les composables avec de nouvelles entrées pour actualiser l'arborescence de composition. Dans ce cas, lorsque TodoScreen
est appelé à nouveau avec une nouvelle liste, LazyColumn
recompose tous les enfants à l'écran. Ceci entraîne un nouvel appel de TodoRow
, ce qui génère une nouvelle teinte aléatoire.
Compose génère une arborescence, mais elle n'est pas tout à fait identique à l'arborescence d'UI que vous avez peut-être déjà rencontrée dans le système Android View. Au lieu d'une arborescence de widgets d'UI, Compose génère une arborescence de composables. Nous pouvons visualiser TodoScreen
comme ceci :
Arborescence de TodoScreen
Lorsque Compose exécute la composition pour la première fois, il crée une arborescence de tous les composables appelés. Ensuite, lors de la recomposition, Compose actualise l'arborescence avec les nouveaux composables appelés.
La raison pour laquelle les icônes sont actualisées chaque fois que l'élément TodoRow
est recomposé est que TodoRow
a un effet secondaire masqué. Un effet secondaire désigne une modification visible en dehors de l'exécution d'une fonction modulable.
L'appel de Random.nextFloat()
actualise la variable interne aléatoire utilisée dans un générateur de nombres pseudo-aléatoires. C'est ainsi que Random
renvoie une valeur différente chaque fois que vous demandez un nombre aléatoire.
Présentation de la mémoire pour les fonctions modulables
Nous ne voulons pas que la teinte change à chaque recomposition de TodoRow
. Nous avons donc besoin d'un emplacement permettant de nous souvenir de la teinte utilisée dans la dernière composition. Compose nous permet de stocker des valeurs dans l'arborescence de composition. Nous pouvons donc actualiser TodoRow
pour stocker iconAlpha
dans l'arborescence de composition.
Modifiez TodoRow
et faites précéder l'appel de randomTint
par remember
comme suit :
TodoScreen.kt
val iconAlpha: Float = remember(todo.id) { randomTint() }
Icon(
imageVector = todo.icon.imageVector,
tint = LocalContentColor.current.copy(alpha = iconAlpha),
contentDescription = stringResource(id = todo.icon.contentDescription)
)
En examinant la nouvelle arborescence de composition pour TodoRow
, vous pouvez constater que iconAlpha
y a été ajouté :
Arborescence TodoRow avec remember
Si vous exécutez à nouveau l'application, vous verrez que la teinte n'est plus modifiée à chaque changement dans la liste. Au lieu de cela, lors de la recomposition, la valeur précédente stockée par remember
est renvoyée.
En examinant attentivement l'appel, vous pouvez constater que nous transmettons todo.id
en tant qu'argument de clé (key
).
remember(todo.id) { randomTint() }
Un appel remember contient deux parties :
- Des arguments de clé : il s'agit de la "clé" utilisée par cet appel remember et qui est indiquée entre parenthèses. Ici, cette clé est
todo.id
. - Un calcul : un lambda qui calcule une nouvelle valeur à mémoriser, transmis dans un lambda placé à la suite. Ici, nous calculons une valeur aléatoire avec
randomTint()
.
La première fois que cela se produit, remember appelle toujours randomTint
et mémorise le résultat pour la recomposition suivante. Il enregistre également l'élément todo.id
qui a été transmis. Lors de la recomposition, il ignorera l'appel de randomTint
et renverra la valeur mémorisée, sauf si une nouvelle valeur todo.id
a été transmise à TodoRow
.
La recomposition d'un composable doit être idempotente. En encadrant l'appel de randomTint
avec remember
, nous ignorons l'appel à une valeur aléatoire lors de la recomposition, sauf en cas de modification des tâches de la liste. Par conséquent, TodoRow
n'a aucun effet secondaire et produit toujours le même résultat chaque fois qu'il se recompose avec la même entrée et est idempotent.
Rendre les valeurs mémorisées contrôlables
Si vous exécutez l'application maintenant, vous verrez qu'elle affiche une teinte aléatoire sur chaque icône. Le concepteur est ravi de constater qu'elle respecte les principes du design déjanté et la valide pour livraison.
Cependant, une petite modification de code doit être effectuée avant de boucler le projet. Pour le moment, l'appelant de TodoRow
ne peut pas spécifier la teinte. Cette option peut être intéressante pour plusieurs raisons. Par exemple, un directeur produit peut remarquer cet écran et exiger un correctif pour supprimer la dimension déjantée juste avant la livraison de l'application.
Pour permettre à l'appelant de contrôler cette valeur, il vous suffit de déplacer l'appel de remember vers un argument par défaut dans un nouveau paramètre iconAlpha
.
@Composable
fun TodoRow(
todo: TodoItem,
onItemClicked: (TodoItem) -> Unit,
modifier: Modifier = Modifier,
iconAlpha: Float = remember(todo.id) { randomTint() }
) {
Row(
modifier = modifier
.clickable { onItemClicked(todo) }
.padding(horizontal = 16.dp)
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(todo.task)
Icon(
imageVector = todo.icon.imageVector,
tint = LocalContentColor.current.copy(alpha = iconAlpha),
contentDescription = stringResource(id = todo.icon.contentDescription)
)
}
}
L'appelant obtient désormais le même comportement par défaut : TodoRow
calcule une valeur randomTint
. Mais il peut spécifier la valeur alpha de son choix. En permettant à l'appelant de contrôler alphaTint
, ce composable devient plus réutilisable. Sur un autre écran, il est possible que le concepteur souhaite afficher toutes les icônes avec un alpha de valeur 0,7.
Il existe également un bug très subtil dans notre utilisation de remember
. Essayez d'ajouter suffisamment de tâches pour faire sortir quelques éléments de l'écran en cliquant de manière répétée sur "Add random todo" ("Ajouter une tâche aléatoire"), puis en faisant défiler la page. Lorsque vous faites défiler la page, vous remarquez que la valeur alpha des icônes change chaque fois que le défilement les fait réapparaître à l'écran.
Dans les sections suivantes, nous allons nous pencher sur l'état et le hissage d'état. Ceci vous fournira les outils dont vous avez besoin pour corriger des bugs comme celui que nous venons de décrire.
6. L'état dans Compose
Dans la section précédente, nous avons appris comment fonctionne la mémoire des fonctions modulables. Nous allons maintenant explorer comment utiliser cette mémoire pour ajouter un état à un composable.
Entrée de la liste de tâches (état : développée) 
Entrée de la liste de tâches (état : réduite) 
Notre concepteur a abandonné le design déjanté en faveur d'une approche post-Material Design. La nouvelle interface de saisie des tâches occupe le même espace qu'un en-tête réductible et présente deux états principaux : développé et réduit. La version développée s'affiche lorsque le texte n'est pas vide.
Pour créer cela, nous allons d'abord créer le texte et le bouton. Ensuite, nous ajouterons des icônes à masquage.
La modification de texte dans une UI est une opération avec état. L'utilisateur actualise le texte affiché chaque fois qu'il saisit un caractère ou même lorsqu'il modifie la sélection. Dans le système Android View, cet état est interne à EditText
et exposé via les écouteurs onTextChanged
. Cependant, étant donné que le composable est conçu pour un flux de données unidirectionnel, cela ne conviendrait pas.
Dans Compose, TextField
est un composable sans état. Tout comme le TodoScreen
qui affiche la liste de tâches changeante, un élément TextField
affiche uniquement ce que vous lui dites et génère des événements lorsque l'utilisateur saisit du texte.
Créer un composable TextField avec état
Pour explorer le concept d'état dans Compose, nous allons créer un composant avec état pour l'affichage d'un élément TextField
modifiable.
Pour commencer, ouvrez TodoScreen.kt
et ajoutez la fonction suivante :
TodoScreen.kt
import androidx.compose.runtime.mutableStateOf
@Composable
fun TodoInputTextField(modifier: Modifier) {
val (text, setText) = remember { mutableStateOf("") }
TodoInputText(text, setText, modifier)
}
Cette fonction utilise remember
pour s'ajouter de la mémoire à elle-même, puis stocke un élément mutableStateOf
afin de créer un élément MutableState<String>
qui est un type intégré de Compose fournissant un conteneur d'état observable.
Comme nous allons transmettre immédiatement une valeur et un événement setter à TodoInputText
, nous décomposons l'objet MutableState
en un getter et un setter.
Et voilà, nous avons créé un état interne dans TodoInputTextField
.
Pour le voir en action, définissez un autre composable TodoItemInput
qui affiche TodoInputTextField
et un Button
.
TodoScreen.kt
import androidx.compose.ui.Alignment
@Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
// onItemComplete is an event will fire when an item is completed by the user
Column {
Row(Modifier
.padding(horizontal = 16.dp)
.padding(top = 16.dp)
) {
TodoInputTextField(Modifier
.weight(1f)
.padding(end = 8.dp)
)
TodoEditButton(
onClick = { /* todo */ },
text = "Add",
modifier = Modifier.align(Alignment.CenterVertically)
)
}
}
}
L'élément TodoItemInput
ne comporte qu'un seul paramètre : un événement onItemComplete
. Lorsque l'utilisateur termine une tâche TodoItem
, l'événement est déclenché. Ce système de transmission d'un lambda est la principale méthode pour définir des événements personnalisés dans Compose.
Modifiez également le composable TodoScreen
pour appeler TodoItemInput
dans le TodoItemInputBackground
en arrière-plan, déjà défini dans le projet :
TodoScreen.kt
@Composable
fun TodoScreen(
items: List<TodoItem>,
onAddItem: (TodoItem) -> Unit,
onRemoveItem: (TodoItem) -> Unit
) {
Column {
// add TodoItemInputBackground and TodoItem at the top of TodoScreen
TodoItemInputBackground(elevate = true, modifier = Modifier.fillMaxWidth()) {
TodoItemInput(onItemComplete = onAddItem)
}
...
Essayer TodoItemInput
Comme nous venons de définir un composable d'UI important pour le fichier, il est judicieux de lui ajouter un @Preview
. Cela nous permettra d'explorer ce composable de façon isolée. Les lecteurs de ce fichier pourront aussi le prévisualiser rapidement.
Dans TodoScreen.kt
, ajoutez une nouvelle fonction d'aperçu en bas de l'écran :
TodoScreen.kt
@Preview
@Composable
fun PreviewTodoItemInput() = TodoItemInput(onItemComplete = { })
Vous pouvez maintenant l'exécuter soit dans l'aperçu interactif, soit dans un émulateur pour déboguer ce composable de façon isolée.
Vous verrez alors qu'il affiche bien un champ de texte modifiable permettant à l'utilisateur de modifier du texte. Chaque fois qu'un caractère est saisi, l'état est mis à jour, ce qui déclenche la recomposition du champ TextField
affiché pour l'utilisateur.
Cliquer sur le bouton pour ajouter un élément
Nous souhaitons maintenant que le bouton "Add" (Ajouter) crée bien un élément TodoItem
. Pour ce faire, nous devons accéder à l'élément text
à partir de TodoInputTextField
.
Si vous examinez une partie de l'arborescence de composition de TodoItemInput
, vous constatez que nous enregistrons l'état du texte dans TodoInputTextField
.
Arborescence de composition TodoItemInput (composables intégrés masqués)
Cette structure ne nous permet pas de connecter l'élément onClick
, car onClick
doit accéder à la valeur actuelle de text
. Nous souhaitons présenter l'état de l'élément text
à TodoItemInput
tout en utilisant un flux de données unidirectionnel.
Le système du flux de données unidirectionnel s'applique à la fois à l'architecture générale et à la conception d'un composable unique lorsqu'on utilise Jetpack Compose. Ici, nous souhaitons faire en sorte que les événements remontent et que l'état descende systématiquement.
Il nous faut donc faire descendre l'état de TodoItemInput
et assurer une circulation vers le haut des événements.
Schéma du flux de données unidirectionnel pour TodoItemInput
Pour ce faire, nous devons déplacer l'état du composable enfant, TodoInputTextField
, vers l'élément parent TodoItemInput
.
Arborescence de composition TodoItemInput avec hissage d'état (composables intégrés masqués)
Ce système est appelé hissage d'état. Nous allons "hisser" (ou "remonter") l'état d'un composable afin de rendre ce dernier sans état. Le hissage d'état est le principal système utilisé pour créer des flux de données unidirectionnels dans Compose.
Pour démarrer le hissage d'état, vous pouvez refactoriser tout état interne T
d'un composable afin d'obtenir une paire de paramètres (value: T, onValueChange: (T) -> Unit)
.
Modifiez TodoInputTextField
pour hisser l'état en ajoutant des paramètres (value, onValueChange)
:
TodoScreen.kt
// TodoInputTextField with hoisted state
@Composable
fun TodoInputTextField(text: String, onTextChange: (String) -> Unit, modifier: Modifier) {
TodoInputText(text, onTextChange, modifier)
}
Ce code ajoute les paramètres value
et onValueChange
à TodoInputTextField
. Le paramètre de valeur est text
et le paramètre onValueChange
est onTextChange
.
L'état étant hissé, nous supprimons l'état mémorisé de TodoInputTextField
.
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 pour le texte. Cela contribue à éviter les bugs.
- Encapsulation : seul l'élément
TodoItemInput
peut modifier l'état, tandis que les autres composants peuvent envoyer des événements àTodoItemInput
. En effectuant le hissage de cette manière, nous obtenons un seul composable avec état, même si plusieurs composables utilisent cet état. - Possibilité de partage : un état ainsi hissé peut être partagé comme une valeur immuable avec plusieurs composables. Ici, nous allons utiliser l'état à la fois dans
TodoInputTextField
et dansTodoEditButton
. - Possibilité d'interception : l'élément
TodoItemInput
peut ignorer ou modifier des événements avant de modifier son état. Par exemple,TodoItemInput
pourrait mettre en forme :emoji-codes: en emoji lorsque l'utilisateur saisit du texte. - Dissociation : l'état de
TodoInputTextField
peut être stocké n'importe où. Par exemple, nous pouvons choisir de sauvegarder cet état dans une base de données Room actualisée chaque fois qu'un caractère est saisi sans modifierTodoInputTextField
.
Ajoutez maintenant l'état dans TodoItemInput
et transmettez-le à TodoInputTextField
:
TodoScreen.kt
@Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
val (text, setText) = remember { mutableStateOf("") }
Column {
Row(Modifier
.padding(horizontal = 16.dp)
.padding(top = 16.dp)
) {
TodoInputTextField(
text = text,
onTextChange = setText,
modifier = Modifier
.weight(1f)
.padding(end = 8.dp)
)
TodoEditButton(
onClick = { /* todo */ },
text = "Add",
modifier = Modifier.align(Alignment.CenterVertically)
)
}
}
}
Nous avons maintenant hissé l'état et utilisons la valeur actuelle du texte pour déterminer le comportement du bouton TodoEditButton
. Finalisez le rappel et activez (enable
) le bouton uniquement lorsque le texte n'est pas vide, conformément à la conception :
TodoScreen.kt
// edit TodoItemInput
TodoEditButton(
onClick = {
onItemComplete(TodoItem(text)) // send onItemComplete event up
setText("") // clear the internal text
},
text = "Add",
modifier = Modifier.align(Alignment.CenterVertically),
enabled = text.isNotBlank() // enable if text is not blank
)
Nous utilisons la même variable d'état, text
, dans deux composables différents. Le hissage de l'état nous permet de partager celui-ci comme nous le faisons dans cet exemple. Nous y sommes parvenus tout en faisant uniquement de TodoItemInput
un composable avec état.
Nouvelle exécution
Exécutez de nouveau l'application. Vous pouvez à présent ajouter des tâches. Félicitations ! Vous venez d'apprendre comment ajouter un état à un composable et comment hisser cet état.
Nettoyage du code
Avant de poursuivre, intégrez le champ TodoInputTextField
. Nous venons de l'ajouter dans cette section pour comprendre comment fonctionne le hissage d'état. En examinant le code de TodoInputText
fourni avec l'atelier de programmation, vous constaterez qu'il hisse déjà l'état en suivant les méthodes décrites dans cette section.
Lorsque vous avez terminé, votre élément TodoItemInput
doit se présenter comme suit :
TodoScreen.kt
@Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
val (text, setText) = remember { mutableStateOf("") }
Column {
Row(Modifier
.padding(horizontal = 16.dp)
.padding(top = 16.dp)
) {
TodoInputText(
text = text,
onTextChange = setText,
modifier = Modifier
.weight(1f)
.padding(end = 8.dp)
)
TodoEditButton(
onClick = {
onItemComplete(TodoItem(text))
setText("")
},
text = "Add",
modifier = Modifier.align(Alignment.CenterVertically),
enabled = text.isNotBlank()
)
}
}
}
Dans la section suivante, nous allons poursuivre la création de ce projet et ajouter les icônes. Vous allez vous servir des outils présentés dans cette section pour hisser l'état et créer des UI interactives avec un flux de données unidirectionnel.
7. UI dynamique basée sur l'état
Dans la section précédente, vous avez appris comment ajouter un état à un composable et comment utiliser le hissage d'état pour rendre sans état un composable utilisant un état.
Nous allons maintenant découvrir comment créer une UI dynamique basée sur l'état. Pour revenir à la maquette du concepteur, nous devons afficher la ligne des icônes chaque fois que le texte n'est pas vide.
Entrée d'une tâche (état : développée – texte non vide) 
Entrée d'une tâche (état : réduite – texte vide) 
Déduire la valeur iconsVisible de l'état
Ouvrez TodoScreen.kt
, puis créez une variable d'état pour contenir l'icône (icon
) sélectionnée actuellement et une nouvelle valeur val
pour iconsVisible
renvoyant "True" chaque fois que le texte n'est pas vide.
TodoScreen.kt
@Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
val (text, setText) = remember { mutableStateOf("") }
val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)}
val iconsVisible = text.isNotBlank()
// ...
Nous avons ajouté un deuxième élément d'état, icon
, qui contient l'icône actuellement sélectionnée.
La valeur iconsVisible
n'ajoute pas d'état à TodoItemInput
. TodoItemInput
ne peut pas le modifier directement. Il est entièrement basé sur la valeur de text
. Quelle que soit la valeur de text
dans cette recomposition, iconsVisible
sera défini en conséquence et nous pourrons l'utiliser pour afficher l'UI souhaitée.
Nous pourrions ajouter un état supplémentaire à TodoItemInput
pour contrôler les cas où les icônes sont visibles, mais si vous examinez attentivement la spécification, la visibilité est entièrement basée sur le texte saisi. Si nous avions défini deux états, ils risqueraient d'évoluer de façon indépendante.
Nous préférons disposer d'une référence unique (Single Source of Truth). Dans ce composable, il suffit de définir text
sur un état, et iconsVisible
peut être basé sur text
.
Poursuivez la modification de TodoItemInput
afin d'afficher AnimatedIconRow
en fonction de la valeur de iconsVisible
. Si iconsVisible
est défini sur "True", affichons un élément AnimatedIconRow
, dans le cas contraire, affichons un espace avec 16.dp
.
TodoScreen.kt
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
@Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
val (text, setText) = remember { mutableStateOf("") }
val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)}
val iconsVisible = text.isNotBlank()
Column {
Row( /* ... */ ) {
/* ... */
}
if (iconsVisible) {
AnimatedIconRow(icon, setIcon, Modifier.padding(top = 8.dp))
} else {
Spacer(modifier = Modifier.height(16.dp))
}
}
}
Si vous exécutez à nouveau l'application maintenant, vous verrez que les icônes s'animent lorsque vous saisissez du texte.
Ici, nous modifions de façon dynamique l'arborescence de composition en fonction de la valeur de iconsVisible
. Voici un diagramme de l'arborescence de composition pour les deux états.
Ce type de logique d'affichage conditionnel équivaut à la valeur GONE pour la visibilité dans le système Android View.
Arborescence de composition de la fonction TodoItemInput lorsque la valeur d'iconVisible change
Si vous exécutez à nouveau l'application, la ligne de l'icône s'affiche correctement. Cependant, si vous cliquez sur "Add" (Ajouter), l'icône ne s'affiche pas dans la ligne de tâche ajoutée. En effet, nous n'avons pas actualisé notre événement afin qu'il transmette le nouvel état de l'icône. Voyons comment procéder pour ce faire.
Actualiser l'événement pour utiliser l'icône
Modifiez TodoEditButton
dans TodoItemInput
pour utiliser le nouvel état de l'élément icon
dans l'écouteur onClick
.
TodoScreen.kt
TodoEditButton(
onClick = {
onItemComplete(TodoItem(text, icon))
setIcon(TodoIcon.Default)
setText("")
},
text = "Add",
modifier = Modifier.align(Alignment.CenterVertically),
enabled = text.isNotBlank()
)
Vous pouvez utiliser le nouvel état icon
directement dans l'écouteur onClick
. Nous le réinitialisons à sa valeur par défaut lorsque l'utilisateur saisit une tâche TodoItem
.
Si vous exécutez l'application maintenant, elle affiche une tâche interactive avec des boutons animés. Bravo !
Terminer la conception avec imeAction
Lorsque vous montrez l'application au concepteur, il vous indique qu'elle doit envoyer la tâche à réaliser à l'aide de l'action IME du clavier. Cela correspond au bouton bleu situé en bas à droite :
Clavier Android avec ImeAction.Done
TodoInputText
vous permet de répondre à imeAction avec son événement onImeAction
.
Nous tenons à ce que onImeAction
se comporte exactement de la même façon que TodoEditButton
. Nous pourrions dupliquer le code, mais il serait difficile de le gérer dans la durée, car on risquerait de n'actualiser qu'un seul des événements.
Extrayons l'événement dans une variable. Cela nous permettra de l'utiliser à la fois pour le onImeAction
de TodoInputText
et pour le onClick
de TodoEditButton
.
Modifiez à nouveau TodoItemInput
pour déclarer une nouvelle fonction lambda submit
qui gère l'utilisateur effectuant une action d'envoi. Transmettez ensuite la fonction lambda nouvellement définie à TodoInputText
et à TodoEditButton
.
TodoScreen.kt
@Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
val (text, setText) = remember { mutableStateOf("") }
val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)}
val iconsVisible = text.isNotBlank()
val submit = {
onItemComplete(TodoItem(text, icon))
setIcon(TodoIcon.Default)
setText("")
}
Column {
Row(Modifier
.padding(horizontal = 16.dp)
.padding(top = 16.dp)
) {
TodoInputText(
text = text,
onTextChange = setText,
modifier = Modifier
.weight(1f)
.padding(end = 8.dp),
onImeAction = submit // pass the submit callback to TodoInputText
)
TodoEditButton(
onClick = submit, // pass the submit callback to TodoEditButton
text = "Add",
modifier = Modifier.align(Alignment.CenterVertically),
enabled = text.isNotBlank()
)
}
if (iconsVisible) {
AnimatedIconRow(icon, setIcon, Modifier.padding(top = 8.dp))
} else {
Spacer(modifier = Modifier.height(16.dp))
}
}
}
Si vous le souhaitez, vous pouvez extraire la logique de cette fonction. Toutefois, ce composable fonctionne de manière satisfaisante. Nous allons donc en rester là.
C'est l'un des principaux avantages de Compose. Comme vous déclarez votre UI dans Kotlin, vous pouvez créer toutes les abstractions nécessaires pour rendre le code découplé et réutilisable.
Pour gérer les actions avec le clavier, TextField
fournit deux paramètres :
keyboardOptions
: pour activer l'affichage de l'action IME "Done".keyboardActions
: pour spécifier l'action à déclencher en réponse à des actions IME spécifiques. Dans ce cas, lorsque l'utilisateur appuie sur "DONE", nous souhaitons appelersubmit
et masquer le clavier.
Pour contrôler le clavier virtuel, nous allons utiliser LocalSoftwareKeyboardController.current
. Comme il s'agit d'une API expérimentale, nous devons annoter la fonction avec @OptIn(ExperimentalComposeUiApi::class)
.
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun TodoInputText(
text: String,
onTextChange: (String) -> Unit,
modifier: Modifier = Modifier,
onImeAction: () -> Unit = {}
) {
val keyboardController = LocalSoftwareKeyboardController.current
TextField(
value = text,
onValueChange = onTextChange,
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent),
maxLines = 1,
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = {
onImeAction()
keyboardController?.hide()
}),
modifier = modifier
)
}
Exécutez à nouveau l'application pour essayer les nouvelles icônes.
Exécutez de nouveau l'application. Vous verrez que les icônes s'affichent et se masquent automatiquement lorsque le texte change d'état. Vous pouvez également modifier la sélection d'icônes. Lorsque vous appuyez sur le bouton "Add" (Ajouter), une nouvelle tâche TodoItem est générée en fonction des valeurs saisies.
Félicitations, vous avez appris comment l'état fonctionne dans Compose, comment hisser un état et comment créer des UI dynamiques basées sur l'état.
Dans les sections suivantes, nous verrons comment s'y prendre pour créer des composants réutilisables qui interagissent avec l'état.
8. Extraire des composables sans état
Cette fois, le concepteur a opté pour une nouvelle approche. Oubliés le design déjanté et l'approche post-Material Design. Sa vision se définit aujourd'hui comme "interactive néo-moderne". Vous lui avez demandé ce que cela signifie. Sa réponse, où il était question d'emoji, n'était pas très claire. Quoi qu'il en soit, voici les maquettes.
Maquette pour le mode Édition
Le concepteur indique qu'il réutilise la même UI pour les entrées, avec un remplacement des boutons par des emoji "enregistrer" et "fait".
À la fin de la section précédente, TodoItemInput
était un composable avec état. Ce n'était pas un problème pour ajouter des tâches, mais comme il s'agit maintenant d'un éditeur, il devra être compatible avec le hissage d'état.
Dans cette section, vous allez apprendre à extraire l'état d'un composable avec état pour le rendre sans état. Nous pourrons ainsi réutiliser le même composable pour ajouter des tâches et les modifier.
Convertir TodoItemInput en composable sans état
Pour commencer, nous devons hisser l'état de TodoItemInput
. Où pouvons-nous le stocker ? Nous pourrions l'intégrer directement dans TodoScreen
, mais cet élément fonctionne déjà correctement avec un état interne et un événement terminé. Il n'y a aucune raison de changer cette API.
Divisons plutôt le composable en deux : un composable avec état, l'autre sans état.
Ouvrez TodoScreen.kt
et divisez TodoItemInput
en deux composables, puis renommez le composable avec état en TodoItemEntryInput
, car il ne servira qu'à saisir de nouvelles tâches TodoItems
.
TodoScreen.kt
@Composable
fun TodoItemEntryInput(onItemComplete: (TodoItem) -> Unit) {
val (text, setText) = remember { mutableStateOf("") }
val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)}
val iconsVisible = text.isNotBlank()
val submit = {
onItemComplete(TodoItem(text, icon))
setIcon(TodoIcon.Default)
setText("")
}
TodoItemInput(
text = text,
onTextChange = setText,
icon = icon,
onIconChange = setIcon,
submit = submit,
iconsVisible = iconsVisible
)
}
@Composable
fun TodoItemInput(
text: String,
onTextChange: (String) -> Unit,
icon: TodoIcon,
onIconChange: (TodoIcon) -> Unit,
submit: () -> Unit,
iconsVisible: Boolean
) {
Column {
Row(
Modifier
.padding(horizontal = 16.dp)
.padding(top = 16.dp)
) {
TodoInputText(
text,
onTextChange,
Modifier
.weight(1f)
.padding(end = 8.dp),
submit
)
TodoEditButton(
onClick = submit,
text = "Add",
modifier = Modifier.align(Alignment.CenterVertically),
enabled = text.isNotBlank()
)
}
if (iconsVisible) {
AnimatedIconRow(icon, onIconChange, Modifier.padding(top = 8.dp))
} else {
Spacer(modifier = Modifier.height(16.dp))
}
}
}
Cette transformation est très importante à comprendre lors de l'utilisation de Compose. Nous avons créé un composable avec état TodoItemInput
et l'avons divisé en deux composables. Un avec état (TodoItemEntryInput
) et un sans état (TodoItemInput
).
Le composable sans état contient tout le code associé à l'UI, et le composable avec état n'a aucun code lié à l'UI. Cela nous permet de rendre le code de l'UI réutilisable dans les situations où il serait nécessaire de sauvegarder l'état d'une autre manière.
Exécuter à nouveau l'application
Exécutez à nouveau l'application pour vérifier que la saisie de la liste de tâches fonctionne toujours.
Félicitations, vous avez réussi à extraire un composable sans état d'un composable avec état sans modifier son API.
Dans la section suivante, nous verrons comment cela nous permet de réutiliser la logique de l'UI dans différents emplacements sans associer l'UI à l'état.
9. Utiliser l'état dans ViewModel
Pour finaliser la maquette interactive néo-moderne de notre concepteur, nous allons ajouter un état représentant la tâche en cours de modification.
Maquette pour le mode Édition
Nous devons maintenant choisir où ajouter l'état pour cet éditeur. Nous pourrions créer un autre composable "TodoRowOrInlineEditor
" avec état pour la gestion de l'affichage ou de la modification d'un élément, mais nous ne voulons afficher qu'un éditeur à la fois. Si l'on examine la maquette, on constate que la partie supérieure change également en mode Édition. Il va donc falloir hisser un état pour en permettre le partage.
Arborescence d'état pour TodoActivity
Étant donné que TodoItemEntryInput
et TodoInlineEditor
doivent tous deux connaître l'état actuel de l'éditeur pour activer le masquage d'entrée en haut de l'écran, nous devons hisser l'état jusqu'au niveau de TodoScreen
au minimum. L'écran est le composable de plus bas niveau dans la hiérarchie qui soit un parent commun à tous les composables ayant besoin de connaître l'état de l'éditeur.
Cependant, comme l'éditeur est dérivé de la liste et qu'il va la modifier, il est préférable de le conserver à proximité de celle-ci. Nous devons hisser l'état au niveau où il est susceptible d'être modifié. La liste se trouve dans TodoViewModel
. C'est donc là que nous l'ajouterons.
Convertir TodoViewModel pour utiliser mutableStateListOf
Dans cette section, vous allez ajouter un état pour l'éditeur dans TodoViewModel
. Dans la section suivante, vous l'utiliserez pour créer une fonctionnalité de modification directe.
En parallèle, nous essaierons d'utiliser mutableStateListOf
dans un ViewModel
afin de voir comment cela simplifie le code de l'état par rapport à LiveData<List>
lors du ciblage de Compose.
mutableStateListOf
permet de créer une instance observable de MutableList
. Cela signifie que nous pouvons utiliser des éléments todoItems de la même manière qu'une liste MutableList, en éliminant la surcharge liée à l'utilisation de LiveData<List>
.
Ouvrez TodoViewModel.kt
et remplacez les éléments todoItems
existants par une liste mutableStateListOf
:
TodoViewModel.kt
import androidx.compose.runtime.mutableStateListOf
class TodoViewModel : ViewModel() {
// remove the LiveData and replace it with a mutableStateListOf
//private var _todoItems = MutableLiveData(listOf<TodoItem>())
//val todoItems: LiveData<List<TodoItem>> = _todoItems
// state: todoItems
var todoItems = mutableStateListOf<TodoItem>()
private set
// event: addItem
fun addItem(item: TodoItem) {
todoItems.add(item)
}
// event: removeItem
fun removeItem(item: TodoItem) {
todoItems.remove(item)
}
}
La déclaration de todoItems
est courte et capture le même comportement que la version LiveData
.
// state: todoItems
var todoItems = mutableStateListOf<TodoItem>()
private set
En spécifiant private set
, nous limitons les écritures à cet objet d'état à un setter privé visible uniquement dans ViewModel
.
Actualiser TodoActivityScreen pour utiliser le nouveau ViewModel
Ouvrez TodoActivity.kt
et actualisez TodoActivityScreen
pour utiliser le nouveau ViewModel
.
TodoActivity.kt
@Composable
private fun TodoActivityScreen(todoViewModel: TodoViewModel) {
TodoScreen(
items = todoViewModel.todoItems,
onAddItem = todoViewModel::addItem,
onRemoveItem = todoViewModel::removeItem
)
}
Exécutez de nouveau l'application. Vous constaterez qu'elle fonctionne avec le nouveau ViewModel. Vous avez modifié l'état pour utiliser mutableStateListOf
. Voyons maintenant comment créer un état éditeur.
Définir un état éditeur
Nous allons maintenant ajouter l'état pour notre éditeur. Pour éviter de répéter le texte des tâches, nous allons modifier la liste directement. Pour ce faire, au lieu de conserver le texte que nous modifions, nous allons conserver un index de liste pour l'élément d'éditeur actuel.
Ouvrez TodoViewModel.kt
et ajoutez un état éditeur.
Définissez un nouveau private var currentEditPosition
qui contient la position de modification actuelle. Il contiendra l'index de la liste que nous sommes en train de modifier.
Ensuite, exposez le currentEditItem
pour composer un message à l'aide d'un getter. Bien qu'il s'agisse d'une fonction Kotlin standard, currentEditPosition
est observable pour Compose au même titre que l'élément State<TodoItem>
.
TodoViewModel.kt
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
class TodoViewModel : ViewModel() {
// private state
private var currentEditPosition by mutableStateOf(-1)
// state: todoItems
var todoItems = mutableStateListOf<TodoItem>()
private set
// state
val currentEditItem: TodoItem?
get() = todoItems.getOrNull(currentEditPosition)
// ..
Lorsqu'un composable appelle currentEditItem
, il observe les modifications apportées à todoItems
et à currentEditPosition
. Si l'une de ces deux valeurs est modifiée, le composable peut appeler de nouveau la méthode getter pour obtenir la nouvelle valeur.
Définir des événements éditeur
Nous avons défini un état éditeur. Nous devons maintenant définir les événements que les composables peuvent appeler pour contrôler l'édition.
Créez trois événements : onEditItemSelected(item: TodoItem)
, onEditDone()
et onEditItemChange(item: TodoItem)
.
Les événements onEditItemSelected
et onEditDone
modifient simplement l'élément currentEditPosition
. Si vous modifiez currentEditPosition
, Compose recomposera tout composable lisant currentEditItem
.
TodoViewModel.kt
class TodoViewModel : ViewModel() {
...
// event: onEditItemSelected
fun onEditItemSelected(item: TodoItem) {
currentEditPosition = todoItems.indexOf(item)
}
// event: onEditDone
fun onEditDone() {
currentEditPosition = -1
}
// event: onEditItemChange
fun onEditItemChange(item: TodoItem) {
val currentItem = requireNotNull(currentEditItem)
require(currentItem.id == item.id) {
"You can only change an item with the same id as currentEditItem"
}
todoItems[currentEditPosition] = item
}
}
L'événement onEditItemChange
actualise la liste au niveau indiqué par currentEditPosition
. Ceci modifiera simultanément les valeurs renvoyées par currentEditItem
et par todoItems
. Avant cela, des contrôles de sécurité permettent de s'assurer que l'appelant n'essaie pas d'écrire un élément erroné.
Mettre fin à la modification lors de la suppression d'éléments
Actualisez l'événement removeItem
pour fermer l'éditeur actuel lorsqu'un élément est supprimé.
TodoViewModel.kt
// event: removeItem
fun removeItem(item: TodoItem) {
todoItems.remove(item)
onEditDone() // don't keep the editor open when removing items
}
Exécuter à nouveau l'application
Et voilà ! Vous avez mis à jour ViewModel
pour utiliser MutableState
et constaté la façon dont cela simplifie le code d'état observable.
Dans la section suivante, nous allons ajouter un test pour ViewModel
, puis passer à la création de l'UI d'édition.
Comme cette section comporte de nombreuses opérations, voici l'intégralité du modèle TodoViewModel
après l'application de toutes les modifications :
TodoViewModel.kt
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
class TodoViewModel : ViewModel() {
private var currentEditPosition by mutableStateOf(-1)
var todoItems = mutableStateListOf<TodoItem>()
private set
val currentEditItem: TodoItem?
get() = todoItems.getOrNull(currentEditPosition)
fun addItem(item: TodoItem) {
todoItems.add(item)
}
fun removeItem(item: TodoItem) {
todoItems.remove(item)
onEditDone() // don't keep the editor open when removing items
}
fun onEditItemSelected(item: TodoItem) {
currentEditPosition = todoItems.indexOf(item)
}
fun onEditDone() {
currentEditPosition = -1
}
fun onEditItemChange(item: TodoItem) {
val currentItem = requireNotNull(currentEditItem)
require(currentItem.id == item.id) {
"You can only change an item with the same id as currentEditItem"
}
todoItems[currentEditPosition] = item
}
}
10. Tester l'état dans ViewModel
Il est recommandé de tester votre ViewModel
pour vous assurer que la logique de votre application est correcte. Dans cette section, nous allons écrire un programme pour montrer comment tester un modèle View en utilisant State<T>
en guise d'état.
Ajouter un test à TodoViewModelTest
Ouvrez TodoViewModelTest.kt
dans le répertoire test/
, puis ajoutez un test pour supprimer un élément :
TodoViewModelTest.kt
import com.example.statecodelab.util.generateRandomTodoItem
import com.google.common.truth.Truth.assertThat
import org.junit.Test
class TodoViewModelTest {
@Test
fun whenRemovingItem_updatesList() {
// before
val viewModel = TodoViewModel()
val item1 = generateRandomTodoItem()
val item2 = generateRandomTodoItem()
viewModel.addItem(item1)
viewModel.addItem(item2)
// during
viewModel.removeItem(item1)
// after
assertThat(viewModel.todoItems).isEqualTo(listOf(item2))
}
}
Ce programme montre comment tester State<T>
, qui est modifié directement par des événements. Dans la section précédente, il crée un nouveau modèle ViewModel
, puis ajoute deux tâches à todoItems
.
La méthode que nous testons est removeItem
. Elle supprime le premier élément de la liste.
Enfin, nous utilisons des assertions de vérité pour confirmer que la liste ne contient que le second élément.
Nous n'avons aucune opération supplémentaire à effectuer pour lire todoItems
dans un test si les actualisations ont été causées directement par le test (comme c'est le cas ici en appelant removeItem
). Il s'agit simplement d'une tâche List<TodoItem>
.
Les autres tests pour ce ViewModel
suivent la même logique. Nous ne les aborderons donc pas dans cet atelier de programmation. Vous pouvez ajouter des tests du ViewModel
pour vérifier qu'il fonctionne, ou ouvrir TodoViewModelTest
dans le module terminé pour accéder à d'autres tests.
Dans la section suivante, nous ajouterons le nouveau mode Édition à l'UI.
11. Réutiliser des composables sans état
Nous sommes enfin prêts à implémenter notre conception interactive néo-moderne. Pour rappel, voici ce que nous essayons de créer :
Maquette pour le mode Édition
Transmettre l'état et les événements à TodoScreen
Nous avons défini tous les états et événements dont nous aurons besoin pour cet écran dans TodoViewModel. Nous allons maintenant actualiser TodoScreen pour prendre en compte l'état et les événements nécessaires à son affichage.
Ouvrez TodoScreen.kt
, puis modifiez la signature de TodoScreen
pour ajouter :
- L'élément en cours de modification :
currentlyEditing: TodoItem?
- Les trois nouveaux événements :
onStartEdit: (TodoItem) -> Unit
, onEditItemChange: (TodoItem) -> Unit
et onEditDone: () -> Unit
TodoScreen.kt
@Composable
fun TodoScreen(
items: List<TodoItem>,
currentlyEditing: TodoItem?,
onAddItem: (TodoItem) -> Unit,
onRemoveItem: (TodoItem) -> Unit,
onStartEdit: (TodoItem) -> Unit,
onEditItemChange: (TodoItem) -> Unit,
onEditDone: () -> Unit
) {
// ...
}
Ces lignes correspondent simplement au nouvel état et au nouvel événement que nous venons de définir sur ViewModel
.
Ensuite, dans TodoActivity.kt
, transmettez les nouvelles valeurs dans TodoActivityScreen
.
TodoActivity.kt
@Composable
private fun TodoActivityScreen(todoViewModel: TodoViewModel) {
TodoScreen(
items = todoViewModel.todoItems,
currentlyEditing = todoViewModel.currentEditItem,
onAddItem = todoViewModel::addItem,
onRemoveItem = todoViewModel::removeItem,
onStartEdit = todoViewModel::onEditItemSelected,
onEditItemChange = todoViewModel::onEditItemChange,
onEditDone = todoViewModel::onEditDone
)
}
Cette opération transmet simplement l'état et les événements requis par notre nouveau TodoScreen
.
Définir un composable pour la fonctionnalité de modification directe
Créez un composable dans TodoScreen.kt
qui utilise un composable sans état TodoItemInput
pour définir une fonctionnalité de modification directe.
TodoScreen.kt
@Composable
fun TodoItemInlineEditor(
item: TodoItem,
onEditItemChange: (TodoItem) -> Unit,
onEditDone: () -> Unit,
onRemoveItem: () -> Unit
) = TodoItemInput(
text = item.task,
onTextChange = { onEditItemChange(item.copy(task = it)) },
icon = item.icon,
onIconChange = { onEditItemChange(item.copy(icon = it)) },
submit = onEditDone,
iconsVisible = true
)
Ce composable est sans état. Il n'affiche que l'élément item
transmis et utilise les événements pour demander l'actualisation de l'état. Comme nous avons déjà extrait un composable sans état TodoItemInput
précédemment, nous pouvons facilement l'utiliser dans ce contexte sans état.
Cet exemple montre le potentiel de réutilisation des composables sans état. Même si l'en-tête utilise un TodoItemEntryInput
avec état sur le même écran, nous pouvons hisser l'état jusqu'à ViewModel
pour la fonctionnalité de modification directe.
Utiliser la fonctionnalité de modification directe dans LazyColumn
Dans la partie LazyColumn
de TodoScreen
, affichez TodoItemInlineEditor
si l'élément actuel est en cours de modification. Sinon, affichez TodoRow
.
De plus, démarrez le mode Édition en cliquant sur une tâche (au lieu de la supprimer comme avant).
TodoScreen.kt
// fun TodoScreen()
// ...
LazyColumn(
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(top = 8.dp)
) {
items(items) { todo ->
if (currentlyEditing?.id == todo.id) {
TodoItemInlineEditor(
item = currentlyEditing,
onEditItemChange = onEditItemChange,
onEditDone = onEditDone,
onRemoveItem = { onRemoveItem(todo) }
)
} else {
TodoRow(
todo,
{ onStartEdit(it) },
Modifier.fillParentMaxWidth()
)
}
}
}
// ...
Le composable LazyColumn
est l'équivalent dans Compose d'un élément RecyclerView
. Il recomposera uniquement les éléments de la liste nécessaires pour afficher l'écran actuel. Lorsque l'utilisateur le fait défiler, il élimine les composables qui ont quitté l'écran et en crée de nouveaux pour les éléments visibles.
Essayez le nouvel éditeur interactif !
Exécutez de nouveau l'application. Cette fois, lorsque vous cliquez sur une ligne de tâche, l'éditeur interactif s'ouvre.
Nous utilisons le même composable d'UI sans état permettant d'afficher à la fois l'en-tête avec état et l'expérience d'éditeur interactive. Nous n'avons introduit aucun état en double pour cela.
Les choses commencent à prendre forme, mais le bouton d'ajout n'est pas correctement positionné et nous devons modifier l'en-tête. Terminons la conception en quelques étapes supplémentaires.
Remplacer l'en-tête lors de l'édition
Nous allons maintenant terminer la conception de l'en-tête, puis étudier comment remplacer le bouton par les emoji interactifs que le concepteur souhaite utiliser pour sa création interactive néo-moderne.
Revenez au composable TodoScreen
, puis faites en sorte que l'en-tête réponde aux changements dans l'état éditeur. Si currentlyEditing
a la valeur null
, nous afficherons TodoItemEntryInput
et transmettrons elevation = true
à TodoItemInputBackground
. Si la valeur de currentlyEditing
n'est pas null
, transmettez elevation = false
à TodoItemInputBackground
et affichez le texte "Editing item" (Modification de l'élément) dans le même arrière-plan.
TodoScreen.kt
import androidx.compose.material.MaterialTheme
import androidx.compose.ui.text.style.TextAlign
@Composable
fun TodoScreen(
items: List<TodoItem>,
currentlyEditing: TodoItem?,
onAddItem: (TodoItem) -> Unit,
onRemoveItem: (TodoItem) -> Unit,
onStartEdit: (TodoItem) -> Unit,
onEditItemChange: (TodoItem) -> Unit,
onEditDone: () -> Unit
) {
Column {
val enableTopSection = currentlyEditing == null
TodoItemInputBackground(elevate = enableTopSection) {
if (enableTopSection) {
TodoItemEntryInput(onAddItem)
} else {
Text(
"Editing item",
style = MaterialTheme.typography.h6,
textAlign = TextAlign.Center,
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(16.dp)
.fillMaxWidth()
)
}
}
// ..
Là encore, nous modifions l'arborescence de composition lors de la recomposition. Lorsque la partie supérieure est activée, la valeur TodoItemEntryInput
est affichée. Sinon, un composable Text
s'affiche avec la mention "Editing item" (Modification de l'élément).
L'arrière-plan TodoItemInputBackground
qui était inclus dans le code de départ permet d'animer automatiquement le redimensionnement ainsi que les changements d'élévation. Ainsi, lorsque vous passez en mode Édition, ce code s'anime automatiquement entre les états.
Exécuter à nouveau l'application
Exécutez de nouveau l'application. Vous verrez qu'elle s'anime entre les états ou l'édition est activée ou désactivée. Nous avons presque terminé ce projet.
Dans la section suivante, nous allons apprendre à structurer le code des boutons emoji.
12. Utiliser des emplacements Slot pour transmettre des sections à l'écran
Les composables sans état qui affichent une UI complexe peuvent finir par contenir de nombreux paramètres. Si le nombre de paramètres n'est pas trop important et qu'ils configurent directement le composable, ce n'est pas un problème. Toutefois, il est parfois nécessaire de transmettre des paramètres pour configurer les enfants d'un composable.
Dans cette approche interactive néo-moderne, le concepteur souhaite que le bouton "Add" (Ajouter) reste en haut de l'écran, mais soit remplacé par deux boutons emoji pour la fonctionnalité de modification directe. Nous pourrions ajouter des paramètres à TodoItemInput
pour gérer ce cas, mais cette tâche ne semble pas relever des attributions de TodoItemInput
.
Il nous faut trouver un moyen d'ajouter une section de boutons préconfigurée à un composable. L'appelant pourra ainsi configurer les boutons comme il le souhaite sans avoir à partager tous les états requis pour les configurer avec TodoItemInput
.
Cela permettra à la fois de réduire le nombre de paramètres transmis au composable sans état et de les rendre plus réutilisables.
Le format à utiliser pour transmettre une section préconfigurée est un emplacement Slot. Les Slots sont des paramètres d'un composable qui permettent à l'appelant de décrire une section de l'écran. Vous trouverez des exemples d'emplacements Slot dans toutes les API de composables intégrées. Scaffold
est l'un des exemples les plus couramment utilisés.
Scaffold
est le composable qui permet de décrire un écran entier dans Material Design, par exemple les éléments topBar
, bottomBar
et le corps de l'écran.
Au lieu de fournir des centaines de paramètres pour configurer chaque section de l'écran, Scaffold
segmente des emplacements Slot où vous pouvez placer les composables de votre choix. Cela réduit à la fois le nombre de paramètres nécessaires pour Scaffold
et rend cet élément plus réutilisable. Si vous souhaitez créer un topBar
personnalisé, Scaffold
l'affichera sans problème.
@Composable
fun Scaffold(
// ..
topBar: @Composable (() -> Unit)? = null,
bottomBar: @Composable (() -> Unit)? = null,
// ..
bodyContent: @Composable (PaddingValues) -> Unit
) {
Définir un emplacement Slot sur TodoItemInput
Ouvrez TodoScreen.kt
, puis définissez un nouveau paramètre @Composable () -> Unit
dans l'élément TodoItemInput
sans état appelé buttonSlot
.
TodoScreen.kt
@Composable
fun TodoItemInput(
text: String,
onTextChange: (String) -> Unit,
icon: TodoIcon,
onIconChange: (TodoIcon) -> Unit,
submit: () -> Unit,
iconsVisible: Boolean,
buttonSlot: @Composable () -> Unit
) {
// ...
Il s'agit d'un emplacement générique où l'appelant peut placer les boutons de son choix. Nous l'utiliserons pour spécifier différents boutons pour l'en-tête et les instances de modification directe.
Afficher le contenu de buttonSlot
Remplacez l'appel de TodoEditButton
par le contenu de l'emplacement Slot.
TodoScreen.kt
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.width
@Composable
fun TodoItemInput(
text: String,
onTextChange: (String) -> Unit,
icon: TodoIcon,
onIconChange: (TodoIcon) -> Unit,
submit: () -> Unit,
iconsVisible: Boolean,
buttonSlot: @Composable() () -> Unit,
) {
Column {
Row(
Modifier
.padding(horizontal = 16.dp)
.padding(top = 16.dp)
) {
TodoInputText(
text,
onTextChange,
Modifier
.weight(1f)
.padding(end = 8.dp),
submit
)
// New code: Replace the call to TodoEditButton with the content of the slot
Spacer(modifier = Modifier.width(8.dp))
Box(Modifier.align(Alignment.CenterVertically)) { buttonSlot() }
// End new code
}
if (iconsVisible) {
AnimatedIconRow(icon, onIconChange, Modifier.padding(top = 8.dp))
} else {
Spacer(modifier = Modifier.height(16.dp))
}
}
}
Nous pourrions appeler directement buttonSlot()
, mais nous devons conserver align
pour centrer tout ce que l'appelant pourra nous transmettre verticalement. Pour ce faire, nous plaçons l'emplacement Slot dans Box
, qui est un composable de base.
Actualiser TodoItemEntryInput
avec état pour utiliser l'emplacement Slot
Nous devons maintenant informer les appelants pour qu'ils utilisent buttonSlot
. Mais modifions d'abord TodoItemEntryInput
:
TodoScreen.kt
@Composable
fun TodoItemEntryInput(onItemComplete: (TodoItem) -> Unit) {
val (text, onTextChange) = remember { mutableStateOf("") }
val (icon, onIconChange) = remember { mutableStateOf(TodoIcon.Default)}
val submit = {
if (text.isNotBlank()) {
onItemComplete(TodoItem(text, icon))
onTextChange("")
onIconChange(TodoIcon.Default)
}
}
TodoItemInput(
text = text,
onTextChange = onTextChange,
icon = icon,
onIconChange = onIconChange,
submit = submit,
iconsVisible = text.isNotBlank()
) {
TodoEditButton(onClick = submit, text = "Add", enabled = text.isNotBlank())
}
}
Étant donné que buttonSlot
est le dernier paramètre de TodoItemInput
, nous pouvons utiliser une syntaxe lambda placée à la suite. Ensuite, il suffit d'appeler TodoEditButton
dans le lambda, comme précédemment.
Actualiser TodoItemInlineEditor
pour utiliser l'emplacement Slot
Pour terminer la refactorisation, modifiez TodoItemInlineEditor
afin d'utiliser également l'emplacement Slot :
TodoScreen.kt
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.TextButton
@Composable
fun TodoItemInlineEditor(
item: TodoItem,
onEditItemChange: (TodoItem) -> Unit,
onEditDone: () -> Unit,
onRemoveItem: () -> Unit
) = TodoItemInput(
text = item.task,
onTextChange = { onEditItemChange(item.copy(task = it)) },
icon = item.icon,
onIconChange = { onEditItemChange(item.copy(icon = it)) },
submit = onEditDone,
iconsVisible = true,
buttonSlot = {
Row {
val shrinkButtons = Modifier.widthIn(20.dp)
TextButton(onClick = onEditDone, modifier = shrinkButtons) {
Text(
text = "\uD83D\uDCBE", // floppy disk
textAlign = TextAlign.End,
modifier = Modifier.width(30.dp)
)
}
TextButton(onClick = onRemoveItem, modifier = shrinkButtons) {
Text(
text = "❌",
textAlign = TextAlign.End,
modifier = Modifier.width(30.dp)
)
}
}
}
)
Ici, nous transmettons buttonSlot
en tant que paramètre nommé. Ensuite, dans buttonSlot
, nous créons une ligne contenant les deux boutons pour la conception avec fonctionnalité de modification directe.
Exécuter à nouveau l'application
Exécutez à nouveau l'application et faites des essais d'utilisation de la fonctionnalité de modification directe.
Dans cette section, nous avons personnalisé notre composable sans état à l'aide d'un emplacement Slot qui a permis à l'appelant de contrôler une section de l'écran. En utilisant des emplacements Slot, nous avons évité l'association de TodoItemInput
avec les différentes conceptions susceptibles d'être ajoutées ultérieurement.
Lorsque vous ajoutez des paramètres à des composables sans état pour personnaliser les enfants, demandez-vous s'il ne vaudrait pas mieux utiliser des emplacements Slot. Ces emplacements ont tendance à rendre les composables plus réutilisables tout en conservant un nombre raisonnable de paramètres.
13. Félicitations
Félicitations, vous avez terminé cet atelier de programmation et appris à structurer l'état à l'aide d'un flux de données unidirectionnel 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 réutiliser un composable complexe dans différentes situations sur le même écran. Vous avez aussi appris à intégrer un élément ViewModel avec Compose à l'aide de LiveData et de MutableState.
Et maintenant ?
Consultez les autres ateliers de programmation du parcours Compose
Exemples d'applications
- JetNews montre comment utiliser un flux de données unidirectionnel pour utiliser des composables avec état afin de gérer l'état dans un écran conçu avec des composables sans état