LazyColumn — o que é, listas eficientes no Compose

Autor: IT Sectr Publicado: 2026-02-24 Tempo de leitura: 10 min

LazyColumn — um componente do Jetpack Compose para exibir listas roláveis com carregamento preguiçoso. Ao contrário do RecyclerView, LazyColumn não cria ViewHolders nem usa XML — todos os elementos são descritos declarativamente por meio de funções composable. Mostramos o que é LazyColumn, como funcionam LazyListState e a API items, e como obter desempenho de 60 FPS. Na IT Sectr usamos LazyColumn em todos os novos projetos Compose. Para comparação com a abordagem clássica, leia o artigo sobre RecyclerView.

Principais pontos

  • LazyColumn — um componente do Jetpack Compose para listas verticais com carregamento preguiçoso, parte do Material Design 3.
  • LazyListState — estado da lista: posição de rolagem, itens visíveis, rolagem programática.
  • items API — funções items(), itemsIndexed(), items() com chaves para vinculação de dados.
  • key — parâmetro para identificação estável de elementos, permitindo ao Compose reutilizar a composição.
  • LazyVerticalGrid — versão do LazyColumn para grades com número fixo de colunas.

O que é LazyColumn no Jetpack Compose?

LazyColumn é uma função composable da biblioteca Foundation Compose que exibe uma lista vertical de itens com carregamento preguiçoso: apenas itens visíveis na tela ou próximos (zona de prefetch) são criados e compostos. LazyColumn apareceu no Compose 1.0 (2021) junto com LazyRow (lista horizontal) e LazyVerticalGrid (grade).

O carregamento preguiçoso funciona através do mecanismo SubcomposeLayout: LazyColumn mede o espaço disponível, solicita o intervalo visível ao LayoutInfo e compõe apenas os itens nesse intervalo. Itens que saem da tela deixam a composição (exceto o buffer de prefetch). De acordo com o Google (Android Performance, 2026), LazyColumn mantém 60 FPS ao rolar uma lista de 10000+ itens em dispositivos de gama média.

Diferença de Column + ScrollState

Column com rolagem vertical é uma disposição normal de todos os itens um abaixo do outro com o modificador verticalScroll aplicado. Column não é preguiçosa: todos os elementos filhos são compostos imediatamente, mesmo que não estejam visíveis. Para uma lista de 200+ itens, Column causa um lag significativo na inicialização. LazyColumn compõe apenas itens visíveis + buffer (1 tela para frente/trás por padrão), tornando-o indispensável para listas longas.

items API: vinculação de dados à lista

items API é um conjunto de funções de extensão para LazyColumn que aceitam uma lista de dados e um descritor DSL para cada item. As principais funções são: items(count, key, itemContent) para quantidade fixa, items(list, key, itemContent) para uma lista, itemsIndexed(list, key, itemContent) com índice. O parâmetro key é obrigatório para desempenho — dá ao Compose um identificador de elemento estável.

kotlin
// LazyColumn básica com itemContent
@Composable
fun ArticleList(articles: List<Article>) {
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        // Header — não preguiçoso, sempre no início
        item {
            Text("Últimos artigos", style = MaterialTheme.typography.headlineMedium)
        }

        // Lista de artigos com chave por id
        items(articles, key = { it.id }) { article ->
            ArticleCard(
                title = article.title,
                summary = article.summary,
                onClick = { onArticleClick(article.id) }
            )
        }

        // Footer com indicador de carregamento
        item {
            CircularProgressIndicator(
                modifier = Modifier.fillMaxWidth().padding(16.dp)
            )
        }
    }
}

// Item de lista personalizado
@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(): use item {} para itens individuais que estão sempre presentes na lista (cabeçalho, rodapé, divisores). Use items() para uma lista dinâmica de dados. items() aceita um Iterable ou Int e um bloco DSL que é chamado para cada elemento. Combine item e items em um mesmo LazyColumn: a ordem das chamadas determina a ordem de exibição.

LazyListState: gerenciamento da posição de rolagem

LazyListState é um objeto que armazena o estado da lista: posição atual de rolagem (firstVisibleItemIndex, firstVisibleItemScrollOffset), itens visíveis (layoutInfo) e métodos de rolagem programática (scrollToItem, animateScrollToItem). LazyListState é criado via rememberLazyListState() e passado para LazyColumn através do parâmetro state.

kotlin
// LazyColumn com preservação de posição de rolagem
@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)
            }
        }

        // Botão "Subir" aparece após o 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, "Subir")
            }
        }
    }
}

// Obtenção de informações de itens visíveis
@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("Mostrando $firstItem–$lastItem de $totalItems")
}

Rolagem programática é realizada através de funções suspend do listState: scrollToItem(index, scrollOffset) — movimento instantâneo (sem animação), animateScrollToItem(index) — com animação. Para rolagem a partir de uma corrotina, use LaunchedEffect ou coroutineScope.launch. Importante: não chame funções suspend da composição (dentro de @Composable) — use lambdas (onClick, LaunchedEffect).

LazyColumn vs RecyclerView: comparação de abordagens

LazyColumn e RecyclerView resolvem o mesmo problema — exibição eficiente de listas longas. A diferença está na arquitetura: RecyclerView usa ViewHolder e Adapter (visualização clássica do Android), LazyColumn usa funções composable declarativas sem XML ou ViewHolder. A escolha depende da pilha de tecnologia do projeto: Compose vs UI baseada em View.

ParâmetroLazyColumn (Compose)RecyclerView (View)
LayoutFunções Composable (Kotlin DSL)Arquivos XML + ViewBinding
Adaptadoritems() / itemsIndexed() DSLRecyclerView.Adapter + ViewHolder
Classificação/FiltragemSnapshot state + derivedStateOfDiffUtil + AsyncListDiffer
AnimaçãoAnimatedVisibility + Modifier.animateItemDefaultItemAnimator
Tipos diferentes de itensitem {}/items() + when por tipogetItemViewType + vários ViewHolders
Rolar para posiçãolistState.animateScrollToItem()layoutManager.scrollToPosition()
PrefetchEm processo (buffer SubcomposeLayout)RecycledViewPool + GapWorker
Rede/PaginaçãocollectAsLazyPagingItems() + Paging 3PagingDataAdapter + Paging 3

Recomendação do Google (Android Developers, 2026): para novos projetos no Jetpack Compose, use LazyColumn. Para projetos baseados em View, use RecyclerView. LazyColumn requer o Compose, que adiciona cerca de 3–5 MB ao APK — considere isso ao suportar dispositivos antigos. Se um projeto já usa o sistema View, LazyColumn não pode coexistir em uma mesma tela com layout XML sem AndroidView — nesses casos, é mais fácil ficar com RecyclerView.

LazyVerticalGrid e LazyHorizontalGrid: grades

LazyVerticalGrid é uma versão do LazyColumn para grades com número fixo de colunas. LazyHorizontalGrid é uma grade horizontal com linhas fixas. Ambos os componentes usam os mesmos princípios de carregamento preguiçoso e a mesma API items que o LazyColumn. O parâmetro columns determina o número de colunas via GridCells.Fixed(N) ou GridCells.Adaptive(minSize).

kotlin
// Grade com colunas adaptativas
@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)
            )
        }
    }
}

// Lista horizontal (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 calcula automaticamente o número de colunas com base no tamanho mínimo da célula. Por exemplo, GridCells.Adaptive(128.dp) em uma tela de 360 dp de largura coloca 2 colunas, em um tablet de 600 dp — 4 colunas. GridCells.Fixed(2) mostra sempre exatamente 2 colunas. Adaptive é preferível para adaptação a múltiplas telas — ajusta-se automaticamente à largura da tela sem consultas de mídia.

Desempenho do LazyColumn: key, contentType, prefetch

O desempenho do LazyColumn depende de três fatores: estabilidade da chave (key), tipo de conteúdo (contentType) e buffer de prefetch (beyondBounds). Por padrão, LazyColumn armazena em buffer 1 tela para frente e 1 para trás. O Google (Android Developers, 2026) recomenda configurar esses parâmetros para listas com diferentes tipos de itens ou composição complexa.

