LazyColumn — komponenta Jetpack Compose pro zobrazení posouvatelných seznamů s líným načítáním. Na rozdíl od RecyclerView, LazyColumn nevytváří ViewHoldery a nepoužívá XML — všechny prvky jsou popsány deklarativně pomocí composable funkcí. Ukazujeme, co je LazyColumn, jak funguje LazyListState a items API, a jak dosáhnout výkonu na úrovni 60 FPS. V IT Sectr používáme LazyColumn ve všech nových Compose projektech. Pro srovnání s klasickým přístupem si přečtěte článek o RecyclerView.
Hlavní body
items(), itemsIndexed(), items() s klíči pro vázání dat.LazyColumn — je composable funkce z knihovny Foundation Compose, která zobrazuje vertikální seznam prvků s líným (lazy) načítáním: vytvářejí se a komponují pouze ty prvky, které jsou viditelné na obrazovce nebo se nacházejí v blízkosti (zóna prefetch). LazyColumn se objevil v Compose 1.0 (2021) spolu s LazyRow (horizontální seznam) a LazyVerticalGrid (mřížka).
Líné načítání funguje prostřednictvím mechanismu SubcomposeLayout: LazyColumn změří dostupný prostor, vyžádá viditelný rozsah z LayoutInfo a komponuje pouze prvky v tomto rozsahu. Prvky, které opustily obrazovku, opouštějí kompozici (s výjimkou vyrovnávací paměti prefetch). Podle údajů Google (Android Performance, 2026) poskytuje LazyColumn 60 FPS při posouvání seznamu 10000+ prvků na zařízeních střední třídy.
Column s vertikálním posouváním — je běžné uspořádání všech prvků pod sebou s aplikací modifikátoru verticalScroll. Column není líný: všechny podřízené prvky jsou komponovány okamžitě, i když nejsou viditelné. Pro seznam 200+ prvků způsobuje Column výrazné zpoždění při spuštění. LazyColumn komponuje pouze viditelné prvky + vyrovnávací paměť (výchozí 1 obrazovka dopředu/dozadu), což ho činí nepostradatelným pro dlouhé seznamy.
items API — sada rozšiřujících funkcí pro LazyColumn, které přijímají seznam dat a DSL popisovač pro každý prvek. Hlavní funkce: items(count, key, itemContent) pro pevný počet, items(list, key, itemContent) pro seznam, itemsIndexed(list, key, itemContent) s indexem. Parametr key — povinná podmínka pro výkon — dává Compose stabilní identifikátor prvku.
// Základní LazyColumn s itemContent
@Composable
fun ArticleList(articles: List<Article>) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// Header — není líný, vždy na začátku
item {
Text("Nejnovější články", style = MaterialTheme.typography.headlineMedium)
}
// Seznam článků s klíčem podle id
items(articles, key = { it.id }) { article ->
ArticleCard(
title = article.title,
summary = article.summary,
onClick = { onArticleClick(article.id) }
)
}
// Footer s indikátorem načítání
item {
CircularProgressIndicator(
modifier = Modifier.fillMaxWidth().padding(16.dp)
)
}
}
}
// Vlastní prvek seznamu
@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(): použijte item {} pro jednotlivé prvky, které jsou v seznamu vždy přítomny (header, footer, oddělovače). Použijte items() pro dynamický seznam dat. items() přijímá Iterable nebo Int a DSL blok, který je volán pro každý prvek. Kombinujte item a items v jednom LazyColumn: pořadí volání určuje pořadí zobrazení.
LazyListState — objekt ukládající stav seznamu: aktuální pozici posouvání (firstVisibleItemIndex, firstVisibleItemScrollOffset), viditelné prvky (layoutInfo) a metody programového posouvání (scrollToItem, animateScrollToItem). LazyListState se vytváří pomocí rememberLazyListState() a předává se LazyColumn prostřednictvím parametru state.
// LazyColumn s uchováním pozice posouvání
@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)
}
}
// Tlačítko "Nahoru" se objeví po 10. prvku
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, "Na začátek")
}
}
}
}
// Získání informací o viditelných prvcích
@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("Zobrazeno $firstItem–$lastItem z $totalItems")
}
Programové posouvání se provádí pomocí suspend funkcí listState: scrollToItem(index, scrollOffset) — okamžitý přesun (bez animace), animateScrollToItem(index) — s animací. Pro posouvání z korutiny použijte LaunchedEffect nebo coroutineScope.launch. Důležité: nevolejte suspend funkce z kompozice (uvnitř @Composable) — použijte lambdy (onClick, LaunchedEffect).
LazyColumn a RecyclerView řeší stejný úkol — efektivní zobrazení dlouhých seznamů. Rozdíl je v architektuře: RecyclerView používá ViewHolder a Adapter (klasické Android View), LazyColumn — deklarativní composable funkce bez XML a ViewHolder. Výběr závisí na technologickém stacku projektu: Compose vs View-based UI.
| Parametr | LazyColumn (Compose) | RecyclerView (View) |
|---|---|---|
| Rozvržení | Composable funkce (Kotlin DSL) | XML soubory + ViewBinding |
| Adaptér | items() / itemsIndexed() DSL | RecyclerView.Adapter + ViewHolder |
| Řazení/filtrování | Snapshot state + derivedStateOf | DiffUtil + AsyncListDiffer |
| Animace | AnimatedVisibility + Modifier.animateItem | DefaultItemAnimator |
| Různé typy prvků | item {}/items() + when podle typu | getItemViewType + více ViewHolderů |
| Posouvání na pozici | listState.animateScrollToItem() | layoutManager.scrollToPosition() |
| Prefetch | In-process (SubcomposeLayout buffer) | RecycledViewPool + GapWorker |
| Síť/stránkování | collectAsLazyPagingItems() + Paging 3 | PagingDataAdapter + Paging 3 |
Doporučení Google (Android Developers, 2026): pro nové projekty na Jetpack Compose používejte LazyColumn. Pro View-based projekty používejte RecyclerView. LazyColumn vyžaduje Compose, který přidává do APK přibližně 3–5 MB — zohledněte to při podpoře starých zařízení. Pokud projekt již používá systém View, LazyColumn nemůže koexistovat na stejné obrazovce s XML rozvržením bez AndroidView — v takových případech je jednodušší zůstat u RecyclerView.
LazyVerticalGrid — verze LazyColumn pro mřížky s pevným počtem sloupců (columns). LazyHorizontalGrid — horizontální mřížka s pevným počtem řádků (rows). Obě komponenty používají stejné principy líného načítání a stejné items API jako LazyColumn. Parametr columns určuje počet sloupců pomocí GridCells.Fixed(N) nebo GridCells.Adaptive(minSize).
// Mřížka s adaptivními sloupci
@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)
)
}
}
}
// Horizontální seznam (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 automaticky vypočítá počet sloupců na základě minimální velikosti buňky. Například GridCells.Adaptive(128.dp) na obrazovce šířky 360 dp umístí 2 sloupce, na tabletu 600 dp — 4 sloupce. GridCells.Fixed(2) vždy zobrazí přesně 2 sloupce. Adaptive je preferován pro multi-obrazovkovou adaptaci — automaticky se přizpůsobuje šířce obrazovky bez media dotazů.
Výkon LazyColumn závisí na třech faktorech: stabilitě klíčů (key), typu obsahu (contentType) a vyrovnávací paměti prefetch (beyondBounds). Ve výchozím nastavení LazyColumn bufferuje 1 obrazovku dopředu a 1 dozadu. Google (Android Developers, 2026) doporučuje konfigurovat tyto parametry pro seznamy s různými typy prvků nebo složitou kompozicí.
| Parametr | Popis | Doporučení |
|---|---|---|
| key | Stabilní identifikátor prvku | Vždy uveďte key = { it.id }. Bez key Compose používá index a změna pořadí prvků kazí animaci. |
| contentType | Typ obsahu pro oddělení poolů | Uveďte pro seznamy s různými typy prvků (text + obrázek + reklama). Compose bude znovu používat kompozici pouze v rámci stejného contentType. |
| beyondBounds | Počet obrazovek pro prefetch | Zvyšte na 2–3 pro seznamy s těžkými obrázky. Výchozí 1 obrazovka. |
| Modifier.animateItem | Animace změny pozice | Použijte pro animované řazení a filtrování (Compose 1.7+). |
// Optimalizovaný LazyColumn s contentType a prefetch
@Composable
fun OptimizedFeed(items: List<FeedItem>) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
beyondBoundsPageCount = 2 // prefetch 2 obrazovky
) {
items(
items = items,
key = { it.id },
contentType = { it.type } // rozdělení podle typů
) { item ->
when (item) {
is FeedItem.Post -> PostView(item)
is FeedItem.Ad -> AdView(item)
is FeedItem.Suggested -> SuggestedView(item)
}
}
}
}
// Měření výkonu pomocí listState
@Composable
fun ScrollPerformance(listState: LazyListState) {
val scrollInfo = listState.layoutInfo
val visibleCount = scrollInfo.visibleItemsInfo.size
val total = scrollInfo.totalItemsCount
// derivedStateOf — nepřekompovává se, pokud se hodnota nezměnila
val scrollProgress by remember {
derivedStateOf {
if (total > 0) visibleCount.toFloat() / total else 0f
}
}
LinearProgressIndicator(
progress = scrollProgress,
modifier = Modifier.fillMaxWidth().height(2.dp)
)
}
Běžné chyby: (1) chybějící key — prvky se překompovávají při každé změně seznamu; (2) použití mutableStateListOf bez snapshot toků — změny nemusí být sledovány; (3) volání suspend funkcí uvnitř itemContent — přerušuje kompozici; (4) bez contentType pro smíšené typy — Compose znovu používá kompozici reklamy pro textový příspěvek, což způsobuje vizuální artefakty. V IT Sectr přidáváme contentType všem seznamům se třemi a více typy obsahu.
Často kladené otázky
Zkontrolujte tři parametry: (1) key — bez stabilního klíče Compose nemůže znovu použít kompozici, (2) contentType — pro seznamy s různými typy prvků, (3) beyondBoundsPageCount — zvyšte na 2 pro obrázky. Těžké výpočty uvnitř itemContent přesuňte do remember s klíčem podle dat. Použijte Modifier.drawWithContent místo Image pro jednoduchou grafiku.
Použijte LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) pro mezery mezi prvky. Pro vizuální oddělovač s čárou přidejte item { Divider() } mezi items. Automatický oddělovač: items(items, key = { it.id }) { item -> ... }, a mezi každý prvek vložte item { HorizontalDivider() }. K tomu použijte LazyListScope.items(list, key) { /* prvek */ } a samostatně LazyListScope.item { Divider() }.
LazyColumn má ve výchozím nastavení nekonečnou výšku (fillMaxSize). Uvnitř Column bez pevné výšky nemůže LazyColumn určit velikost. Řešení: (1) nastavte LazyColumn pevnou výšku: Modifier.height(400.dp), (2) použijte Modifier.weight(1f) uvnitř Column, (3) nevkládejte LazyColumn do vertikálně posouvatelného kontejneru — použijte LazyColumn jako kořenový prvek s item {} pro header a footer.
LazyColumn automaticky reaguje na změny State. Pokud jsou data uložena v mutableStateListOf nebo mutableStateOf, Compose překompovává pouze změněné prvky. Pro seznamy z ViewModel použijte collectAsState() s Flow. Při aktualizaci seznamu s key Compose automaticky animuje změny (přidání, odstranění, přesun) prostřednictvím Modifier.animateItemPlacement().
LazyColumn — pro seznamy s jedním sloupcem (feed, chat, komentáře). LazyVerticalGrid — pro mřížky (galerie, katalog, ikony). Pokud mají být prvky v telefonu v jednom sloupci a na tabletu ve dvou — použijte LazyVerticalGrid s GridCells.Adaptive. Pokud potřebujete smíšenou mřížku (různý počet sloupců v jedné sekci) — použijte LazyColumn a uvnitř items vložte horizontální LazyRow nebo vnořené LazyVerticalGrid.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také