LazyColumn — ce este, liste eficiente în Compose

Autor: IT Sectr Publicat: 2026-02-24 Timp de citire: 10 min

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

  • LazyColumn — componenta Jetpack Compose pentru liste verticale cu încărcare lazy, parte din Material Design 3.
  • LazyListState — starea listei: poziția de derulare, elementele vizibile, derularea programatică.
  • items API — funcțiile items(), itemsIndexed(), items() cu chei pentru legarea datelor.
  • key — parametrul pentru identificarea stabilă a elementelor, permițând Compose să reutilizeze compoziția.
  • LazyVerticalGrid — versiunea LazyColumn pentru grile cu număr fix de coloane.

Ce este LazyColumn în Jetpack Compose?

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.

Diferența cu Column + ScrollState

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: legarea datelor la listă

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.

kotlin
// 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: gestionarea poziției de derulare

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.

kotlin
// 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 vs RecyclerView: comparația abordărilor

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.

ParametruLazyColumn (Compose)RecyclerView (View)
LayoutFuncții Composable (Kotlin DSL)Fișiere XML + ViewBinding
Adapteritems() / itemsIndexed() DSLRecyclerView.Adapter + ViewHolder
Sortare/filtrareSnapshot state + derivedStateOfDiffUtil + AsyncListDiffer
AnimațieAnimatedVisibility + Modifier.animateItemDefaultItemAnimator
Diferite tipuri de elementeitem {}/items() + when după tipgetItemViewType + multiple ViewHolder-uri
Derulare la pozițielistState.animateScrollToItem()layoutManager.scrollToPosition()
PrefetchIn-process (buffer SubcomposeLayout)RecycledViewPool + GapWorker
Rețea/paginarecollectAsLazyPagingItems() + Paging 3PagingDataAdapter + 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 și LazyHorizontalGrid: grile

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).

kotlin
// 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: key, contentType, prefetch

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ă.

ParametruDescriereRecomandare
keyIdentificator stabil al elementuluiIndicați întotdeauna key = { it.id }. Fără key, Compose folosește indexul, iar reordonarea elementelor strică animația.
contentTypeTipul conținutului pentru separarea pool-urilorSpecificați pentru liste cu diferite tipuri de elemente (text + imagine + reclamă). Compose va reutiliza compoziția doar în cadrul aceluiași contentType.
beyondBoundsNumărul de ecrane pentru prefetchMăriți la 2–3 pentru liste cu imagini grele. Implicit 1 ecran.
Modifier.animateItemAnimația schimbării pozițieiUtilizați pentru sortare și filtrare animată (Compose 1.7+).
kotlin
// 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

LazyColumn se derulează încet — ce să fac?

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ă.

Cum adaug un separator între elementele LazyColumn?

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 în interiorul Column nu funcționează — de ce?

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.

Cum actualizez LazyColumn la modificarea datelor?

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 vs LazyVerticalGrid — ce să aleg?

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

  • LazyColumn — componenta Jetpack Compose pentru liste verticale lazy cu API declarativ.
  • items() API acceptă o listă și un bloc DSL pentru fiecare element; key este obligatoriu pentru performanță.
  • LazyListState gestionează poziția de derulare, oferă funcții suspend pentru derulare programatică.
  • contentType separă pool-urile de compoziție pentru liste cu diferite tipuri de elemente.
  • LazyVerticalGrid / LazyRow — variante pentru grile și liste orizontale cu același items API.
  • LazyColumn asigură 60 FPS pentru 10000+ elemente datorită SubcomposeLayout și bufferului prefetch.
  • Pentru proiecte noi Compose utilizați LazyColumn; pentru proiecte View-based — RecyclerView.

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.

Discutați proiectul

Citiți și