De nombreuses applications affichent des collections d'éléments. Cette présentation vous explique comment effectuer cette opération efficacement dans Jetpack Compose.
Si vous savez que votre cas d'utilisation ne nécessite pas de défilement, vous pouvez utiliser une simple Column
ou une Row
(en fonction de la direction) et émettre le contenu de chaque élément en effectuant une itération sur une liste comme suit:
@Composable fun MessageList(messages: List<Message>) { Column { messages.forEach { message -> MessageRow(message) } } }
Nous pouvons mettre en place une Column
déroulante en utilisant le modificateur verticalScroll()
.
Listes différées
Si vous devez afficher un grand nombre d'éléments (ou une liste d'une longueur indéterminée), l'utilisation d'une mise en page de type Column
peut entraîner des problèmes de performances, car tous les éléments seront composés et affichés, qu'ils soient visibles ou non.
Compose fournit un ensemble de composants qui composent et mettent en page uniquement les éléments visibles dans la fenêtre d'affichage du composant. Ces composants incluent LazyColumn
et LazyRow
.
Comme leur nom l'indique, la différence entre LazyColumn
et LazyRow
réside dans l'orientation qu'elles imposent aux éléments et au défilement. LazyColumn
génère une liste à défilement vertical, et LazyRow
génère une liste à défilement horizontal.
Les composants inactifs diffèrent de la plupart des mises en page dans Compose. Au lieu d'accepter un paramètre de bloc de contenu @Composable
, qui permet aux applications d'émettre directement des composables, les composants inactifs fournissent un bloc LazyListScope.()
. Ce bloc LazyListScope
propose un DSL qui permet aux applications de décrire les contenus d'un élément. Le composant différé est ensuite chargé d'ajouter le contenu de chaque élément selon la mise en page et la position de défilement définies.
LazyListScope
DSL
Le DSL de LazyListScope
fournit un certain nombre de fonctions permettant de décrire les éléments de la mise en page. En premier lieu, item()
ajoute un seul élément, et items(Int)
ajoute plusieurs éléments :
LazyColumn { // Add a single item item { Text(text = "First item") } // Add 5 items items(5) { index -> Text(text = "Item: $index") } // Add another single item item { Text(text = "Last item") } }
De plus, de nombreuses fonctions d'extension vous permettent d'ajouter des collections d'éléments, comme List
. Ces extensions nous permettent de migrer facilement l'exemple Column
ci-dessus :
/** * import androidx.compose.foundation.lazy.items */ LazyColumn { items(messages) { message -> MessageRow(message) } }
Il existe également une variante de la fonction d'extension items()
, appelée itemsIndexed()
, qui fournit l'index. Pour en savoir plus, consultez la documentation de référence sur LazyListScope
.
Grilles différées
Les composables LazyVerticalGrid
et LazyHorizontalGrid
permettent d'afficher des éléments dans une grille. Une grille verticale différée affiche ses éléments dans un conteneur à faire défiler verticalement, s'étendant sur plusieurs colonnes, tandis que les grilles horizontales différées ont le même comportement sur l'axe horizontal.
Les grilles ont les mêmes capacités d'API puissantes que les listes et utilisent également un DSL très similaire (LazyGridScope.()
) pour décrire le contenu.
Les paramètres columns
dans LazyVerticalGrid
et rows
dans LazyHorizontalGrid
contrôlent la manière dont les cellules deviennent des colonnes ou des lignes. L'exemple suivant affiche des éléments dans une grille, en utilisant GridCells.Adaptive
pour définir chaque colonne sur une largeur d'au moins 128.dp
:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 128.dp) ) { items(photos) { photo -> PhotoItem(photo) } }
LazyVerticalGrid
permet de spécifier la largeur des éléments. La grille peut ainsi contenir autant de colonnes que possible. Une fois le nombre de colonnes calculé, toute largeur restante est répartie de manière égale entre les colonnes.
Cette méthode de dimensionnement adaptative est particulièrement utile pour afficher des ensembles d'éléments sur différentes tailles d'écran.
Si vous connaissez le nombre exact de colonnes à utiliser, vous pouvez fournir une instance de GridCells.Fixed
contenant le nombre de colonnes requises.
Lorsque certains éléments ont des dimensions non standards, vous pouvez utiliser la grille pour définir des intervalles de colonnes personnalisées pour vos éléments.
Spécifiez l'intervalle de colonne avec le paramètre span
des méthodes LazyGridScope DSL
item
et items
.
maxLineSpan
, l'une des valeurs du champ d'application de la portée, est particulièrement utile lorsque vous utilisez le dimensionnement adaptatif, car le nombre de colonnes n'est pas fixe.
Cet exemple montre comment fournir un intervalle de lignes complet :
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 30.dp) ) { item(span = { // LazyGridItemSpanScope: // maxLineSpan GridItemSpan(maxLineSpan) }) { CategoryCard("Fruits") } // ... }
Grille décalée différée
LazyVerticalStaggeredGrid
et LazyHorizontalStaggeredGrid
sont des composables qui vous permettent de créer une grille d'éléments décalée et chargée de manière différée.
Une grille verticale décalée différée affiche ses éléments dans un conteneur à faire défiler verticalement qui s'étend sur plusieurs colonnes et permet aux éléments individuels d'avoir des hauteurs différentes. Les grilles horizontales différées ont le même comportement sur l'axe horizontal avec des éléments de différentes largeurs.
L'extrait de code suivant est un exemple de base d'utilisation de LazyVerticalStaggeredGrid
avec une largeur 200.dp
par élément:
LazyVerticalStaggeredGrid( columns = StaggeredGridCells.Adaptive(200.dp), verticalItemSpacing = 4.dp, horizontalArrangement = Arrangement.spacedBy(4.dp), content = { items(randomSizedPhotos) { photo -> AsyncImage( model = photo, contentScale = ContentScale.Crop, contentDescription = null, modifier = Modifier .fillMaxWidth() .wrapContentHeight() ) } }, modifier = Modifier.fillMaxSize() )
Pour définir un nombre fixe de colonnes, vous pouvez utiliser StaggeredGridCells.Fixed(columns)
au lieu de StaggeredGridCells.Adaptive
.
La largeur disponible est alors divisée par le nombre de colonnes (ou de lignes pour une grille horizontale), et chaque élément occupe cette largeur (ou cette hauteur pour une grille horizontale):
LazyVerticalStaggeredGrid( columns = StaggeredGridCells.Fixed(3), verticalItemSpacing = 4.dp, horizontalArrangement = Arrangement.spacedBy(4.dp), content = { items(randomSizedPhotos) { photo -> AsyncImage( model = photo, contentScale = ContentScale.Crop, contentDescription = null, modifier = Modifier .fillMaxWidth() .wrapContentHeight() ) } }, modifier = Modifier.fillMaxSize() )
Marge intérieure du contenu
Vous devrez parfois ajouter une marge intérieure aux bords du contenu. Pour ce faire, les composants à chargement différé vous permettent de transmettre un élément PaddingValues
au paramètre contentPadding
:
LazyColumn( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), ) { // ... }
Dans cet exemple, nous ajoutons 16.dp
de marge intérieure sur les bords horizontaux (à gauche et à droite), puis 8.dp
en haut et en bas du contenu.
Notez que cette marge intérieure est appliquée au contenu, et non à la LazyColumn
elle-même. Dans l'exemple ci-dessus, le premier élément ajoutera une marge intérieure de 8.dp
en haut, le dernier élément ajoutera 8.dp
en bas, et une marge de 16.dp
sera ajoutée à tous les éléments à gauche et à droite.
Espacement du contenu
Pour ajouter un espace entre les éléments, vous pouvez utiliser Arrangement.spacedBy()
.
L'exemple ci-dessous ajoute 4.dp
d'espace entre chaque élément :
LazyColumn( verticalArrangement = Arrangement.spacedBy(4.dp), ) { // ... }
De même pour LazyRow
:
LazyRow( horizontalArrangement = Arrangement.spacedBy(4.dp), ) { // ... }
Les grilles acceptent les configurations verticales et horizontales :
LazyVerticalGrid( columns = GridCells.Fixed(2), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { items(photos) { item -> PhotoItem(item) } }
Clés d'élément
Par défaut, l'état de chaque élément pointe vers sa position dans la liste ou la grille. Toutefois, cela peut entraîner des problèmes en cas de modification de l'ensemble de données, car les éléments qui changent de position perdent tout état mémorisé. Imaginons le scénario suivant : un LazyRow
se trouve dans une LazyColumn
. Si la ligne change de position, l'utilisateur perd alors sa position de défilement dans la ligne.
Pour résoudre ce problème, vous pouvez fournir une clé stable et unique pour chaque élément, en bloquant le paramètre key
. Fournir une clé stable permet d'assurer la cohérence de l'état des éléments pour toutes les modifications de l'ensemble de données :
LazyColumn { items( items = messages, key = { message -> // Return a stable + unique key for the item message.id } ) { message -> MessageRow(message) } }
En fournissant des clés, vous aidez Compose à gérer les réorganisations correctement. Par exemple, si votre élément contient un état mémorisé, les clés permettent à Compose de déplacer cet état avec l'élément lorsque sa position change.
LazyColumn { items(books, key = { it.id }) { val rememberedValue = remember { Random.nextInt() } } }
Cependant, les types de clés d'élément que vous pouvez utiliser sont limités.
Le type de clé doit être compatible avec Bundle
, le mécanisme d'Android permettant de conserver les états lorsque l'activité est recréée. Bundle
prend en charge les primitives, les énumérations, ou encore les parcelables.
LazyColumn { items(books, key = { // primitives, enums, Parcelable, etc. }) { // ... } }
La clé doit être compatible avec Bundle
pour que le rememberSaveable
à l'intérieur du composable de l'élément puisse être restauré lors de la recréation de l'activité, ou même lorsque vous faites défiler la page hors de cet élément et vers l'arrière.
LazyColumn { items(books, key = { it.id }) { val rememberedValue = rememberSaveable { Random.nextInt() } } }
Animation des éléments
Si vous avez déjà utilisé le widget RecyclerView, vous savez qu'il anime les modifications d'éléments automatiquement.
Les mises en page différées offrent les mêmes fonctionnalités pour réorganiser les éléments.
Le fonctionnement de l'API est simple : il vous suffit de définir le modificateur animateItem
sur le contenu de l'élément :
LazyColumn { // It is important to provide a key to each item to ensure animateItem() works as expected. items(books, key = { it.id }) { Row(Modifier.animateItem()) { // ... } } }
Si nécessaire, vous pouvez même fournir des spécifications d'animations personnalisées :
LazyColumn { items(books, key = { it.id }) { Row( Modifier.animateItem( fadeInSpec = tween(durationMillis = 250), fadeOutSpec = tween(durationMillis = 100), placementSpec = spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy) ) ) { // ... } } }
Assurez-vous de fournir des clés pour vos éléments afin de pouvoir trouver la nouvelle position de l'élément déplacé.
En-têtes persistants (expérimental)
Le format "en-tête persistant" est utile pour afficher des listes de données groupées. Vous trouverez ci-dessous un exemple de "liste de contacts", regroupés par leur initiale :
Pour obtenir un en-tête persistant avec LazyColumn
, vous pouvez utiliser la fonction expérimentale stickyHeader()
, qui fournit le contenu de l'en-tête :
@OptIn(ExperimentalFoundationApi::class) @Composable fun ListWithHeader(items: List<Item>) { LazyColumn { stickyHeader { Header() } items(items) { item -> ItemRow(item) } } }
Pour obtenir une liste avec plusieurs en-têtes, comme dans l'exemple "Liste de contacts" ci-dessus, vous pouvez procéder comme suit :
// This ideally would be done in the ViewModel val grouped = contacts.groupBy { it.firstName[0] } @OptIn(ExperimentalFoundationApi::class) @Composable fun ContactsList(grouped: Map<Char, List<Contact>>) { LazyColumn { grouped.forEach { (initial, contactsForInitial) -> stickyHeader { CharacterHeader(initial) } items(contactsForInitial) { contact -> ContactListItem(contact) } } } }
Réagir à la position de défilement
De nombreuses applications doivent réagir et écouter la position de défilement et les changements au niveau de la mise en page des éléments.
Les composants différés prennent en charge ce cas d'utilisation en hissant LazyListState
:
@Composable fun MessageList(messages: List<Message>) { // Remember our own LazyListState val listState = rememberLazyListState() // Provide it to LazyColumn LazyColumn(state = listState) { // ... } }
Pour les cas d'utilisation simples, les applications ont besoin uniquement de connaître le premier élément visible. LazyListState
fournit donc firstVisibleItemIndex
et firstVisibleItemScrollOffset
.
Prenons l'exemple de l'affichage et du masquage d'un bouton selon que l'utilisateur a fait défiler la page au-delà du premier élément :
@Composable fun MessageList(messages: List<Message>) { Box { val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } // Show the button if the first visible item is past // the first item. We use a remembered derived state to // minimize unnecessary compositions val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() } } }
Lire l'état directement dans la composition est utile lorsque vous devez mettre à jour d'autres composables d'UI, mais dans certains cas, l'événement n'a pas besoin d'être traité dans la même composition. Par exemple, lors de l'envoi d'un événement analytics une fois que l'utilisateur a fait défiler la page au-delà d'un certain point. Pour gérer efficacement ce cas de figure, nous pouvons utiliser snapshotFlow()
:
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .map { index -> index > 0 } .distinctUntilChanged() .filter { it } .collect { MyAnalyticsService.sendScrolledPastFirstItemEvent() } }
LazyListState
fournit également des informations sur tous les éléments affichés actuellement et leurs contours à l'écran, via la propriété layoutInfo
. Pour en savoir plus, consultez LazyListLayoutInfo
.
Contrôler la position de défilement
En plus de réagir à la position de défilement, les applications peuvent aussi contrôler la position de défilement.
LazyListState
vous permet d'effectuer cette action via la fonction scrollToItem()
, qui enregistre "immédiatement" la position de défilement, et animateScrollToItem()
, qui fait défiler une animation (également appelée défilement fluide) :
@Composable fun MessageList(messages: List<Message>) { val listState = rememberLazyListState() // Remember a CoroutineScope to be able to launch val coroutineScope = rememberCoroutineScope() LazyColumn(state = listState) { // ... } ScrollToTopButton( onClick = { coroutineScope.launch { // Animate scroll to the first item listState.animateScrollToItem(index = 0) } } ) }
Ensembles de données volumineux (pagination)
La bibliothèque Paging permet aux applications de prendre en charge des listes d'éléments étendues, de charger et d'afficher de petits segments de la liste si nécessaire. Paging 3.0 et ses versions ultérieures sont compatibles avec Compose via la bibliothèque androidx.paging:paging-compose
.
Pour afficher une liste de contenu paginé, nous pouvons utiliser la fonction d'extension collectAsLazyPagingItems()
, puis transmettre les valeurs LazyPagingItems
à items()
dans notre LazyColumn
. Comme pour la pagination dans les vues, vous pouvez afficher des espaces réservés pendant le chargement des données en vérifiant si item
est null
:
@Composable fun MessageList(pager: Pager<Int, Message>) { val lazyPagingItems = pager.flow.collectAsLazyPagingItems() LazyColumn { items( lazyPagingItems.itemCount, key = lazyPagingItems.itemKey { it.id } ) { index -> val message = lazyPagingItems[index] if (message != null) { MessageRow(message) } else { MessagePlaceholder() } } } }
Conseils pour utiliser les mises en page différées
Voici quelques conseils pour vous assurer que vos mises en page différées fonctionnent comme prévu.
Évitez d'utiliser des éléments de taille 0 px
Cela peut se produire lorsque, par exemple, vous prévoyez de récupérer de manière asynchrone certaines données telles que des images afin de remplir les éléments de votre liste ultérieurement. La mise en page différée pourrait ainsi composer tous ses éléments dans la première mesure, car leur hauteur est de 0 pixel et tous les éléments pourraient s'afficher dans la fenêtre d'affichage. Une fois les éléments chargés et leur hauteur développée, les mises en page différées suppriment tous les autres éléments qui ont été composés inutilement la première fois, car ils ne peuvent en fait pas être placés dans la fenêtre d'affichage. Pour éviter cela, vous devez définir la taille par défaut de vos éléments, de sorte que la mise en page différée puisse calculer correctement le nombre d'éléments pouvant tenir dans la fenêtre d'affichage :
@Composable fun Item(imageUrl: String) { AsyncImage( model = rememberAsyncImagePainter(model = imageUrl), modifier = Modifier.size(30.dp), contentDescription = null // ... ) }
Lorsque vous connaissez la taille approximative de vos éléments après le chargement asynchrone des données, nous vous recommandons de vous assurer que leur taille reste la même avant et après le chargement, par exemple en ajoutant des espaces réservés. Cela permettra de conserver la bonne position de défilement.
Éviter l'imbrication de composants à faire défiler dans la même direction
Cela s'applique uniquement aux cas d'imbrication d'enfants à défilement sans taille prédéfinie dans un autre parent à défilement parallèle. Par exemple, en essayant d'imbriquer un LazyColumn
enfant sans hauteur fixe dans un parent Column
à défilement vertical :
// throws IllegalStateException Column( modifier = Modifier.verticalScroll(state) ) { LazyColumn { // ... } }
Vous pouvez obtenir le même résultat en encapsulant tous vos composables dans un LazyColumn
parent et en utilisant son DSL pour transmettre différents types de contenus. Ainsi, vous n'émettez qu'un seul élément et plusieurs éléments de liste au même endroit :
LazyColumn { item { Header() } items(data) { item -> PhotoItem(item) } item { Footer() } }
N'oubliez pas que les cas où vous imbriquez des mises en page avec des directions différentes, par exemple un parent Row
déroulant et un LazyColumn
enfant sont autorisés :
Row( modifier = Modifier.horizontalScroll(scrollState) ) { LazyColumn { // ... } }
Il en est de même pour les cas où vous utilisez toujours les mêmes mises en page dans la même direction, mais où vous définissez également une taille fixe sur les éléments enfants imbriqués :
Column( modifier = Modifier.verticalScroll(scrollState) ) { LazyColumn( modifier = Modifier.height(200.dp) ) { // ... } }
Évitez de placer plusieurs éléments dans un élément
Dans cet exemple, le deuxième élément lambda émet deux éléments dans un bloc :
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Item(2) } item { Item(3) } // ... }
Les mises en page différées gèrent ce processus comme prévu : les éléments sont disposés les uns après les autres, comme s'il s'agissait d'éléments différents. Toutefois, cela pose quelques problèmes.
Lorsque plusieurs éléments sont émis pour un même élément, ils sont traités comme une seule entité, ce qui signifie qu'ils ne peuvent plus être composés individuellement. Si un élément devient visible à l'écran, tous les éléments correspondant à l'élément doivent être composés et mesurés. Cependant, une utilisation excessive peut nuire aux performances. Un regroupement de tous les éléments dans un seul et même élément irait à l'encontre de l'objectif de la mise en page différée. Hormis les problèmes de performances potentiels, le fait d'insérer davantage d'éléments dans un élément peut également interférer avec scrollToItem()
et animateScrollToItem()
.
Toutefois, il existe des cas d'utilisation valides pour placer plusieurs éléments dans un même élément, comme des séparateurs dans une liste. Les séparateurs ne doivent pas être des indicateurs de défilement, car ils ne doivent pas être considérés comme des éléments indépendants. De plus, les séparateurs sont petits, donc les performances ne seront pas affectées. Un séparateur devra probablement être visible lorsque l'élément qui le précède est visible. Il pourra ainsi appartenir à l'élément précédent :
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Divider() } item { Item(2) } // ... }
Envisagez d'utiliser des dispositions personnalisées
Généralement, les listes différées comportent de nombreux éléments et occupent plus d'espace que le conteneur à défilement. Cependant, lorsque votre liste contient peu d'éléments, votre design peut avoir des exigences plus spécifiques quant à leur positionnement dans la fenêtre d'affichage.
Pour ce faire, vous pouvez utiliser l'élément Arrangement
personnalisé et le transmettre à LazyColumn
. Dans l'exemple suivant, l'objet TopWithFooter
doit se contenter d'implémenter la méthode arrange
. Tout d'abord, il dispose les éléments les uns après les autres. Ensuite, si la hauteur totale utilisée est inférieure à la hauteur de la fenêtre d'affichage, le pied de page sera placé en bas :
object TopWithFooter : Arrangement.Vertical { override fun Density.arrange( totalSize: Int, sizes: IntArray, outPositions: IntArray ) { var y = 0 sizes.forEachIndexed { index, size -> outPositions[index] = y y += size } if (y < totalSize) { val lastIndex = outPositions.lastIndex outPositions[lastIndex] = totalSize - sizes.last() } } }
Envisagez d'ajouter contentType
À partir de la version 1.2 de Compose, pensez à ajouter contentType
à vos listes ou grilles afin d'optimiser les performances de votre mise en page différée. Cela vous permet de spécifier le type de contenu pour chaque élément de la mise en page lorsque vous composez une liste ou une grille composée de plusieurs types d'éléments :
LazyColumn { items(elements, contentType = { it.type }) { // ... } }
Lorsque vous fournissez l'élément contentType
, Compose ne peut réutiliser les compositions qu'entre les éléments du même type. La réutilisation est plus efficace lorsque vous composez des éléments de structure similaire. En fournissant les types de contenu, vous vous assurez que Compose n'essaie pas de composer un élément de type A au-dessus d'un élément complètement différent de type B. Ainsi, vous tirerez davantage parti de la réutilisation des compositions et optimiserez les performances de vos mises en page différées.
Évaluation des performances
Vous ne pouvez mesurer de manière fiable les performances d'une mise en page différée que lorsque vous utilisez le mode de publication et que l'optimisation R8 est activée. Sur les versions de débogage, le défilement différé de la mise en page peut sembler plus lent. Pour en savoir plus, consultez la page Performances de Compose.
Recommandations personnalisées
- Remarque : Le texte du lien s'affiche lorsque JavaScript est désactivé
- Migrer
RecyclerView
vers une liste paressseuse - Enregistrer l'état de l'UI dans Compose
- Kotlin pour Jetpack Compose