LazyColumn — un composant Jetpack Compose pour afficher des listes défilables avec chargement paresseux. Contrairement à RecyclerView, LazyColumn ne crée pas de ViewHolders et n'utilise pas XML — tous les éléments sont décrits de manière déclarative via des fonctions composables. Nous montrons ce qu'est LazyColumn, comment fonctionnent LazyListState et l'API items, et comment atteindre des performances de 60 FPS. Chez IT Sectr, nous utilisons LazyColumn dans tous les nouveaux projets Compose. Pour une comparaison avec l'approche classique, lisez l'article sur RecyclerView.
Points clés
items(), itemsIndexed(), items() avec clés pour la liaison de données.LazyColumn est une fonction composable de la bibliothèque Foundation Compose qui affiche une liste verticale d'éléments avec chargement paresseux : seuls les éléments visibles à l'écran ou à proximité (zone de préchargement) sont créés et composés. LazyColumn est apparu dans Compose 1.0 (2021) avec LazyRow (liste horizontale) et LazyVerticalGrid (grille).
Le chargement paresseux fonctionne via le mécanisme SubcomposeLayout : LazyColumn mesure l'espace disponible, demande la plage visible à LayoutInfo et compose uniquement les éléments de cette plage. Les éléments qui sortent de l'écran quittent la composition (sauf le tampon de préchargement). Selon Google (Android Performance, 2026), LazyColumn maintient 60 FPS lors du défilement d'une liste de 10000+ éléments sur des appareils de gamme moyenne.
Column avec défilement vertical est un agencement normal de tous les éléments les uns sous les autres avec le modificateur verticalScroll appliqué. Column n'est pas paresseuse : tous les éléments enfants sont composés immédiatement, même s'ils ne sont pas visibles. Pour une liste de 200+ éléments, Column provoque un décalage important au démarrage. LazyColumn compose uniquement les éléments visibles + tampon (1 écran avant/arrière par défaut), ce qui le rend indispensable pour les longues listes.
items API est un ensemble de fonctions d'extension pour LazyColumn qui acceptent une liste de données et un descripteur DSL pour chaque élément. Les principales fonctions sont : items(count, key, itemContent) pour un nombre fixe, items(list, key, itemContent) pour une liste, itemsIndexed(list, key, itemContent) avec index. Le paramètre key est obligatoire pour les performances — il donne à Compose un identifiant d'élément stable.
// LazyColumn de base avec itemContent
@Composable
fun ArticleList(articles: List<Article>) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// Header — pas paresseux, toujours au début
item {
Text("Derniers articles", style = MaterialTheme.typography.headlineMedium)
}
// Liste d'articles avec clé par id
items(articles, key = { it.id }) { article ->
ArticleCard(
title = article.title,
summary = article.summary,
onClick = { onArticleClick(article.id) }
)
}
// Footer avec indicateur de chargement
item {
CircularProgressIndicator(
modifier = Modifier.fillMaxWidth().padding(16.dp)
)
}
}
}
// Élément de liste personnalisé
@Composable
fun ArticleCard(title: String, summary: String, onClick: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
) {
Column(modifier = Modifier.padding(12.dp)) {
Text(text = title, style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(4.dp))
Text(text = summary, style = MaterialTheme.typography.bodyMedium)
}
}
}
item {} vs items() : utilisez item {} pour les éléments uniques toujours présents dans la liste (en-tête, pied de page, séparateurs). Utilisez items() pour une liste de données dynamique. items() accepte un Iterable ou Int et un bloc DSL appelé pour chaque élément. Combinez item et items dans un même LazyColumn : l'ordre des appels détermine l'ordre d'affichage.
LazyListState est un objet qui stocke l'état de la liste : position actuelle du défilement (firstVisibleItemIndex, firstVisibleItemScrollOffset), éléments visibles (layoutInfo) et méthodes de défilement programmatique (scrollToItem, animateScrollToItem). LazyListState est créé via rememberLazyListState() et transmis à LazyColumn via le paramètre state.
// LazyColumn avec préservation de la position de défilement
@Composable
fun ScrollingList(items: List<Item>) {
val listState = rememberLazyListState()
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(state = listState) {
items(items, key = { it.id }) { item ->
ListItemView(item = item)
}
}
// Bouton "Vers le haut" apparaît après le 10e élément
val showButton by remember {
derivedStateOf { listState.firstVisibleItemIndex > 10 }
}
AnimatedVisibility(visible = showButton) {
FloatingActionButton(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
onClick = {
coroutineScope.launch {
listState.animateScrollToItem(0)
}
}
) {
Icon(Icons.Default.KeyboardArrowUp, "Vers le haut")
}
}
}
}
// Obtention d'informations sur les éléments visibles
@Composable
fun ListDebugInfo(listState: LazyListState) {
val visibleItems = listState.layoutInfo.visibleItemsInfo
val totalItems = listState.layoutInfo.totalItemsCount
val firstItem = visibleItems.firstOrNull()?.index
val lastItem = visibleItems.lastOrNull()?.index
Text("Affichage $firstItem–$lastItem sur $totalItems")
}
Le défilement programmatique s'effectue via les fonctions suspend de listState : scrollToItem(index, scrollOffset) — déplacement instantané (sans animation), animateScrollToItem(index) — avec animation. Pour le défilement depuis une coroutine, utilisez LaunchedEffect ou coroutineScope.launch. Important : n'appelez pas de fonctions suspend depuis la composition (dans @Composable) — utilisez des lambdas (onClick, LaunchedEffect).
LazyColumn et RecyclerView résolvent le même problème — l'affichage efficace de longues listes. La différence réside dans l'architecture : RecyclerView utilise ViewHolder et Adapter (vue Android classique), LazyColumn utilise des fonctions composables déclaratives sans XML ni ViewHolder. Le choix dépend de la pile technologique du projet : Compose vs UI basée sur View.
| Paramètre | LazyColumn (Compose) | RecyclerView (View) |
|---|---|---|
| Disposition | Fonctions composables (Kotlin DSL) | Fichiers XML + ViewBinding |
| Adaptateur | items() / itemsIndexed() DSL | RecyclerView.Adapter + ViewHolder |
| Tri/Filtrage | Snapshot state + derivedStateOf | DiffUtil + AsyncListDiffer |
| Animation | AnimatedVisibility + Modifier.animateItem | DefaultItemAnimator |
| Différents types d'éléments | item {}/items() + when par type | getItemViewType + plusieurs ViewHolders |
| Défiler vers une position | listState.animateScrollToItem() | layoutManager.scrollToPosition() |
| Préchargement | En processus (tampon SubcomposeLayout) | RecycledViewPool + GapWorker |
| Réseau/Pagination | collectAsLazyPagingItems() + Paging 3 | PagingDataAdapter + Paging 3 |
Recommandation de Google (Android Developers, 2026) : pour les nouveaux projets sur Jetpack Compose, utilisez LazyColumn. Pour les projets basés sur View, utilisez RecyclerView. LazyColumn nécessite Compose, qui ajoute environ 3–5 Mo à l'APK — tenez-en compte lors du support des appareils anciens. Si un projet utilise déjà le système View, LazyColumn ne peut pas coexister sur un même écran avec une mise en page XML sans AndroidView — dans ces cas, il est plus simple de rester avec RecyclerView.
LazyVerticalGrid est une version de LazyColumn pour les grilles avec un nombre fixe de colonnes. LazyHorizontalGrid est une grille horizontale avec des lignes fixes. Les deux composants utilisent les mêmes principes de chargement paresseux et la même API items que LazyColumn. Le paramètre columns détermine le nombre de colonnes via GridCells.Fixed(N) ou GridCells.Adaptive(minSize).
// Grille avec colonnes adaptatives
@Composable
fun PhotoGrid(photos: List<Photo>) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 128.dp),
contentPadding = PaddingValues(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(photos, key = { it.id }) { photo ->
AsyncImage(
model = photo.url,
contentDescription = photo.title,
modifier = Modifier.aspectRatio(1f)
)
}
}
}
// Liste horizontale (LazyRow)
@Composable
fun CategoryCarousel(categories: List<Category>) {
LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
items(categories, key = { it.id }) { category ->
FilterChip(
selected = category.isSelected,
onClick = { onCategoryClick(category.id) },
label = { Text(category.name) }
)
}
}
}
GridCells.Adaptive calcule automatiquement le nombre de colonnes en fonction de la taille minimale de la cellule. Par exemple, GridCells.Adaptive(128.dp) sur un écran de 360 dp de large place 2 colonnes, sur une tablette de 600 dp — 4 colonnes. GridCells.Fixed(2) affiche toujours exactement 2 colonnes. Adaptive est préférable pour l'adaptation multi-écran — il s'ajuste automatiquement à la largeur de l'écran sans requêtes média.
Les performances de LazyColumn dépendent de trois facteurs : la stabilité de la clé (key), le type de contenu (contentType) et le tampon de préchargement (beyondBounds). Par défaut, LazyColumn met en tampon 1 écran avant et 1 arrière. Google (Android Developers, 2026) recommande de configurer ces paramètres pour les listes avec différents types d'éléments ou une composition complexe.
| Paramètre | Description | Recommandation |
|---|---|---|
| key | Identifiant stable d'élément | Spécifiez toujours key = { it.id }. Sans key, Compose utilise l'index, et le réordonnancement des éléments casse l'animation. |
| contentType | Type de contenu pour la séparation des pools | Spécifiez pour les listes avec différents types d'éléments (texte + image + publicité). Compose ne réutilisera la composition qu'au sein du même contentType. |
| beyondBounds | Nombre d'écrans de préchargement | Augmentez à 2–3 pour les listes avec des images lourdes. Par défaut, 1 écran. |
| Modifier.animateItem | Animation de changement de position | Utilisez pour le tri et le filtrage animés (Compose 1.7+). |
// LazyColumn optimisée avec contentType et prefetch
@Composable
fun OptimizedFeed(items: List<FeedItem>) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
beyondBoundsPageCount = 2 // préchargement de 2 écrans
) {
items(
items = items,
key = { it.id },
contentType = { it.type } // division par type
) { item ->
when (item) {
is FeedItem.Post -> PostView(item)
is FeedItem.Ad -> AdView(item)
is FeedItem.Suggested -> SuggestedView(item)
}
}
}
}
// Mesure des performances via listState
@Composable
fun ScrollPerformance(listState: LazyListState) {
val scrollInfo = listState.layoutInfo
val visibleCount = scrollInfo.visibleItemsInfo.size
val total = scrollInfo.totalItemsCount
// derivedStateOf — ne recompose pas si la valeur n'a pas changé
val scrollProgress by remember {
derivedStateOf {
if (total > 0) visibleCount.toFloat() / total else 0f
}
}
LinearProgressIndicator(
progress = scrollProgress,
modifier = Modifier.fillMaxWidth().height(2.dp)
)
}
Erreurs courantes : (1) absence de key — les éléments sont recomposés à tout changement de liste ; (2) utilisation de mutableStateListOf sans flux snapshot — les changements peuvent ne pas être suivis ; (3) appel de fonctions suspend dans itemContent — interrompt la composition ; (4) sans contentType pour les types mixtes — Compose réutilise la composition des publicités pour les publications textuelles, provoquant des artefacts visuels. Chez IT Sectr, nous ajoutons contentType à toutes les listes ayant trois types de contenu ou plus.
Questions fréquentes
Vérifiez trois paramètres : (1) key — sans clé stable, Compose ne peut pas réutiliser la composition, (2) contentType — pour les listes avec différents types d'éléments, (3) beyondBoundsPageCount — augmentez à 2 pour les images. Déplacez les calculs lourds dans itemContent vers remember avec une clé par données. Utilisez Modifier.drawWithContent au lieu de Image pour les graphiques simples.
Utilisez LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) pour l'espacement entre les éléments. Pour un séparateur visuel avec une ligne, ajoutez item { Divider() } entre les items. Séparateur automatique : items(items, key = { it.id }) { item -> ... }, et entre chaque élément insérez item { HorizontalDivider() }. Pour cela, utilisez LazyListScope.items(list, key) { /* élément */ } et séparément LazyListScope.item { Divider() }.
LazyColumn a une hauteur infinie (fillMaxSize) par défaut. Dans une Column sans hauteur fixe, LazyColumn ne peut pas déterminer sa taille. Solution : (1) définissez une hauteur fixe sur LazyColumn : Modifier.height(400.dp), (2) utilisez Modifier.weight(1f) dans Column, (3) n'imbriquez pas LazyColumn dans un conteneur à défilement vertical — utilisez LazyColumn comme élément racine avec item {} pour l'en-tête et le pied de page.
LazyColumn réagit automatiquement aux changements d'état. Si les données sont stockées dans mutableStateListOf ou mutableStateOf, Compose ne recompose que les éléments modifiés. Pour les listes provenant de ViewModel, utilisez collectAsState() avec Flow. Lors de la mise à jour de la liste avec key, Compose anime automatiquement les changements (ajout, suppression, réorganisation) via Modifier.animateItemPlacement().
LazyColumn — pour les listes à une colonne (fil d'actualité, chat, commentaires). LazyVerticalGrid — pour les grilles (galerie, catalogue, icônes). Si les éléments doivent être en une colonne sur téléphone et en deux sur tablette — utilisez LazyVerticalGrid avec GridCells.Adaptive. Si une grille mixte est nécessaire (nombre de colonnes différent dans une section) — utilisez LazyColumn et insérez LazyRow horizontal ou LazyVerticalGrid imbriqué dans les items.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi