LazyColumn — eine Jetpack-Compose-Komponente zur Darstellung scrollbarer Listen mit träger Ladung. Im Gegensatz zu RecyclerView erstellt LazyColumn keine ViewHolder und verwendet kein XML — alle Elemente werden deklarativ durch composable-Funktionen beschrieben. Wir zeigen, was LazyColumn ist, wie LazyListState und die items-API funktionieren und wie Sie eine Leistung von 60 FPS erreichen. Bei IT Sectr verwenden wir LazyColumn in allen neuen Compose-Projekten. Zum Vergleich mit dem klassischen Ansatz lesen Sie den Artikel über RecyclerView.
Wichtige Punkte
items(), itemsIndexed(), items() mit Schlüsseln zur Datenbindung.LazyColumn ist eine composable-Funktion aus der Foundation-Compose-Bibliothek, die eine vertikale Liste von Elementen mit träger Ladung anzeigt: Nur sichtbare oder nahe Elemente (Prefetch-Zone) werden erstellt und komponiert. LazyColumn erschien in Compose 1.0 (2021) zusammen mit LazyRow (horizontale Liste) und LazyVerticalGrid (Raster).
Träge Ladung funktioniert über den SubcomposeLayout-Mechanismus: LazyColumn misst den verfügbaren Platz, fordert den sichtbaren Bereich von LayoutInfo an und komponiert nur Elemente in diesem Bereich. Elemente, die aus dem Bildschirm scrollen, verlassen die Komposition (außer dem Prefetch-Puffer). Laut Google (Android Performance, 2026) hält LazyColumn 60 FPS beim Scrollen durch eine Liste von 10000+ Elementen auf Mittelklasse-Geräten.
Column mit vertikalem Scrollen ist eine normale Anordnung aller Elemente untereinander mit dem verticalScroll-Modifikator. Column ist nicht träge: Alle untergeordneten Elemente werden sofort komponiert, auch wenn sie nicht sichtbar sind. Bei einer Liste von 200+ Elementen verursacht Column eine erhebliche Verzögerung beim Start. LazyColumn komponiert nur sichtbare Elemente + Puffer (standardmäßig 1 Bildschirm vorwärts/rückwärts), was es für lange Listen unverzichtbar macht.
items API ist eine Reihe von Erweiterungsfunktionen für LazyColumn, die eine Datenliste und einen DSL-Deskriptor für jedes Element akzeptieren. Die Hauptfunktionen sind: items(count, key, itemContent) für eine feste Anzahl, items(list, key, itemContent) für eine Liste, itemsIndexed(list, key, itemContent) mit Index. Der key-Parameter ist für die Leistung obligatorisch — er gibt Compose eine stabile Elementkennung.
// Basis-LazyColumn mit itemContent
@Composable
fun ArticleList(articles: List<Article>) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// Header — nicht träge, immer am Anfang
item {
Text("Neueste Artikel", style = MaterialTheme.typography.headlineMedium)
}
// Artikelliste mit Schlüssel nach id
items(articles, key = { it.id }) { article ->
ArticleCard(
title = article.title,
summary = article.summary,
onClick = { onArticleClick(article.id) }
)
}
// Footer mit Ladeanzeiger
item {
CircularProgressIndicator(
modifier = Modifier.fillMaxWidth().padding(16.dp)
)
}
}
}
// Benutzerdefiniertes Listenelement
@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(): Verwenden Sie item {} für einzelne Elemente, die immer in der Liste vorhanden sind (Kopfzeile, Fußzeile, Trennzeichen). Verwenden Sie items() für eine dynamische Datenliste. items() akzeptiert ein Iterable oder Int und einen DSL-Block, der für jedes Element aufgerufen wird. Kombinieren Sie item und items in einem LazyColumn: Die Reihenfolge der Aufrufe bestimmt die Anzeigereihenfolge.
LazyListState ist ein Objekt, das den Listenstatus speichert: aktuelle Bildlaufposition (firstVisibleItemIndex, firstVisibleItemScrollOffset), sichtbare Elemente (layoutInfo) und Methoden zum programmgesteuerten Scrollen (scrollToItem, animateScrollToItem). LazyListState wird über rememberLazyListState() erstellt und über den state-Parameter an LazyColumn übergeben.
// LazyColumn mit Beibehaltung der Bildlaufposition
@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)
}
}
// Schaltfläche "Nach oben" erscheint nach dem 10. 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, "Nach oben")
}
}
}
}
// Informationen zu sichtbaren Elementen abrufen
@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("Zeige $firstItem–$lastItem von $totalItems")
}
Programmgesteuertes Scrollen erfolgt über listState-suspend-Funktionen: scrollToItem(index, scrollOffset) — sofortige Bewegung (ohne Animation), animateScrollToItem(index) — mit Animation. Zum Scrollen von einer Koroutine aus verwenden Sie LaunchedEffect oder coroutineScope.launch. Wichtig: Rufen Sie keine suspend-Funktionen aus der Komposition auf (innerhalb von @Composable) — verwenden Sie Lambdas (onClick, LaunchedEffect).
LazyColumn und RecyclerView lösen dasselbe Problem — effiziente Anzeige langer Listen. Der Unterschied liegt in der Architektur: RecyclerView verwendet ViewHolder und Adapter (klassische Android-Ansicht), LazyColumn verwendet deklarative composable-Funktionen ohne XML oder ViewHolder. Die Wahl hängt vom Technologie-Stack des Projekts ab: Compose vs View-basierte UI.
| Parameter | LazyColumn (Compose) | RecyclerView (View) |
|---|---|---|
| Layout | Composable-Funktionen (Kotlin DSL) | XML-Dateien + ViewBinding |
| Adapter | items() / itemsIndexed() DSL | RecyclerView.Adapter + ViewHolder |
| Sortieren/Filtern | Snapshot state + derivedStateOf | DiffUtil + AsyncListDiffer |
| Animation | AnimatedVisibility + Modifier.animateItem | DefaultItemAnimator |
| Verschiedene Elementtypen | item {}/items() + when nach Typ | getItemViewType + mehrere ViewHolder |
| Zur Position scrollen | listState.animateScrollToItem() | layoutManager.scrollToPosition() |
| Prefetch | In-process (SubcomposeLayout-Puffer) | RecycledViewPool + GapWorker |
| Netzwerk/Paginierung | collectAsLazyPagingItems() + Paging 3 | PagingDataAdapter + Paging 3 |
Googles Empfehlung (Android Developers, 2026): Verwenden Sie für neue Projekte auf Jetpack Compose LazyColumn. Für View-basierte Projekte verwenden Sie RecyclerView. LazyColumn erfordert Compose, das etwa 3–5 MB zur APK hinzufügt — berücksichtigen Sie dies bei der Unterstützung älterer Geräte. Wenn ein Projekt bereits das View-System verwendet, kann LazyColumn nicht ohne AndroidView auf einem Bildschirm mit XML-Layout koexistieren — in solchen Fällen ist es einfacher, bei RecyclerView zu bleiben.
LazyVerticalGrid ist eine Version von LazyColumn für Raster mit fester Spaltenanzahl. LazyHorizontalGrid ist ein horizontales Raster mit festen Zeilen. Beide Komponenten verwenden dieselben Prinzipien der trägen Ladung und dieselbe items-API wie LazyColumn. Der Parameter columns bestimmt die Anzahl der Spalten über GridCells.Fixed(N) oder GridCells.Adaptive(minSize).
// Raster mit adaptiven Spalten
@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 Liste (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 berechnet automatisch die Anzahl der Spalten basierend auf der minimalen Zellgröße. Zum Beispiel platziert GridCells.Adaptive(128.dp) auf einem 360 dp breiten Bildschirm 2 Spalten, auf einem 600 dp Tablet — 4 Spalten. GridCells.Fixed(2) zeigt immer genau 2 Spalten. Adaptive ist für die Multi-Screen-Anpassung zu bevorzugen — es passt sich automatisch an die Bildschirmbreite ohne Media Queries an.
Die Leistung von LazyColumn hängt von drei Faktoren ab: Schlüsselstabilität (key), Inhaltstyp (contentType) und Prefetch-Puffer (beyondBounds). Standardmäßig puffert LazyColumn 1 Bildschirm vorwärts und 1 rückwärts. Google (Android Developers, 2026) empfiehlt, diese Parameter für Listen mit unterschiedlichen Elementtypen oder komplexer Komposition zu konfigurieren.
| Parameter | Beschreibung | Empfehlung |
|---|---|---|
| key | Stabile Elementkennung | Geben Sie immer key = { it.id } an. Ohne key verwendet Compose den Index, und die Neuanordnung von Elementen zerstört die Animation. |
| contentType | Inhaltstyp zur Pool-Trennung | Geben Sie dies für Listen mit unterschiedlichen Elementtypen (Text + Bild + Anzeige) an. Compose wird die Komposition nur innerhalb desselben contentType wiederverwenden. |
| beyondBounds | Anzahl der Prefetch-Bildschirme | Erhöhen Sie auf 2–3 für Listen mit schweren Bildern. Standard ist 1 Bildschirm. |
| Modifier.animateItem | Animationsänderung der Position | Verwenden Sie für animiertes Sortieren und Filtern (Compose 1.7+). |
// Optimierte LazyColumn mit contentType und prefetch
@Composable
fun OptimizedFeed(items: List<FeedItem>) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
beyondBoundsPageCount = 2 // 2 Bildschirme prefetch
) {
items(
items = items,
key = { it.id },
contentType = { it.type } // Aufteilung nach Typ
) { item ->
when (item) {
is FeedItem.Post -> PostView(item)
is FeedItem.Ad -> AdView(item)
is FeedItem.Suggested -> SuggestedView(item)
}
}
}
}
// Leistungsmessung über listState
@Composable
fun ScrollPerformance(listState: LazyListState) {
val scrollInfo = listState.layoutInfo
val visibleCount = scrollInfo.visibleItemsInfo.size
val total = scrollInfo.totalItemsCount
// derivedStateOf — komponiert nicht neu, wenn sich der Wert nicht geändert hat
val scrollProgress by remember {
derivedStateOf {
if (total > 0) visibleCount.toFloat() / total else 0f
}
}
LinearProgressIndicator(
progress = scrollProgress,
modifier = Modifier.fillMaxWidth().height(2.dp)
)
}
Häufige Fehler: (1) Fehlender key — Elemente werden bei jeder Listenänderung neu komponiert; (2) Verwendung von mutableStateListOf ohne Snapshot-Flows — Änderungen werden möglicherweise nicht verfolgt; (3) Aufruf von suspend-Funktionen innerhalb von itemContent — unterbricht die Komposition; (4) Kein contentType für gemischte Typen — Compose verwendet die Anzeigenkomposition für Textbeiträge wieder und verursacht visuelle Artefakte. Bei IT Sectr fügen wir contentType zu allen Listen mit drei oder mehr Inhaltstypen hinzu.
Häufig gestellte Fragen
Überprüfen Sie drei Parameter: (1) key — ohne einen stabilen Schlüssel kann Compose die Komposition nicht wiederverwenden, (2) contentType — für Listen mit unterschiedlichen Elementtypen, (3) beyondBoundsPageCount — auf 2 für Bilder erhöhen. Verlagern Sie schwere Berechnungen innerhalb von itemContent in remember mit einem Schlüssel nach Daten. Verwenden Sie Modifier.drawWithContent anstelle von Image für einfache Grafiken.
Verwenden Sie LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) für Abstände zwischen Elementen. Für einen visuellen Trenner mit Linie fügen Sie item { Divider() } zwischen items hinzu. Automatischer Trenner: items(items, key = { it.id }) { item -> ... }, und zwischen jedem Element item { HorizontalDivider() } einfügen. Verwenden Sie dazu LazyListScope.items(list, key) { /* element */ } und separat LazyListScope.item { Divider() }.
LazyColumn hat standardmäßig eine unendliche Höhe (fillMaxSize). Innerhalb einer Column ohne feste Höhe kann LazyColumn seine Größe nicht bestimmen. Lösung: (1) Legen Sie eine feste Höhe für LazyColumn fest: Modifier.height(400.dp), (2) verwenden Sie Modifier.weight(1f) innerhalb von Column, (3) verschachteln Sie LazyColumn nicht in einem vertikal scrollbaren Container — verwenden Sie LazyColumn als Wurzelelement mit item {} für Kopf- und Fußzeile.
LazyColumn reagiert automatisch auf State-Änderungen. Wenn Daten in mutableStateListOf oder mutableStateOf gespeichert sind, komponiert Compose nur die geänderten Elemente neu. Für Listen aus ViewModel verwenden Sie collectAsState() mit Flow. Beim Aktualisieren der Liste mit key animiert Compose Änderungen (Hinzufügen, Entfernen, Neuanordnen) automatisch über Modifier.animateItemPlacement().
LazyColumn — für einspaltige Listen (Feed, Chat, Kommentare). LazyVerticalGrid — für Raster (Galerie, Katalog, Symbole). Wenn Elemente auf dem Telefon in einer Spalte und auf dem Tablet in zwei Spalten sein sollen — verwenden Sie LazyVerticalGrid mit GridCells.Adaptive. Wenn ein gemischtes Raster benötigt wird (unterschiedliche Spaltenanzahl in einem Abschnitt) — verwenden Sie LazyColumn und fügen Sie horizontales LazyRow oder verschachteltes LazyVerticalGrid innerhalb von items ein.
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch