L'utilisation de layouts basiques commeRow
ouColumn
pour lister des nombreux éléments engendre des problèmes de performances. Avec l'ancien UI Toolkit, vous avez peut-être utiliséRecyclerView
pour résoudre ce genre de problème. Bien entendu, Jetpack Compose propose également des layouts commeLazyColumn
ouLazyRow
qui vont gérer efficacement l'affichage des listes et des grilles en ne composant, mesurant et dessinant que les éléments visibles à l'écran. On appelle ces layouts des “lazy layouts” (“mises en page paresseuses” en français). Vous n’aurez plus besoin de définir le rendu visuel des éléments de la liste dans un XML, puis d’implémenter une classe de typeViewHolder
ainsi qu’unAdapter
pour lier les données à afficher.
Découvrez la simplicité d'afficher des listes avec Jetpack Compose
Voici comment afficher une liste d’éléments de manière optimisée :
@Composable
fun ListOfItems(
itemsList: List<String>,
modifier: Modifier = Modifier,
){
LazyColumn(modifier = modifier.fillMaxWidth()) {
items(itemsList) { item ->
Text(
text = item
)
}
}
}
@Preview
@Composable
fun PreviewListOfItems(){
BestPodcastTheme {
ListOfItems(
itemsList = listOf("Item 1", "Item 2", "Item 3", "Item 4")
)
}
}
Le layoutLazyColumn
permet d'afficher une liste d'éléments tout en veillant à ce que l’application reste performante. Pour définir chaque élément, on utilise la fonctionitems{}
qui fonctionne comme une boucleforEach{}
. Cette fonction utilise chaque élément de la liste pour créer un composantText
correspondant.
C'est tout ! Vous pouvez essayer de l'intégrer dans unePreview
pour voir à quel point cela fonctionne bien.
Prenez en main les fonctions fournies par les listes
Dans l’exemple précédent, nous avons utilisé la fonctionitems{}
à l’intérieur du composantLazyColumn
. Cette fonction est en fait issue d’un Domain Specific Language ou DSL qui permet de faciliter la déclaration du contenu d’une liste.
Un DSL est un langage spécifiquement conçu pour une partie précise d'une application. Son objectif est de rendre le code plus lisible et plus compréhensible en masquant l'implémentation interne et en réduisant la redondance du code. Dans le développement Android, nous utilisons fréquemment des DSL. Par exemple, nous nous en servons dans nos scripts Gradle, écrits en langage Groovy ou Kotlin, pour simplifier la configuration et l'automatisation de nos projets.
Pour définir le contenu d’une liste, il est obligatoire de passer par le DSL fourni par Jetpack Compose. D’ailleurs, si vous essayez de vous en passer, votre code ne compilera pas. Explorons ensemble les fonctions principales de ce DSL.
La fonctionitems{}
Deux fonctionsitems{}
bien distinctes peuvent être utilisées pour ajouter des éléments à une liste avec Jetpack Compose :
1. items( items: List<T>)
: Elle prend en paramètre une liste à la manière d’unforEach
comme illustré précédemment.
val podcasts = remember { PodcastFactory.makePodcasts() }
LazyColumn(modifier = Modifier.fillMaxWidth()) {
items(items = podcasts) { podcastItem ->
PodcastItem(
podcast = podcastItem,
onDownloadClicked = { /* TODO */ },
)
}
}
2. fun items( count: Int)
: Elle prend en paramètre un nombre entier, correspondant au nombre d’éléments à afficher, comme illustré ci-dessous :
val podcasts = remember { PodcastFactory.makePodcasts() }
LazyColumn(modifier = Modifier.fillMaxWidth()) {
items(count = podcasts.size) { index ->
PodcastItem(
podcast = podcasts[index],
onDownloadClicked = { /* TODO */ },
)
}
}
La fonctionitem{}
Cette fonction est utilisée pour ajouter un seul élément à une liste. Elle est particulièrement utile pour insérer des éléments comme des en-têtes, des pieds de page ou des éléments isolés au sein d'une liste contenant plusieurs éléments. Ici, nous ajoutons un textePodcasts
en amont de la liste des podcasts. Par exemple :
LazyColumn(modifier = Modifier.fillMaxWidth()) {
item {
Text(
text = "Podcasts",
style = MaterialTheme.typography.headlineLarge,
)
}
items(count = podcasts.size) { index ->
/*...*/
}
}
La fonctionstickyHeader{}
Cette fonction permet de définir un élément qui reste épinglé en haut de la liste jusqu'à ce qu'un autre élément de typesticky header
prenne sa place. Ici, nous ajoutons un texteNouveauté
en amont de la liste des nouveautés.
LazyColumn(modifier = Modifier.fillMaxWidth()) {
stickyHeader {
Text(
text = "Nouveautés !",
style = MaterialTheme.typography.titleLarge,
)
}
items(count = news.size) { index ->
/*...*/
}
/*...*/
}
Personnalisez les layoutsLazyColumn
etLazyRow
Personnalisez le rendu visuel d’un lazy layout
Les exemples vus précédemment montrent comment créer une liste simple. Cependant, il est parfois nécessaire de personnaliser davantage le rendu et le comportement d'une liste. Analysons le code suivant.
val podcasts = remember { PodcastFactory.makePodcasts() }
val listState = rememberLazyListState()
LazyRow(
state = listState,
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
flingBehavior = rememberSnapFlingBehavior(lazyListState = listState),
) {
items(podcasts) { podcast ->
OtherPodcastItem(
podcast = podcast,
modifier = Modifier
.width(200.dp)
.height(300.dp)
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.small
)
.border(
width = 2.dp,
color = MaterialTheme.colorScheme.primary,
shape = MaterialTheme.shapes.small
)
.padding(8.dp),
)
}
}
Et voici le rendu.
Dans cet exemple, vous pouvez constater que :
Le paramètre
contentPadding
permet d’appliquer une marge autour de l’ensemble du lazy layout.Pour définir une disposition particulière entre les éléments, on utilise ici le paramètre
horizontalArrangement
, similaire à celui du layoutRow
. Vous constaterez dans les signatures des layoutsLazyRow
etLazyColumn
qu’ils exposent chacun un paramètre permettant d’ajuster la disposition verticale et horizontale de leurs enfants.La variable
listState
est créée grâce à la fonctionrememberLazyListState()
. Elle permet d’observer et de contrôler l’état de la liste et de maintenir cet état à travers les recompositions de la liste et en cas de changement de configuration.Cette variable
listState
est ensuite utilisée par la fonctionrememberSnapFlingBehavior
, qui gère le comportement de défilement de la liste en s’appuyant sur son état.flingBehavior
ajuste le comportement d'animation lors d'un défilement rapide (fling en anglais). Il contrôle la vitesse de défilement et la manière dont le contenu se stabilise après un mouvement rapide de l'utilisateur.
Voyons maintenant en quoi cette variablelistState
pourrait également nous être utile.
Gérez l’état de défilement grâce àrememberLazyListState
Il est fréquent au sein d’une application de devoir réagir à l’état de défilement d’une liste ou de devoir actionner le défilement d’une liste vers une position donnée. Voici quelques cas d'usages où cela peut être utile :
Montrer un bouton lorsque l'utilisateur fait défiler la liste, permettant par exemple de remonter rapidement au début de la liste.
Charger davantage d'éléments lorsque l'utilisateur arrive au bout de la liste.
Afficher des indicateurs de section ou de position pour aider l'utilisateur à comprendre où il se trouve dans la liste.
Voyons ensemble comment il est possible de réaliser tout cela grâce au paramètre de type LazyListState
exposé par les lazy layouts.
Il faut tout d’abord instancier une variable de typeLazyListState
en utilisant la fonction rememberLazyListState()
et l’affecter au lazy layout, comme ceci.
val listState = rememberLazyListState()
LazyRow(
modifier = modifier,
state : LazyListState = listState
) {
/*...*/
}
Ensuite, grâce à cette variable nous allons pouvoir accéder aux états suivants :
firstVisibleItemIndex
, qui correspond à l’index du premier élément du lazy layout visible à l’écran.firstVisibleItemScrollOffset
, qui correspond à la portion en pixel visible du premier élément.isScrollInProgress
, qui permet de savoir si l’utilisateur est actuellement en train de faire défiler le layout.
Dans notre liste de podcasts, nous pouvons décider d’afficher en superposition un bouton flottant permettant à l’utilisateur de revenir rapidement au début de la liste, dès que le premier élément n’est plus visible.
Cela donne :
val podcasts = remember { PodcastFactory.makePodcasts() }
val listState = rememberLazyListState()
Box(
modifier = Modifier.fillMaxWidth()
) {
LazyRow(
state = listState,
) {
items(podcasts) { podcast ->
/* ... */
}
}
if (listState.firstVisibleItemIndex > 0) {
FloatingActionButton(
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 8.dp),
shape = CircleShape,
onClick = { /*TODO*/ },
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
contentColor = MaterialTheme.colorScheme.onTertiaryContainer
) {
Icon(
Icons.AutoMirrored.Default.ArrowBack,
contentDescription = "Revenir au début de la liste",
)
}
}
}
Voici le résultat.
Optimisez les performances de la lecture de l’état de défilement
Si vous testez le code précédent dans Android Studio, vous remarquerez peut-être que celui-ci suggère une amélioration sur le code chargé de conditionner l’affichage de ce bouton flottant.
Concrètement, l’optimisation qu’il suggère de faire est celle-ci :
val isScrollToFirstVisible = remember {
derivedStateOf { listState.firstVisibleItemIndex > 0 }
}
if (isScrollToFirstVisible.value) {
/*...*/
}
À quoi sert cette optimisation ?
UtiliserderivedStateOf
optimise la recomposition en évitant des mises à jour inutiles. Par exemple, utiliser directement la valeurlistState.firstVisibleItemIndex
dans votre condition, pourrait entraîner une recomposition à chaque fois que l’utilisateur fait défiler la liste, même juste d’un pixel. Dans les listes volumineuses, cela pourrait significativement réduire les performances car chaque petit déplacement déclencherait une nouvelle évaluation de l’état.
AvecderivedStateOf
, vous créez un état dérivé qui ne se met à jour que si la condition sur laquelle il est basé change. Cela réduit ainsi le nombre de recompositions nécessaires, comme illustré ci-dessous :
Actionnez le défilement
Pour enclencher l’action de défilement de la liste lorsque l’utilisateur clique sur le bouton flottant, nous allons :
Utiliser ces deux fonctions fournies par la variable
listState
:suspend fun scrollToItem(index: Int, scrollOffset: Int)
suspend fun animateScrollToItem(index: Int, scrollOffset: Int)
Utiliser la fonction
animateScrollToItem
dans la lambdaonClick
de notre bouton flottant.
Voici le code associé.
FloatingActionButton(
onClick = {
listState.animateScrollToItem(0)
},
/*...*/
) {
Icon(
Icons.AutoMirrored.Default.ArrowBack,
contentDescription = "Revenir au début de la liste",
)
}
Problème, ça ne compile pas ! En effet, Android Studio nous signale que la fonction animateScrollToItem
est une fonction de typesuspend
, ce qui signifie qu'elle doit être appelée depuis une coroutine ou une autre fonctionsuspend
.
Dans Jetpack Compose, pour créer une coroutine, on peut utiliser la fonctionrememberCoroutineScope()
qui fournit un scope de coroutine. Cela crée un contexte de coroutine qui survit aux recompositions. Vous pouvez ensuite utiliser ce contexte de coroutine, par exemple, dans l'événement onClick
d'un bouton. Cela permet de lancer des opérations asynchrones spécifiques au composant, comme faire défiler une liste automatiquement vers le début de la liste. Voici comment cela peut être mis en œuvre dans notre cas.
val scope = rememberCoroutineScope()
if (isScrollToFirstVisible.value) {
FloatingActionButton(
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 8.dp),
shape = CircleShape,
onClick = {
scope.launch {
listState.animateScrollToItem(0)
}
},
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
contentColor = MaterialTheme.colorScheme.onTertiaryContainer
) {
Icon(
Icons.AutoMirrored.Default.ArrowBack,
contentDescription = "Revenir au début de la liste",
)
}
}
Cette vidéo récapitule comment concevoir une liste horizontale classique et comment contrôler son défilement.
À vous de jouer !
Contexte
Vous allez maintenant pouvoir afficher la liste des podcasts. Charlie a déjà développé le squelette de l’écran principal, en utilisant le composant MaterialScaffold
. Cela permet de structurer l’écran plus facilement. Elle y a, par exemple, déjà intégré une barre d’action en haut grâce au composant MaterialTopAppBar
. C’est maintenant à vous de compléter l’écran.
Consignes
1. Créez un nouveau fichier "PodcastsScreen.ktet" intégrez le squelette d’écran suivant.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PodcastsScreen(
modifier: Modifier = Modifier
) {
val podcasts = remember { PodcastFactory.makePodcasts() }
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(R.string.best_podcasts),
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.primary
)
}
)
}
) { innerPadding ->
/* TODO : à compléter avec la liste des podcasts */
}
}
2. Dans le même fichier "PodcastsScreen.kt", affichez la liste de podcasts en paramètre en veillant à l’harmonie visuelle. Par exemple, espacez suffisamment les podcasts les uns des autres. N’hésitez pas à créer une prévisualisation de cet écran afin de vous assister.
3. Intégrez le composantPodcastSearchField
au début de la liste des podcasts. Ne vous préoccupez pas pour le moment de la logique de filtre sur la liste de podcasts. Vous allez vous en occuper plus tard.
4. Intégrez un bouton flottant en superposition de la liste, qui s’affiche lorsque le premier élément de la liste n’est plus visible à l’écran et qui permet, en cliquant dessus, de retourner au début de la liste.
Livrables
Votre projet sur Android Studio doit être conforme aux consignes ci-dessus.
En résumé
Jetpack Compose propose des solutions optimisées appelées "lazy layouts", telles que
LazyColumn
etLazyRow
, pour afficher des listes de manière performante, en ne composant, mesurant, et dessinant que les éléments visibles à l'écran.Les fonctions
items{}
,item{}
, etstickyHeader{}
permettent d'ajouter des éléments à une liste, soit en passant une liste d'éléments, soit un seul élément..Des paramètres comme
contentPadding
,horizontalArrangement
etverticalAlignment
pourLazyRow
permettent de personnaliser le rendu visuel et la disposition des éléments dans les lazy layouts.Il est possible d’observer l'état de défilement d’une liste afin de réagir à des événements, en instanciant une variable de type
LazyListState
grâce à la fonctionrememberLazyListState
et en associant cette variable au lazy layout cible.Pour utiliser
animateScrollToItem
au sein de Jetpack Compose, il faut utiliser un scope de coroutine fourni par la fonctionrememberCoroutineScope()
.
Notre application commence à être fonctionnelle. Cependant, les données affichées proviennent actuellement de notre fabrique de podcasts,PodcastFactory
. Dans une application réelle, il est préférable de récupérer les données depuis unViewModel
. Voyons donc comment dynamiser notre interface en récupérant les données issues duViewModel
.