LazyColumn — Jetpack Compose-component voor het weergeven van scrollbare lijsten met lazy loading. In tegenstelling tot RecyclerView maakt LazyColumn geen ViewHolders aan en gebruikt geen XML — alle elementen worden declaratief beschreven via composable-functies. We laten zien wat LazyColumn is, hoe LazyListState en items API werken, en hoe je prestaties op 60 FPS-niveau kunt bereiken. Bij IT Sectr gebruiken we LazyColumn in al onze nieuwe Compose-projecten. Voor een vergelijking met de klassieke aanpak lees je het artikel over RecyclerView.
Belangrijkste punten
items(), itemsIndexed(), items() met sleutels voor gegevensbinding.LazyColumn — is een composable-functie uit de Foundation Compose-bibliotheek die een verticale lijst met elementen weergeeft met lazy loading: alleen elementen die op het scherm zichtbaar zijn of zich in de buurt bevinden (prefetch-zone) worden aangemaakt en gecomponeerd. LazyColumn verscheen in Compose 1.0 (2021) samen met LazyRow (horizontale lijst) en LazyVerticalGrid (raster).
Lazy loading werkt via het SubcomposeLayout-mechanisme: LazyColumn meet de beschikbare ruimte, vraagt het zichtbare bereik op bij LayoutInfo en componeert alleen elementen in dat bereik. Elementen die van het scherm zijn verdwenen, verlaten de compositie (behalve de prefetch-buffer). Volgens Google-gegevens (Android Performance, 2026) levert LazyColumn 60 FPS bij het scrollen van een lijst met 10000+ elementen op middenklasse apparaten.
Column met verticaal scrollen — is de gewone plaatsing van alle elementen onder elkaar met toepassing van de verticalScroll-modifier. Column is niet lui: alle onderliggende elementen worden onmiddellijk gecomponeerd, zelfs als ze niet zichtbaar zijn. Voor een lijst met 200+ elementen veroorzaakt Column aanzienlijke vertraging bij het starten. LazyColumn componeert alleen zichtbare elementen + buffer (standaard 1 scherm vooruit/achteruit), wat het onmisbaar maakt voor lange lijsten.
items API — set extensiefuncties voor LazyColumn die een gegevenslijst en een DSL-beschrijving voor elk element accepteren. Hoofdfuncties: items(count, key, itemContent) voor een vast aantal, items(list, key, itemContent) voor een lijst, itemsIndexed(list, key, itemContent) met index. De parameter key — verplichte voorwaarde voor prestaties — geeft Compose een stabiele identificatie van het element.
// Basis LazyColumn met itemContent
@Composable
fun ArticleList(articles: List<Article>) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// Header — niet lui, altijd aan het begin
item {
Text("Laatste artikelen", style = MaterialTheme.typography.headlineMedium)
}
// Lijst van artikelen met sleutel op id
items(articles, key = { it.id }) { article ->
ArticleCard(
title = article.title,
summary = article.summary,
onClick = { onArticleClick(article.id) }
)
}
// Footer met laadindicator
item {
CircularProgressIndicator(
modifier = Modifier.fillMaxWidth().padding(16.dp)
)
}
}
}
// Aangepast lijstelement
@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(): gebruik item {} voor afzonderlijke elementen die altijd in de lijst aanwezig zijn (header, footer, scheidingstekens). Gebruik items() voor een dynamische gegevenslijst. items() accepteert Iterable of Int en een DSL-blok dat voor elk element wordt aangeroepen. Combineer item en items in één LazyColumn: de volgorde van aanroepen bepaalt de weergavevolgorde.
LazyListState — object dat de lijststatus opslaat: huidige scrollpositie (firstVisibleItemIndex, firstVisibleItemScrollOffset), zichtbare elementen (layoutInfo) en methoden voor programmatisch scrollen (scrollToItem, animateScrollToItem). LazyListState wordt aangemaakt via rememberLazyListState() en doorgegeven aan LazyColumn via de parameter state.
// LazyColumn met behoud van scrollpositie
@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)
}
}
// Knop "Omhoog" verschijnt na het 10e element
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, "Naar boven")
}
}
}
}
// Informatie ophalen over zichtbare elementen
@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("$firstItem–$lastItem van $totalItems getoond")
}
Programmatisch scrollen wordt uitgevoerd via de suspend-functies van listState: scrollToItem(index, scrollOffset) — onmiddellijke verplaatsing (zonder animatie), animateScrollToItem(index) — met animatie. Gebruik voor scrollen vanuit een coroutine LaunchedEffect of coroutineScope.launch. Belangrijk: roep geen suspend-functies aan vanuit compositie (binnen @Composable) — gebruik lambda's (onClick, LaunchedEffect).
LazyColumn en RecyclerView lossen dezelfde taak op — efficiënte weergave van lange lijsten. Het verschil zit in de architectuur: RecyclerView gebruikt ViewHolder en Adapter (klassieke Android View), LazyColumn — declaratieve composable-functies zonder XML en ViewHolder. De keuze hangt af van de technologiestack van het project: Compose vs View-based UI.
| Parameter | LazyColumn (Compose) | RecyclerView (View) |
|---|---|---|
| Layout | Composable-functies (Kotlin DSL) | XML-bestanden + ViewBinding |
| Adapter | items() / itemsIndexed() DSL | RecyclerView.Adapter + ViewHolder |
| Sorteren/filteren | Snapshot state + derivedStateOf | DiffUtil + AsyncListDiffer |
| Animatie | AnimatedVisibility + Modifier.animateItem | DefaultItemAnimator |
| Verschillende elementtypen | item {}/items() + when op type | getItemViewType + meerdere ViewHolders |
| Scrollen naar positie | listState.animateScrollToItem() | layoutManager.scrollToPosition() |
| Prefetch | In-process (SubcomposeLayout-buffer) | RecycledViewPool + GapWorker |
| Netwerk/paginatie | collectAsLazyPagingItems() + Paging 3 | PagingDataAdapter + Paging 3 |
Google-advies (Android Developers, 2026): gebruik voor nieuwe projecten op Jetpack Compose LazyColumn. Gebruik voor View-based projecten RecyclerView. LazyColumn vereist Compose, wat ongeveer 3–5 MB aan de APK toevoegt — houd hier rekening mee bij ondersteuning van oude apparaten. Als het project al het View-systeem gebruikt, kan LazyColumn niet op hetzelfde scherm met XML-layout bestaan zonder AndroidView — in zulke gevallen is het eenvoudiger om bij RecyclerView te blijven.
LazyVerticalGrid — versie van LazyColumn voor rasters met een vast aantal kolommen (columns). LazyHorizontalGrid — horizontaal raster met een vast aantal rijen (rows). Beide componenten gebruiken dezelfde principes van lazy loading en dezelfde items API als LazyColumn. De parameter columns bepaalt het aantal kolommen via GridCells.Fixed(N) of GridCells.Adaptive(minSize).
// Raster met adaptieve kolommen
@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)
)
}
}
}
// Horizontale lijst (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 berekent automatisch het aantal kolommen op basis van de minimale celgrootte. Bijvoorbeeld, GridCells.Adaptive(128.dp) plaatst op een scherm van 360 dp breed 2 kolommen, op een tablet van 600 dp — 4 kolommen. GridCells.Fixed(2) toont altijd precies 2 kolommen. Adaptive heeft de voorkeur voor multi-scherm aanpassing — het past zich automatisch aan de schermbreedte aan zonder media-query's.
Prestaties van LazyColumn zijn afhankelijk van drie factoren: stabiliteit van sleutels (key), inhoudstype (contentType) en prefetch-buffer (beyondBounds). Standaard buffert LazyColumn 1 scherm vooruit en 1 achteruit. Google (Android Developers, 2026) adviseert deze parameters te configureren voor lijsten met verschillende elementtypen of complexe compositie.
| Parameter | Beschrijving | Aanbeveling |
|---|---|---|
| key | Stabiele identificatie van het element | Geef altijd key = { it.id } op. Zonder key gebruikt Compose de index en verstoort het herordenen van elementen de animatie. |
| contentType | Inhoudstype voor scheiding van pools | Specificeer voor lijsten met verschillende elementtypen (tekst + afbeelding + advertentie). Compose hergebruikt compositie alleen binnen dezelfde contentType. |
| beyondBounds | Aantal schermen voor prefetch | Verhoog naar 2–3 voor lijsten met zware afbeeldingen. Standaard 1 scherm. |
| Modifier.animateItem | Animatie van positiewijziging | Gebruik voor geanimeerd sorteren en filteren (Compose 1.7+). |
// Geoptimaliseerde LazyColumn met contentType en prefetch
@Composable
fun OptimizedFeed(items: List<FeedItem>) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
beyondBoundsPageCount = 2 // prefetch 2 schermen
) {
items(
items = items,
key = { it.id },
contentType = { it.type } // scheiding op type
) { item ->
when (item) {
is FeedItem.Post -> PostView(item)
is FeedItem.Ad -> AdView(item)
is FeedItem.Suggested -> SuggestedView(item)
}
}
}
}
// Prestatiemeting via listState
@Composable
fun ScrollPerformance(listState: LazyListState) {
val scrollInfo = listState.layoutInfo
val visibleCount = scrollInfo.visibleItemsInfo.size
val total = scrollInfo.totalItemsCount
// derivedStateOf — wordt niet opnieuw samengesteld als de waarde niet is veranderd
val scrollProgress by remember {
derivedStateOf {
if (total > 0) visibleCount.toFloat() / total else 0f
}
}
LinearProgressIndicator(
progress = scrollProgress,
modifier = Modifier.fillMaxWidth().height(2.dp)
)
}
Veelvoorkomende fouten: (1) ontbreken van key — elementen worden opnieuw gecomponeerd bij elke wijziging van de lijst; (2) gebruik van mutableStateListOf zonder snapshot-streams — wijzigingen worden mogelijk niet bijgehouden; (3) aanroepen van suspend-functies binnen itemContent — onderbreekt compositie; (4) geen contentType voor gemengde typen — Compose hergebruikt de compositie van een advertentie voor een tekstbericht, wat visuele artefacten veroorzaakt. Bij IT Sectr voegen we contentType toe aan alle lijsten met drie of meer inhoudstypen.
Veelgestelde vragen
Controleer drie parameters: (1) key — zonder stabiele sleutel kan Compose compositie niet hergebruiken, (2) contentType — voor lijsten met verschillende elementtypen, (3) beyondBoundsPageCount — verhoog naar 2 voor afbeeldingen. Verplaats zware berekeningen binnen itemContent naar remember met een sleutel op basis van gegevens. Gebruik Modifier.drawWithContent in plaats van Image voor eenvoudige grafische weergave.
Gebruik LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) voor spaties tussen elementen. Voeg voor een visueel scheidingsteken met een lijn item { Divider() } toe tussen items. Automatisch scheidingsteken: items(items, key = { it.id }) { item -> ... }, en voeg tussen elk element item { HorizontalDivider() } in. Gebruik hiervoor LazyListScope.items(list, key) { /* element */ } en apart LazyListScope.item { Divider() }.
LazyColumn heeft standaard een oneindige hoogte (fillMaxSize). Binnen Column zonder vaste hoogte kan LazyColumn de grootte niet bepalen. Oplossing: (1) geef LazyColumn een vaste hoogte: Modifier.height(400.dp), (2) gebruik Modifier.weight(1f) binnen Column, (3) plaats LazyColumn niet in een verticaal scrollbare container — gebruik LazyColumn als root-element met item {} voor header en footer.
LazyColumn reageert automatisch op wijzigingen in State. Als gegevens worden opgeslagen in mutableStateListOf of mutableStateOf, componeert Compose alleen de gewijzigde elementen opnieuw. Gebruik voor lijsten uit ViewModel collectAsState() met Flow. Bij het bijwerken van de lijst met key animeert Compose automatisch wijzigingen (toevoegen, verwijderen, verplaatsen) via Modifier.animateItemPlacement().
LazyColumn — voor lijsten met één kolom (feed, chat, reacties). LazyVerticalGrid — voor rasters (galerij, catalogus, pictogrammen). Als elementen op de telefoon in één kolom en op de tablet in twee kolommen moeten staan — gebruik LazyVerticalGrid met GridCells.Adaptive. Als je een gemengd raster nodig hebt (verschillend aantal kolommen in één sectie) — gebruik LazyColumn en plaats binnen items horizontale LazyRow of geneste LazyVerticalGrid.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook