LazyColumn — cos'è, elenchi efficienti in Compose

Autore: IT Sectr Pubblicato: 2026-02-24 Tempo di lettura: 10 min

LazyColumn — un componente Jetpack Compose per visualizzare elenchi scorrevoli con caricamento pigro. A differenza di RecyclerView, LazyColumn non crea ViewHolder e non usa XML — tutti gli elementi sono descritti in modo dichiarativo tramite funzioni composable. Mostriamo cos'è LazyColumn, come funzionano LazyListState e l'API items, e come ottenere prestazioni a 60 FPS. In IT Sectr usiamo LazyColumn in tutti i nuovi progetti Compose. Per un confronto con l'approccio classico, leggi l'articolo su RecyclerView.

Punti chiave

  • LazyColumn — un componente Jetpack Compose per elenchi verticali con caricamento pigro, parte di Material Design 3.
  • LazyListState — stato dell'elenco: posizione di scorrimento, elementi visibili, scorrimento programmatico.
  • items API — funzioni items(), itemsIndexed(), items() con chiavi per il binding dei dati.
  • key — parametro per l'identificazione stabile degli elementi, che consente a Compose di riutilizzare la composizione.
  • LazyVerticalGrid — versione di LazyColumn per griglie con numero fisso di colonne.

Cos'è LazyColumn in Jetpack Compose?

LazyColumn è una funzione composable della libreria Foundation Compose che visualizza un elenco verticale di elementi con caricamento pigro: vengono creati e composti solo gli elementi visibili sullo schermo o nelle vicinanze (zona di prefetch). LazyColumn è apparso in Compose 1.0 (2021) insieme a LazyRow (elenco orizzontale) e LazyVerticalGrid (griglia).

Il caricamento pigro funziona attraverso il meccanismo SubcomposeLayout: LazyColumn misura lo spazio disponibile, richiede l'intervallo visibile a LayoutInfo e compone solo gli elementi in quell'intervallo. Gli elementi che escono dallo schermo abbandonano la composizione (eccetto il buffer di prefetch). Secondo Google (Android Performance, 2026), LazyColumn mantiene 60 FPS durante lo scorrimento di un elenco di 10000+ elementi su dispositivi di fascia media.

Differenza con Column + ScrollState

Column con scorrimento verticale è una disposizione normale di tutti gli elementi uno sotto l'altro con il modificatore verticalScroll applicato. Column non è pigra: tutti gli elementi figlio vengono composti immediatamente, anche se non sono visibili. Per un elenco di 200+ elementi, Column causa un notevole ritardo all'avvio. LazyColumn compone solo gli elementi visibili + buffer (1 schermo avanti/indietro per impostazione predefinita), rendendolo indispensabile per elenchi lunghi.

items API: binding dei dati all'elenco

items API è un insieme di funzioni di estensione per LazyColumn che accettano un elenco di dati e un descrittore DSL per ogni elemento. Le funzioni principali sono: items(count, key, itemContent) per un numero fisso, items(list, key, itemContent) per un elenco, itemsIndexed(list, key, itemContent) con indice. Il parametro key è obbligatorio per le prestazioni — fornisce a Compose un identificatore stabile dell'elemento.

kotlin
// LazyColumn di base con itemContent
@Composable
fun ArticleList(articles: List<Article>) {
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        // Header — non pigro, sempre all'inizio
        item {
            Text("Ultimi articoli", style = MaterialTheme.typography.headlineMedium)
        }

        // Elenco articoli con chiave per id
        items(articles, key = { it.id }) { article ->
            ArticleCard(
                title = article.title,
                summary = article.summary,
                onClick = { onArticleClick(article.id) }
            )
        }

        // Footer con indicatore di caricamento
        item {
            CircularProgressIndicator(
                modifier = Modifier.fillMaxWidth().padding(16.dp)
            )
        }
    }
}

// Elemento elenco personalizzato
@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(): usa item {} per elementi singoli sempre presenti nell'elenco (intestazione, piè di pagina, separatori). Usa items() per un elenco di dati dinamico. items() accetta un Iterable o Int e un blocco DSL che viene chiamato per ogni elemento. Combina item e items in un unico LazyColumn: l'ordine delle chiamate determina l'ordine di visualizzazione.

LazyListState: gestione della posizione di scorrimento

LazyListState è un oggetto che memorizza lo stato dell'elenco: posizione corrente di scorrimento (firstVisibleItemIndex, firstVisibleItemScrollOffset), elementi visibili (layoutInfo) e metodi di scorrimento programmatico (scrollToItem, animateScrollToItem). LazyListState viene creato tramite rememberLazyListState() e passato a LazyColumn tramite il parametro state.

kotlin
// LazyColumn con preservazione della posizione di scorrimento
@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)
            }
        }

        // Pulsante "Su" appare dopo il 10° elemento
        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, "In alto")
            }
        }
    }
}

// Ottenere informazioni sugli elementi visibili
@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("Mostra $firstItem–$lastItem di $totalItems")
}

Lo scorrimento programmatico viene eseguito tramite funzioni suspend di listState: scrollToItem(index, scrollOffset) — movimento istantaneo (senza animazione), animateScrollToItem(index) — con animazione. Per lo scorrimento da una coroutine, usa LaunchedEffect o coroutineScope.launch. Importante: non chiamare funzioni suspend dalla composizione (dentro @Composable) — usa lambda (onClick, LaunchedEffect).

LazyColumn vs RecyclerView: confronto degli approcci

LazyColumn e RecyclerView risolvono lo stesso problema — visualizzazione efficiente di elenchi lunghi. La differenza sta nell'architettura: RecyclerView usa ViewHolder e Adapter (vista Android classica), LazyColumn usa funzioni composable dichiarative senza XML o ViewHolder. La scelta dipende dallo stack tecnologico del progetto: Compose vs UI basata su View.

ParametroLazyColumn (Compose)RecyclerView (View)
LayoutFunzioni Composable (Kotlin DSL)File XML + ViewBinding
Adattatoreitems() / itemsIndexed() DSLRecyclerView.Adapter + ViewHolder
Ordinamento/FiltraggioSnapshot state + derivedStateOfDiffUtil + AsyncListDiffer
AnimazioneAnimatedVisibility + Modifier.animateItemDefaultItemAnimator
Tipi di elementi diversiitem {}/items() + when per tipogetItemViewType + più ViewHolder
Scorrere a posizionelistState.animateScrollToItem()layoutManager.scrollToPosition()
PrefetchIn-process (buffer SubcomposeLayout)RecycledViewPool + GapWorker
Rete/PaginazionecollectAsLazyPagingItems() + Paging 3PagingDataAdapter + Paging 3

Raccomandazione di Google (Android Developers, 2026): per i nuovi progetti su Jetpack Compose, usa LazyColumn. Per i progetti basati su View, usa RecyclerView. LazyColumn richiede Compose, che aggiunge circa 3–5 MB all'APK — tienilo in considerazione quando supporti dispositivi vecchi. Se un progetto utilizza già il sistema View, LazyColumn non può coesistere sullo stesso schermo con un layout XML senza AndroidView — in questi casi, è più semplice rimanere con RecyclerView.

LazyVerticalGrid e LazyHorizontalGrid: griglie

LazyVerticalGrid è una versione di LazyColumn per griglie con numero fisso di colonne. LazyHorizontalGrid è una griglia orizzontale con righe fisse. Entrambi i componenti utilizzano gli stessi principi di caricamento pigro e la stessa API items di LazyColumn. Il parametro columns determina il numero di colonne tramite GridCells.Fixed(N) o GridCells.Adaptive(minSize).

kotlin
// Griglia con colonne adattive
@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)
            )
        }
    }
}

// Elenco orizzontale (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 calcola automaticamente il numero di colonne in base alla dimensione minima della cella. Ad esempio, GridCells.Adaptive(128.dp) su uno schermo largo 360 dp posiziona 2 colonne, su un tablet da 600 dp — 4 colonne. GridCells.Fixed(2) mostra sempre esattamente 2 colonne. Adaptive è preferibile per l'adattamento multi-schermo — si adatta automaticamente alla larghezza dello schermo senza media query.

Prestazioni di LazyColumn: key, contentType, prefetch

Le prestazioni di LazyColumn dipendono da tre fattori: stabilità della chiave (key), tipo di contenuto (contentType) e buffer di prefetch (beyondBounds). Per impostazione predefinita, LazyColumn bufferizza 1 schermo avanti e 1 indietro. Google (Android Developers, 2026) raccomanda di configurare questi parametri per elenchi con diversi tipi di elementi o composizione complessa.

ParametroDescrizioneRaccomandazione
keyIdentificatore stabile dell'elementoSpecifica sempre key = { it.id }. Senza key, Compose usa l'indice e il riordinamento degli elementi rompe l'animazione.
contentTypeTipo di contenuto per la separazione dei poolSpecifica per elenchi con diversi tipi di elementi (testo + immagine + annuncio). Compose riutilizzerà la composizione solo all'interno dello stesso contentType.
beyondBoundsNumero di schermi di prefetchAumenta a 2–3 per elenchi con immagini pesanti. L'impostazione predefinita è 1 schermo.
Modifier.animateItemAnimazione del cambiamento di posizioneUsa per ordinamento e filtraggio animati (Compose 1.7+).
kotlin
// LazyColumn ottimizzata con contentType e prefetch
@Composable
fun OptimizedFeed(items: List<FeedItem>) {
    val listState = rememberLazyListState()

    LazyColumn(
        state = listState,
        modifier = Modifier.fillMaxSize(),
        beyondBoundsPageCount = 2  // prefetch 2 schermi
    ) {
        items(
            items = items,
            key = { it.id },
            contentType = { it.type }  // divisione per tipo
        ) { item ->
            when (item) {
                is FeedItem.Post -> PostView(item)
                is FeedItem.Ad -> AdView(item)
                is FeedItem.Suggested -> SuggestedView(item)
            }
        }
    }
}

// Misurazione delle prestazioni tramite listState
@Composable
fun ScrollPerformance(listState: LazyListState) {
    val scrollInfo = listState.layoutInfo
    val visibleCount = scrollInfo.visibleItemsInfo.size
    val total = scrollInfo.totalItemsCount

    // derivedStateOf — non ricompone se il valore non è cambiato
    val scrollProgress by remember {
        derivedStateOf {
            if (total > 0) visibleCount.toFloat() / total else 0f
        }
    }

    LinearProgressIndicator(
        progress = scrollProgress,
        modifier = Modifier.fillMaxWidth().height(2.dp)
    )
}

Errori comuni: (1) mancanza di key — gli elementi vengono ricomposti a ogni modifica dell'elenco; (2) uso di mutableStateListOf senza flussi snapshot — le modifiche potrebbero non essere tracciate; (3) chiamata di funzioni suspend all'interno di itemContent — interrompe la composizione; (4) senza contentType per tipi misti — Compose riutilizza la composizione degli annunci per i post di testo, causando artefatti visivi. In IT Sectr aggiungiamo contentType a tutti gli elenchi con tre o più tipi di contenuto.

Domande frequenti

LazyColumn scorre lentamente — cosa fare?

Controlla tre parametri: (1) key — senza una chiave stabile, Compose non può riutilizzare la composizione, (2) contentType — per elenchi con diversi tipi di elementi, (3) beyondBoundsPageCount — aumenta a 2 per le immagini. Sposta i calcoli pesanti all'interno di itemContent in remember con una chiave per dati. Usa Modifier.drawWithContent invece di Image per grafiche semplici.

Come aggiungere un separatore tra gli elementi di LazyColumn?

Usa LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) per la spaziatura tra gli elementi. Per un separatore visivo con linea, aggiungi item { Divider() } tra gli items. Separatore automatico: items(items, key = { it.id }) { item -> ... } e tra ogni elemento inserisci item { HorizontalDivider() }. Per questo, usa LazyListScope.items(list, key) { /* elemento */ } e separatamente LazyListScope.item { Divider() }.

LazyColumn dentro Column non funziona — perché?

LazyColumn ha un'altezza infinita (fillMaxSize) per impostazione predefinita. Dentro una Column senza altezza fissa, LazyColumn non può determinare la sua dimensione. Soluzione: (1) imposta un'altezza fissa su LazyColumn: Modifier.height(400.dp), (2) usa Modifier.weight(1f) dentro Column, (3) non annidare LazyColumn in un contenitore scorrevole verticalmente — usa LazyColumn come elemento radice con item {} per intestazione e piè di pagina.

Come aggiornare LazyColumn quando i dati cambiano?

LazyColumn reagisce automaticamente ai cambiamenti di State. Se i dati sono memorizzati in mutableStateListOf o mutableStateOf, Compose ricompone solo gli elementi modificati. Per elenchi da ViewModel, usa collectAsState() con Flow. Quando aggiorni l'elenco con key, Compose anima automaticamente le modifiche (aggiunta, rimozione, riordinamento) tramite Modifier.animateItemPlacement().

LazyColumn vs LazyVerticalGrid — quale scegliere?

LazyColumn — per elenchi a una colonna (feed, chat, commenti). LazyVerticalGrid — per griglie (galleria, catalogo, icone). Se gli elementi devono essere in una colonna sul telefono e in due sul tablet — usa LazyVerticalGrid con GridCells.Adaptive. Se è necessaria una griglia mista (diverso numero di colonne in una sezione) — usa LazyColumn e inserisci LazyRow orizzontale o LazyVerticalGrid annidato dentro items.

Riepilogo

  • LazyColumn — un componente Jetpack Compose per elenchi verticali pigri con API dichiarativa.
  • items() API accetta un elenco e un blocco DSL per ogni elemento; key è obbligatorio per le prestazioni.
  • LazyListState gestisce la posizione di scorrimento, fornisce funzioni suspend per lo scorrimento programmatico.
  • contentType separa i pool di composizione per elenchi con diversi tipi di elementi.
  • LazyVerticalGrid / LazyRow — varianti per griglie ed elenchi orizzontali con la stessa API items.
  • LazyColumn mantiene 60 FPS per 10000+ elementi grazie a SubcomposeLayout e al buffer di prefetch.
  • Per nuovi progetti Compose usa LazyColumn; per progetti basati su View — RecyclerView.

Svilupperemo un'applicazione mobile chiavi in mano

IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.

Discuti il progetto

Leggi anche