LazyColumn — componenta Jetpack Compose pentru afișarea listelor derulabile cu încărcare lentă (lazy). Spre deosebire de RecyclerView, LazyColumn nu creează ViewHolder-uri și nu folosește XML — toate elementele sunt descrise declarativ prin funcții composable. Arătăm ce este LazyColumn, cum funcționează LazyListState și items API și cum să obții performanță la nivel de 60 FPS. La IT Sectr folosim LazyColumn în toate proiectele noi Compose. Pentru comparație cu abordarea clasică, citiți articolul despre RecyclerView.
Principalele
items(), itemsIndexed(), items() cu chei pentru legarea datelor.LazyColumn — este o funcție composable din biblioteca Foundation Compose care afișează o listă verticală de elemente cu încărcare lentă (lazy): sunt create și compuse doar elementele vizibile pe ecran sau aflate în apropiere (zona de prefetch). LazyColumn a apărut în Compose 1.0 (2021) împreună cu LazyRow (listă orizontală) și LazyVerticalGrid (grilă).
Încărcarea lentă funcționează prin mecanismul SubcomposeLayout: LazyColumn măsoară spațiul disponibil, solicită intervalul vizibil de la LayoutInfo și compune doar elementele din acest interval. Elementele care au ieșit de pe ecran părăsesc compoziția (cu excepția bufferului prefetch). Conform datelor Google (Android Performance, 2026), LazyColumn asigură 60 FPS la derularea unei liste de 10000+ elemente pe dispozitive de gamă medie.
Column cu derulare verticală — este aranjarea obișnuită a tuturor elementelor unul sub altul cu aplicarea modificatorului verticalScroll. Column nu este lazy: toate elementele copil sunt compuse imediat, chiar dacă nu sunt vizibile. Pentru o listă de 200+ elemente, Column provoacă o întârziere semnificativă la pornire. LazyColumn compune doar elementele vizibile + bufferul (implicit 1 ecran înainte/înapoi), ceea ce îl face indispensabil pentru liste lungi.
items API — set de funcții de extensie pentru LazyColumn care primesc o listă de date și un descriptor DSL pentru fiecare element. Funcțiile principale: items(count, key, itemContent) pentru număr fix, items(list, key, itemContent) pentru listă, itemsIndexed(list, key, itemContent) cu index. Parametrul key — condiție obligatorie pentru performanță — oferă lui Compose un identificator stabil al elementului.
// LazyColumn de bază cu itemContent
@Composable
fun ArticleList(articles: List<Article>) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// Header — nu este lazy, întotdeauna la început
item {
Text("Ultimele articole", style = MaterialTheme.typography.headlineMedium)
}
// Lista articolelor cu cheie după id
items(articles, key = { it.id }) { article ->
ArticleCard(
title = article.title,
summary = article.summary,
onClick = { onArticleClick(article.id) }
)
}
// Footer cu indicator de încărcare
item {
CircularProgressIndicator(
modifier = Modifier.fillMaxWidth().padding(16.dp)
)
}
}
}
// Element de listă personalizat
@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(): utilizați item {} pentru elemente individuale care sunt întotdeauna prezente în listă (header, footer, separatoare). Utilizați items() pentru lista dinamică de date. items() acceptă Iterable sau Int și un bloc DSL care este apelat pentru fiecare element. Combinați item și items în același LazyColumn: ordinea apelurilor determină ordinea de afișare.
LazyListState — obiect care stochează starea listei: poziția curentă de derulare (firstVisibleItemIndex, firstVisibleItemScrollOffset), elementele vizibile (layoutInfo) și metodele de derulare programatică (scrollToItem, animateScrollToItem). LazyListState se creează prin rememberLazyListState() și se transmite către LazyColumn prin parametrul state.
// LazyColumn cu păstrarea poziției de derulare
@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)
}
}
// Butonul "Sus" apare după al 10-lea 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, "La început")
}
}
}
}
// Obținerea informațiilor despre elementele vizibile
@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("Se afișează $firstItem–$lastItem din $totalItems")
}
Derularea programatică se realizează prin funcțiile suspend ale listState: scrollToItem(index, scrollOffset) — deplasare instantanee (fără animație), animateScrollToItem(index) — cu animație. Pentru derulare din corutină utilizați LaunchedEffect sau coroutineScope.launch. Important: nu apelați funcții suspend din compoziție (în interiorul @Composable) — utilizați lambda (onClick, LaunchedEffect).
LazyColumn și RecyclerView rezolvă aceeași sarcină — afișarea eficientă a listelor lungi. Diferența constă în arhitectură: RecyclerView folosește ViewHolder și Adapter (Android View clasic), LazyColumn — funcții composable declarative fără XML și ViewHolder. Alegerea depinde de stiva tehnologică a proiectului: Compose vs View-based UI.
| Parametru | LazyColumn (Compose) | RecyclerView (View) |
|---|---|---|
| Layout | Funcții Composable (Kotlin DSL) | Fișiere XML + ViewBinding |
| Adapter | items() / itemsIndexed() DSL | RecyclerView.Adapter + ViewHolder |
| Sortare/filtrare | Snapshot state + derivedStateOf | DiffUtil + AsyncListDiffer |
| Animație | AnimatedVisibility + Modifier.animateItem | DefaultItemAnimator |
| Diferite tipuri de elemente | item {}/items() + when după tip | getItemViewType + multiple ViewHolder-uri |
| Derulare la poziție | listState.animateScrollToItem() | layoutManager.scrollToPosition() |
| Prefetch | In-process (buffer SubcomposeLayout) | RecycledViewPool + GapWorker |
| Rețea/paginare | collectAsLazyPagingItems() + Paging 3 | PagingDataAdapter + Paging 3 |
Recomandarea Google (Android Developers, 2026): pentru proiecte noi pe Jetpack Compose utilizați LazyColumn. Pentru proiecte View-based utilizați RecyclerView. LazyColumn necesită Compose, care adaugă în APK aproximativ 3–5 MB — luați în calcul acest lucru la suportul dispozitivelor vechi. Dacă proiectul folosește deja sistemul View, LazyColumn nu poate coexista pe același ecran cu layout XML fără AndroidView — în astfel de cazuri e mai simplu să rămâneți pe RecyclerView.
LazyVerticalGrid — versiunea LazyColumn pentru grile cu număr fix de coloane (columns). LazyHorizontalGrid — grilă orizontală cu număr fix de rânduri (rows). Ambele componente folosesc aceleași principii de încărcare lazy și același items API ca LazyColumn. Parametrul columns determină numărul de coloane prin GridCells.Fixed(N) sau GridCells.Adaptive(minSize).
// Grillă cu coloane adaptive
@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)
)
}
}
}
// Listă orizontală (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 calculează automat numărul de coloane pe baza dimensiunii minime a celulei. De exemplu, GridCells.Adaptive(128.dp) pe un ecran de 360 dp lățime plasează 2 coloane, pe o tabletă de 600 dp — 4 coloane. GridCells.Fixed(2) arată întotdeauna exact 2 coloane. Adaptive este preferat pentru adaptarea multi-ecran — se ajustează automat la lățimea ecranului fără interogări media.
Performanța LazyColumn depinde de trei factori: stabilitatea cheilor (key), tipul conținutului (contentType) și bufferul prefetch (beyondBounds). Implicit, LazyColumn face buffer 1 ecran înainte și 1 înapoi. Google (Android Developers, 2026) recomandă configurarea acestor parametri pentru liste cu diferite tipuri de elemente sau compoziție complexă.
| Parametru | Descriere | Recomandare |
|---|---|---|
| key | Identificator stabil al elementului | Indicați întotdeauna key = { it.id }. Fără key, Compose folosește indexul, iar reordonarea elementelor strică animația. |
| contentType | Tipul conținutului pentru separarea pool-urilor | Specificați pentru liste cu diferite tipuri de elemente (text + imagine + reclamă). Compose va reutiliza compoziția doar în cadrul aceluiași contentType. |
| beyondBounds | Numărul de ecrane pentru prefetch | Măriți la 2–3 pentru liste cu imagini grele. Implicit 1 ecran. |
| Modifier.animateItem | Animația schimbării poziției | Utilizați pentru sortare și filtrare animată (Compose 1.7+). |
// LazyColumn optimizată cu contentType și prefetch
@Composable
fun OptimizedFeed(items: List<FeedItem>) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
beyondBoundsPageCount = 2 // prefetch 2 ecrane
) {
items(
items = items,
key = { it.id },
contentType = { it.type } // separare pe tipuri
) { item ->
when (item) {
is FeedItem.Post -> PostView(item)
is FeedItem.Ad -> AdView(item)
is FeedItem.Suggested -> SuggestedView(item)
}
}
}
}
// Măsurarea performanței prin listState
@Composable
fun ScrollPerformance(listState: LazyListState) {
val scrollInfo = listState.layoutInfo
val visibleCount = scrollInfo.visibleItemsInfo.size
val total = scrollInfo.totalItemsCount
// derivedStateOf — nu se recompune dacă valoarea nu s-a schimbat
val scrollProgress by remember {
derivedStateOf {
if (total > 0) visibleCount.toFloat() / total else 0f
}
}
LinearProgressIndicator(
progress = scrollProgress,
modifier = Modifier.fillMaxWidth().height(2.dp)
)
}
Greșeli frecvente: (1) lipsa key — elementele se recompun la orice modificare a listei; (2) utilizarea mutableStateListOf fără fluxuri snapshot — modificările pot să nu fie urmărite; (3) apelarea funcțiilor suspend în interiorul itemContent — întrerupe compoziția; (4) fără contentType pentru tipuri mixte — Compose reutilizează compoziția reclamei pentru un post text, cauzând artefacte vizuale. La IT Sectr adăugăm contentType la toate listele cu trei sau mai multe tipuri de conținut.
Întrebări frecvente
Verificați trei parametri: (1) key — fără o cheie stabilă, Compose nu poate reutiliza compoziția, (2) contentType — pentru liste cu diferite tipuri de elemente, (3) beyondBoundsPageCount — măriți la 2 pentru imagini. Calculele grele din interiorul itemContent mutați-le în remember cu cheie după date. Utilizați Modifier.drawWithContent în loc de Image pentru grafică simplă.
Utilizați LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) pentru spații între elemente. Pentru un separator vizual cu linie, adăugați item { Divider() } între items. Separator automat: items(items, key = { it.id }) { item -> ... }, iar între fiecare element inserați item { HorizontalDivider() }. Pentru aceasta utilizați LazyListScope.items(list, key) { /* element */ } și separat LazyListScope.item { Divider() }.
LazyColumn are înălțime infinită (fillMaxSize) implicit. În interiorul Column fără înălțime fixă, LazyColumn nu poate determina dimensiunea. Soluție: (1) setați LazyColumn o înălțime fixă: Modifier.height(400.dp), (2) utilizați Modifier.weight(1f) în interiorul Column, (3) nu plasați LazyColumn într-un container derulabil vertical — utilizați LazyColumn ca element rădăcină cu item {} pentru header și footer.
LazyColumn reacționează automat la modificările State. Dacă datele sunt stocate în mutableStateListOf sau mutableStateOf, Compose recombină doar elementele modificate. Pentru liste din ViewModel utilizați collectAsState() cu Flow. La actualizarea listei cu key, Compose animează automat modificările (adăugare, ștergere, mutare) prin Modifier.animateItemPlacement().
LazyColumn — pentru liste cu o coloană (feed, chat, comentarii). LazyVerticalGrid — pentru grile (galerie, catalog, icone). Dacă elementele trebuie să fie pe o coloană pe telefon și pe două pe tabletă — utilizați LazyVerticalGrid cu GridCells.Adaptive. Dacă aveți nevoie de o grilă mixtă (număr diferit de coloane într-o secțiune) — utilizați LazyColumn și în interiorul items inserați LazyRow orizontale sau LazyVerticalGrid imbricate.
Concluzii
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și