• 12 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 01/10/2024

Concevez une liste

L'utilisation de layouts basiques commeRowouColumnpour lister des nombreux éléments engendre des problèmes de performances. Avec l'ancien UI Toolkit, vous avez peut-être utiliséRecyclerViewpour résoudre ce genre de problème. Bien entendu, Jetpack Compose propose également des layouts commeLazyColumn ouLazyRowqui 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’unAdapterpour 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 layoutLazyColumnpermet 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 unePreviewpour 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 textePodcastsen 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 headerprenne 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 layoutsLazyColumnetLazyRow

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.

Rendu final de la liste des `OtherPodcastItem`avec des marges pour aérer le contenu.

Dans cet exemple, vous pouvez constater que :

  • Le paramètrecontentPaddingpermet 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ètrehorizontalArrangement, similaire à celui du layoutRow. Vous constaterez dans les signatures des layoutsLazyRowetLazyColumn qu’ils exposent chacun un paramètre permettant d’ajuster la disposition verticale et horizontale de leurs enfants.

  • La variablelistStateest 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 variablelistStateest ensuite utilisée par la fonctionrememberSnapFlingBehavior , qui gère le comportement de défilement de la liste en s’appuyant sur son état.

  • flingBehaviorajuste 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  LazyListStateexposé par les lazy layouts.

Il faut tout d’abord instancier une variable de typeLazyListStateen 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.

Rendu de la liste des `OtherPodcastItem` avec un bouton flottant qui apparaît dès que le premier élément de la liste n’est plus visible. Le bouton est sur la droite.
Illustration du rendu de la liste des "OtherPodcastItem" avec un bouton flottant qui apparaît dès que le premier élément de la liste n’est plus visible 

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.

L’image montre un code Kotlin avec un avertissement suggérant d’utiliser derivedStateOf pour lire firstVisibleItemIndex dans une fonction composable.
Alerte de Android Studio suggérant d’utiliser "derivedStateOf" pour déterminer la visibilité du 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 ?

UtiliserderivedStateOfoptimise la recomposition en évitant des mises à jour inutiles. Par exemple, utiliser directement la valeurlistState.firstVisibleItemIndexdans 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 :

L’image compare isScrollToFirstVisible sans et avec derivedState. Sans derivedState, l’état change souvent; avec derivedState, les changements sont réduits (true, false).
Alerte de Android Studio suggérant d’utiliser "derivedStateOf" pour déterminer la visibilité du bouton flottant

Actionnez le défilement

Pour enclencher l’action de défilement de la liste lorsque l’utilisateur clique sur le bouton flottant, nous allons :

  1. Utiliser ces deux fonctions fournies par la variablelistState : 

    • suspend fun scrollToItem(index: Int, scrollOffset: Int)

    • suspend fun animateScrollToItem(index: Int, scrollOffset: Int) 

  2. Utiliser la fonctionanimateScrollToItemdans 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   animateScrollToItemest une fonction de typesuspend, ce qui signifie qu'elle doit être appelée depuis une coroutine ou une autre fonctionsuspend.

L’image montre un code Kotlin utilisant animateScrollToItem avec un avertissement. Cette fonction suspendue doit être appelée uniquement depuis une coroutine ou une autre fonction suspendue pour assurer un comportement correct.
Erreur de compilation dans Android studio lors de l’utilisation de la fonction "animateScrollToItem" en dehors d’une coroutine

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 onClickd'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  LazyColumnetLazyRow, pour afficher des listes de manière performante, en ne composant, mesurant, et dessinant que les éléments visibles à l'écran. 

  • Les fonctionsitems{},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 commecontentPadding,horizontalArrangementetverticalAlignment  pour LazyRow 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 typeLazyListStategrâce à la fonctionrememberLazyListState et en associant cette variable au lazy layout cible.

  • Pour utiliseranimateScrollToItemau 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.

Exemple de certificat de réussite
Exemple de certificat de réussite