ParâmetroDescriçãoRecomendação
keyIdentificador estável de elementoSempre especifique key = { it.id }. Sem key, o Compose usa o índice, e a reordenação de itens quebra a animação.
contentTypeTipo de conteúdo para separação de poolsEspecifique para listas com diferentes tipos de itens (texto + imagem + anúncio). O Compose reutilizará a composição apenas dentro do mesmo contentType.
beyondBoundsNúmero de telas de prefetchAumente para 2–3 para listas com imagens pesadas. O padrão é 1 tela.
Modifier.animateItemAnimação de mudança de posiçãoUse para classificação e filtragem animadas (Compose 1.7+).
kotlin
// LazyColumn otimizada com contentType e prefetch
@Composable
fun OptimizedFeed(items: List<FeedItem>) {
    val listState = rememberLazyListState()

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

// Medição de desempenho via listState
@Composable
fun ScrollPerformance(listState: LazyListState) {
    val scrollInfo = listState.layoutInfo
    val visibleCount = scrollInfo.visibleItemsInfo.size
    val total = scrollInfo.totalItemsCount

    // derivedStateOf — não recompõe se o valor não mudou
    val scrollProgress by remember {
        derivedStateOf {
            if (total > 0) visibleCount.toFloat() / total else 0f
        }
    }

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

Erros comuns: (1) falta de key — os elementos são recompostos a qualquer alteração na lista; (2) usar mutableStateListOf sem fluxos snapshot — as alterações podem não ser rastreadas; (3) chamar funções suspend dentro de itemContent — interrompe a composição; (4) sem contentType para tipos mistos — o Compose reutiliza a composição de anúncios para postagens de texto, causando artefatos visuais. Na IT Sectr adicionamos contentType a todas as listas com três ou mais tipos de conteúdo.

Perguntas frequentes

LazyColumn rola lentamente — o que fazer?

Verifique três parâmetros: (1) key — sem uma chave estável, o Compose não pode reutilizar a composição, (2) contentType — para listas com diferentes tipos de itens, (3) beyondBoundsPageCount — aumente para 2 para imagens. Mova cálculos pesados dentro de itemContent para remember com uma chave por dados. Use Modifier.drawWithContent em vez de Image para gráficos simples.

Como adicionar um divisor entre itens do LazyColumn?

Use LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) para espaçamento entre itens. Para um divisor visual com linha, adicione item { Divider() } entre os items. Divisor automático: items(items, key = { it.id }) { item -> ... }, e entre cada elemento insira item { HorizontalDivider() }. Para isso, use LazyListScope.items(list, key) { /* elemento */ } e separadamente LazyListScope.item { Divider() }.

LazyColumn dentro de Column não funciona — por quê?

LazyColumn tem altura infinita (fillMaxSize) por padrão. Dentro de uma Column sem altura fixa, LazyColumn não consegue determinar seu tamanho. Solução: (1) defina uma altura fixa no LazyColumn: Modifier.height(400.dp), (2) use Modifier.weight(1f) dentro de Column, (3) não aninhe LazyColumn dentro de um contêiner com rolagem vertical — use LazyColumn como elemento raiz com item {} para cabeçalho e rodapé.

Como atualizar LazyColumn ao alterar os dados?

LazyColumn reage automaticamente a alterações de State. Se os dados forem armazenados em mutableStateListOf ou mutableStateOf, o Compose recompõe apenas os elementos alterados. Para listas do ViewModel, use collectAsState() com Flow. Ao atualizar a lista com key, o Compose anima automaticamente as alterações (adição, remoção, reordenação) através de Modifier.animateItemPlacement().

LazyColumn vs LazyVerticalGrid — qual escolher?

LazyColumn — para listas de uma coluna (feed, chat, comentários). LazyVerticalGrid — para grades (galeria, catálogo, ícones). Se os itens devem estar em uma coluna no telefone e em duas no tablet — use LazyVerticalGrid com GridCells.Adaptive. Se for necessária uma grade mista (diferente número de colunas em uma seção) — use LazyColumn e insira LazyRow horizontal ou LazyVerticalGrid aninhada dentro dos items.

Resumo

  • LazyColumn — um componente Jetpack Compose para listas verticais preguiçosas com API declarativa.
  • items() API aceita uma lista e um bloco DSL para cada item; key é obrigatório para desempenho.
  • LazyListState gerencia a posição de rolagem, fornece funções suspend para rolagem programática.
  • contentType separa os pools de composição para listas com diferentes tipos de itens.
  • LazyVerticalGrid / LazyRow — variantes para grades e listas horizontais com a mesma API items.
  • LazyColumn mantém 60 FPS para 10000+ itens graças ao SubcomposeLayout e ao buffer de prefetch.
  • Para novos projetos Compose use LazyColumn; para projetos baseados em View — RecyclerView.

Vamos desenvolver um aplicativo móvel chave na mão

A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.

Discutir o projeto

Leia também