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
items(), itemsIndexed(), items() com chaves para vinculação de dados.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.
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 é 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.
// 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 é 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.
// 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 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âmetro | LazyColumn (Compose) | RecyclerView (View) |
|---|---|---|
| Layout | Funções Composable (Kotlin DSL) | Arquivos XML + ViewBinding |
| Adaptador | items() / itemsIndexed() DSL | RecyclerView.Adapter + ViewHolder |
| Classificação/Filtragem | Snapshot state + derivedStateOf | DiffUtil + AsyncListDiffer |
| Animação | AnimatedVisibility + Modifier.animateItem | DefaultItemAnimator |
| Tipos diferentes de itens | item {}/items() + when por tipo | getItemViewType + vários ViewHolders |
| Rolar para posição | listState.animateScrollToItem() | layoutManager.scrollToPosition() |
| Prefetch | Em processo (buffer SubcomposeLayout) | RecycledViewPool + GapWorker |
| Rede/Paginação | collectAsLazyPagingItems() + Paging 3 | PagingDataAdapter + 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 é 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).
// 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.
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âmetro | Descrição | Recomendação |
|---|---|---|
| key | Identificador estável de elemento | Sempre especifique key = { it.id }. Sem key, o Compose usa o índice, e a reordenação de itens quebra a animação. |
| contentType | Tipo de conteúdo para separação de pools | Especifique para listas com diferentes tipos de itens (texto + imagem + anúncio). O Compose reutilizará a composição apenas dentro do mesmo contentType. |
| beyondBounds | Número de telas de prefetch | Aumente para 2–3 para listas com imagens pesadas. O padrão é 1 tela. |
| Modifier.animateItem | Animação de mudança de posição | Use para classificação e filtragem animadas (Compose 1.7+). |
// 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
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.
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 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é.
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 — 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
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.
Leia também