Muitos apps precisam mostrar coleções de itens. Este documento explica como fazer isso de forma eficiente no Jetpack Compose.
Se você sabe que seu caso de uso não requer rolagem,
use uma Column
ou Row
simples (dependendo da direção) e emita o conteúdo de cada item
iterando em uma lista da seguinte maneira:
@Composable fun MessageList(messages: List<Message>) { Column { messages.forEach { message -> MessageRow(message) } } }
É possível permitir a rolagem de Column
usando o modificador verticalScroll()
.
Listas lentas
Se você precisar exibir muitos itens (ou uma lista de tamanho desconhecido),
usar um layout como uma Column
pode causar problemas de performance, já que todos os itens vão ser compostos e colocados no layout independente de estarem visíveis ou não.
O Compose fornece um conjunto de componentes para compor e posicionar apenas os itens
que estão visíveis na janela de visualização. Esses componentes incluem
LazyColumn
e
LazyRow
.
Como o nome sugere, a diferença entre
LazyColumn
e
LazyRow
é a orientação da disposição e rolagem dos itens. LazyColumn
gera uma lista de rolagem vertical e LazyRow
gera uma lista de
rolagem horizontal.
Os componentes lentos são diferentes da maioria dos layouts no Compose. Em vez de
aceitar um parâmetro de bloco de conteúdo @Composable
, permitindo que os apps emitam elementos combináveis diretamente,
os componentes lentos fornecem um bloco LazyListScope.()
. Esse
bloco LazyListScope
fornece uma DSL que permite que os apps descrevam o conteúdo do item. O
componente lento é responsável por adicionar o conteúdo de cada item conforme
exigido pelo layout e pela posição de rolagem.
DSL LazyListScope
A DSL de LazyListScope
fornece uma série de funções para descrever itens
no layout. No nível mais básico,
item()
adiciona um único item e
items(Int)
adiciona vários itens:
LazyColumn { // Add a single item item { Text(text = "First item") } // Add 5 items items(5) { index -> Text(text = "Item: $index") } // Add another single item item { Text(text = "Last item") } }
Há também várias funções de extensão que permitem adicionar
conjuntos de itens, como uma List
. Essas extensões permitem migrar facilmente
o exemplo de Column
acima:
/** * import androidx.compose.foundation.lazy.items */ LazyColumn { items(messages) { message -> MessageRow(message) } }
Há também uma variante da função de extensão
items()
com o nome
itemsIndexed()
,
que fornece o índice. Consulte
a referência LazyListScope
para mais detalhes.
Grades lentas
Os elementos combináveis
LazyVerticalGrid
e
LazyHorizontalGrid
oferecem suporte à exibição de itens em uma grade. Uma grade vertical lenta
mostra os itens em um contêiner de rolagem vertical, dividido em
várias colunas, enquanto grades horizontais lentas fazem o mesmo
no eixo horizontal.
As grades têm os mesmos recursos avançados da API que as listas e usam uma
DSL muito semelhante
(LazyGridScope.()
)
para descrever o conteúdo.
O parâmetro columns
em
LazyVerticalGrid
e o parâmetro rows
em
LazyHorizontalGrid
controlam como as células são formadas em colunas ou linhas. O exemplo
abaixo mostra itens em uma grade, usando
GridCells.Adaptive
para definir cada coluna com pelo menos 128.dp
de largura:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 128.dp) ) { items(photos) { photo -> PhotoItem(photo) } }
LazyVerticalGrid
permite especificar uma largura para os itens. A grade vai incluir
o máximo de colunas possível. Qualquer largura restante é distribuída igualmente
entre as colunas depois que a quantidade é calculada.
Essa maneira adaptável de dimensionamento é útil principalmente para exibir conjuntos de itens
em diferentes tamanhos de tela.
Se você souber o número exato de colunas que serão usadas, forneça uma
instância de
GridCells.Fixed
com o número de colunas necessárias.
Se o design exigir que apenas alguns itens tenham dimensões não padrão,
use o suporte de grade para fornecer períodos de colunas personalizados para os itens.
Especifique o período da coluna com o parâmetro span
dos métodos
LazyGridScope DSL
item
e items
.
O maxLineSpan
,
um dos valores do escopo do período, é útil principalmente quando você está usando
o dimensionamento adaptável, já que o número de colunas não é fixo.
Este exemplo mostra como mostrar um período completo de linha:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 30.dp) ) { item(span = { // LazyGridItemSpanScope: // maxLineSpan GridItemSpan(maxLineSpan) }) { CategoryCard("Fruits") } // ... }
Grade escalonada lenta
LazyVerticalStaggeredGrid
e
LazyHorizontalStaggeredGrid
são elementos combináveis que permitem criar uma grade de itens com carregamento lento e escalonado.
Uma grade escalonada vertical lenta mostra os itens em um contêiner de rolagem vertical
que abrange várias colunas e permite que itens individuais tenham
alturas diferentes. As grades horizontais lentas têm o mesmo comportamento no
eixo horizontal com itens de larguras diferentes.
O snippet a seguir é um exemplo básico de como usar LazyVerticalStaggeredGrid
com uma largura de 200.dp
por item:
LazyVerticalStaggeredGrid( columns = StaggeredGridCells.Adaptive(200.dp), verticalItemSpacing = 4.dp, horizontalArrangement = Arrangement.spacedBy(4.dp), content = { items(randomSizedPhotos) { photo -> AsyncImage( model = photo, contentScale = ContentScale.Crop, contentDescription = null, modifier = Modifier .fillMaxWidth() .wrapContentHeight() ) } }, modifier = Modifier.fillMaxSize() )
Para definir um número fixo de colunas, use
StaggeredGridCells.Fixed(columns)
em vez de StaggeredGridCells.Adaptive
.
Isso divide a largura disponível pelo número de colunas (ou linhas para uma
grade horizontal) e faz com que cada item ocupe essa largura (ou altura para uma
grade horizontal):
LazyVerticalStaggeredGrid( columns = StaggeredGridCells.Fixed(3), verticalItemSpacing = 4.dp, horizontalArrangement = Arrangement.spacedBy(4.dp), content = { items(randomSizedPhotos) { photo -> AsyncImage( model = photo, contentScale = ContentScale.Crop, contentDescription = null, modifier = Modifier .fillMaxWidth() .wrapContentHeight() ) } }, modifier = Modifier.fillMaxSize() )
Padding de conteúdo
Às vezes, você precisará adicionar padding ao redor das bordas do conteúdo. Os componentes
lentos permitem transmitir alguns
PaddingValues
ao parâmetro contentPadding
para que ofereçam suporte para o código a seguir:
LazyColumn( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), ) { // ... }
Nesse exemplo, adicionamos 16.dp
de padding às bordas horizontais (esquerda e
direita) e 8.dp
à parte de cima e de baixo do conteúdo.
O padding é aplicado ao conteúdo, não à
LazyColumn
em si. No exemplo acima, o primeiro item vai adicionar 8.dp
de padding à parte de cima, o último item vai adicionar 8.dp
à parte de baixo e todos os itens
terão 16.dp
de padding à esquerda e à direita.
Espaçamento de conteúdo
Para adicionar espaçamento entre os itens, use
Arrangement.spacedBy()
.
O exemplo abaixo adiciona 4.dp
de espaço entre cada item:
LazyColumn( verticalArrangement = Arrangement.spacedBy(4.dp), ) { // ... }
O mesmo ocorreu para LazyRow
LazyRow( horizontalArrangement = Arrangement.spacedBy(4.dp), ) { // ... }
As grades aceitam arranjos verticais e horizontais:
LazyVerticalGrid( columns = GridCells.Fixed(2), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { items(photos) { item -> PhotoItem(item) } }
Chaves de itens
Por padrão, o estado de cada item é vinculado à posição dele na
lista ou grade. No entanto, isso pode causar problemas se o conjunto de dados mudar, já que os itens
que efetivamente mudam de posição perdem qualquer estado memorizado. Imaginando o cenário
de uma LazyRow
em uma LazyColumn
, caso a posição do item na linha mude,
o usuário perderá a posição de rolagem dentro da linha.
Para impedir isso, atribua uma chave estável e única a cada item, fornecendo
um bloco para o parâmetro key
. A chave estável permite que o estado do item seja
consistente em todas as mudanças do conjunto de dados:
LazyColumn { items( items = messages, key = { message -> // Return a stable + unique key for the item message.id } ) { message -> MessageRow(message) } }
Ao fornecer chaves, você ajuda o Compose a processar as reordenações corretamente. Por exemplo, se o item tiver um estado memorizado, a definição das chaves vai permitir que o Compose mova esse estado junto ao item na mudança de posição.
LazyColumn { items(books, key = { it.id }) { val rememberedValue = remember { Random.nextInt() } } }
Há uma limitação quanto aos tipos que você pode usar como chaves de itens.
O tipo da chave precisa ter suporte do
Bundle
, o mecanismo do Android para manter os
estados quando a atividade é recriada. O Bundle
oferece suporte a tipos primitivos,
enumerados ou parceláveis.
LazyColumn { items(books, key = { // primitives, enums, Parcelable, etc. }) { // ... } }
A chave precisa do suporte do Bundle
para que o rememberSaveable
dentro
do elemento combinável do item possa ser restaurado quando a atividade for recriada ou até mesmo
quando você rolar a tela para fora desse item e voltar.
LazyColumn { items(books, key = { it.id }) { val rememberedValue = rememberSaveable { Random.nextInt() } } }
Animações de itens
Se você já usou o widget RecyclerView, sabe que ele anima as mudanças
nos itens de forma automática.
Os layouts lentos oferecem a mesma funcionalidade para reordenações de itens.
A API é simples. Você só precisa definir o
modificador animateItem
para o conteúdo do item:
LazyColumn { // It is important to provide a key to each item to ensure animateItem() works as expected. items(books, key = { it.id }) { Row(Modifier.animateItem()) { // ... } } }
Você pode até fornecer especificações de animação personalizadas, se precisar:
LazyColumn { items(books, key = { it.id }) { Row( Modifier.animateItem( fadeInSpec = tween(durationMillis = 250), fadeOutSpec = tween(durationMillis = 100), placementSpec = spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy) ) ) { // ... } } }
Forneça chaves para seus itens para que seja possível encontrar a nova posição do elemento movido.
Cabeçalhos fixos (experimental)
O padrão "cabeçalho fixo" é útil para exibir listas de dados agrupados. Veja abaixo um exemplo de uma "lista de contatos", agrupada pela inicial de cada contato:
Para ter um cabeçalho fixo com LazyColumn
, use a função experimental
stickyHeader()
,
informando o conteúdo do cabeçalho:
@OptIn(ExperimentalFoundationApi::class) @Composable fun ListWithHeader(items: List<Item>) { LazyColumn { stickyHeader { Header() } items(items) { item -> ItemRow(item) } } }
Para ter uma lista com vários cabeçalhos, como no exemplo de "lista de contatos" acima, faça o seguinte:
// This ideally would be done in the ViewModel val grouped = contacts.groupBy { it.firstName[0] } @OptIn(ExperimentalFoundationApi::class) @Composable fun ContactsList(grouped: Map<Char, List<Contact>>) { LazyColumn { grouped.forEach { (initial, contactsForInitial) -> stickyHeader { CharacterHeader(initial) } items(contactsForInitial) { contact -> ContactListItem(contact) } } } }
Como reagir de acordo com a posição de rolagem
Muitos apps precisam reagir e detectar as mudanças de posição e layout dos itens.
Os componentes lentos empregam esse caso de uso elevando a
LazyListState
:
@Composable fun MessageList(messages: List<Message>) { // Remember our own LazyListState val listState = rememberLazyListState() // Provide it to LazyColumn LazyColumn(state = listState) { // ... } }
Para casos de uso simples, os apps geralmente só precisam ter informações sobre o
primeiro item visível. Para isso,
LazyListState
fornece as propriedades
firstVisibleItemIndex
e
firstVisibleItemScrollOffset
.
Considerando o exemplo de exibir ou ocultar um botão, dependendo de o usuário rolar a tela até passar do primeiro item:
@Composable fun MessageList(messages: List<Message>) { Box { val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } // Show the button if the first visible item is past // the first item. We use a remembered derived state to // minimize unnecessary compositions val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() } } }
Ler o estado diretamente na composição é útil quando você precisa atualizar
outros elementos combináveis na interface, mas também há cenários em que o evento não precisa
ser processado na mesma composição. Um exemplo comum disso é o envio de
um evento de análise depois que o usuário rola a tela e passa de um ponto determinado. Para processar isso
de forma eficiente, podemos usar um
snapshotFlow()
:
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .map { index -> index > 0 } .distinctUntilChanged() .filter { it } .collect { MyAnalyticsService.sendScrolledPastFirstItemEvent() } }
LazyListState
também fornece informações sobre todos os itens sendo exibidos
no momento e seus limites na tela,
usando a propriedade
layoutInfo
. Consulte a
classe LazyListLayoutInfo
para ver mais informações.
Como controlar a posição de rolagem
Além de reagir à posição de rolagem, também é útil que os apps possam
controlar a posição de rolagem.
LazyListState
oferece suporte para a função scrollToItem()
,
que ajusta imediatamente a
posição de rolagem, e com animateScrollToItem()
,
que executa a rolagem usando uma animação (também conhecida como rolagem suave):
@Composable fun MessageList(messages: List<Message>) { val listState = rememberLazyListState() // Remember a CoroutineScope to be able to launch val coroutineScope = rememberCoroutineScope() LazyColumn(state = listState) { // ... } ScrollToTopButton( onClick = { coroutineScope.launch { // Animate scroll to the first item listState.animateScrollToItem(index = 0) } } ) }
Conjuntos de dados grandes (paginação)
A biblioteca Paging permite que os apps
ofereçam suporte a grandes listas de itens, carregando e exibindo pequenos pedaços da lista conforme
necessário. A Paging 3.0 e versões mais recentes oferecem suporte ao Compose pela
biblioteca androidx.paging:paging-compose
.
Para exibir uma lista de conteúdo paginado, use a função de extensão
collectAsLazyPagingItems()
e, em seguida, transmita o resultado retornado
LazyPagingItems
para items()
na LazyColumn
. De forma semelhante ao suporte à Paging em visualizações, é possível
exibir marcadores de posição enquanto os dados são carregados verificando se item
é null
:
@Composable fun MessageList(pager: Pager<Int, Message>) { val lazyPagingItems = pager.flow.collectAsLazyPagingItems() LazyColumn { items( lazyPagingItems.itemCount, key = lazyPagingItems.itemKey { it.id } ) { index -> val message = lazyPagingItems[index] if (message != null) { MessageRow(message) } else { MessagePlaceholder() } } } }
Dicas para usar layouts lentos
Temos algumas dicas para garantir o funcionamento adequado dos layouts lentos.
Evitar o uso de itens com 0 pixel
Isso pode acontecer em cenários em que, por exemplo, você espera acessar alguns dados de forma assíncrona, como imagens, para preencher os itens da lista em uma fase seguinte. Com essa configuração, o layout lento vai fazer a composição de todos os itens na primeira medida, já que a altura deles vai ser de 0 pixel, cabendo, portanto, na janela de visualização. Depois que os itens forem carregados e a altura ampliada, os layouts lentos vão descartar todos os outros itens combinados sem necessidade na primeira vez, já que eles não cabem na janela de visualização. Para evitar esse problema, defina o dimensionamento padrão para os itens de modo que o layout lento possa fazer o cálculo correto de quantos cabem na janela de visualização:
@Composable fun Item(imageUrl: String) { AsyncImage( model = rememberAsyncImagePainter(model = imageUrl), modifier = Modifier.size(30.dp), contentDescription = null // ... ) }
Quando você sabe o tamanho aproximado dos itens depois que os dados são carregados de maneira assíncrona, recomendamos garantir que o dimensionamento permaneça o mesmo antes e depois do carregamento, por exemplo, adicionando alguns marcadores de posição. Isso ajuda a manter a posição de rolagem correta.
Evitar o aninhamento de componentes roláveis na mesma direção
Isso se aplica apenas a casos de aninhamento de elementos filhos roláveis sem um tamanho predefinido
dentro de um pai rolável na mesma direção. Por exemplo, veja a tentativa de
aninhar uma LazyColumn
filha sem uma altura fixa dentro de uma Column
mãe
rolável verticalmente:
// throws IllegalStateException Column( modifier = Modifier.verticalScroll(state) ) { LazyColumn { // ... } }
O mesmo resultado pode ser alcançado ao envolver todos os elementos combináveis
em uma LazyColumn
mãe e usar a DSL para transmitir diferentes tipos de
conteúdo. Isso permite a emissão de itens únicos e também de vários itens de lista
em um só lugar:
LazyColumn { item { Header() } items(data) { item -> PhotoItem(item) } item { Footer() } }
Lembre-se de que os casos em que você está aninhando layouts de direções diferentes,
por exemplo, uma Row
mãe rolável e uma LazyColumn
filha, são permitidos:
Row( modifier = Modifier.horizontalScroll(scrollState) ) { LazyColumn { // ... } }
Também são permitidos casos em que você ainda usa os mesmos layouts de direção, mas também define um tamanho fixo para os filhos aninhados:
Column( modifier = Modifier.verticalScroll(scrollState) ) { LazyColumn( modifier = Modifier.height(200.dp) ) { // ... } }
Cuidado ao colocar vários elementos em um item
Neste exemplo, a lambda do segundo item emite dois itens em um bloco:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Item(2) } item { Item(3) } // ... }
Os layouts lentos vão processar tudo como esperado. Os elementos vão ser mostrados um após o outro, como se fossem itens diferentes. No entanto, há alguns problemas com esse comportamento.
Quando vários elementos são emitidos como parte de um item, eles são processados como
uma entidade, ou seja, não podem mais ser combinados individualmente. Se um
elemento se tornar visível na tela, todos os elementos correspondentes ao
item vão precisar ser combinados e medidos. Isso pode prejudicar a performance se usado
excessivamente. No caso extremo de colocar todos os elementos em um item,
os layouts lentos se tornam totalmente inutilizados. Além de possíveis
problemas de performance, colocar mais elementos em um item também vai interferir
com scrollToItem()
e animateScrollToItem()
.
No entanto, há casos de uso válidos para colocar vários elementos em um item, como para ter divisores dentro de uma lista. Não é recomendado que os divisores mudem índices de rolagem, já que não podem ser considerados elementos independentes. Além disso, a performance não é afetada, já que os divisores são pequenos. Um divisor provavelmente vai precisar estar visível quando o item anterior também estiver visível para que possa fazer parte desse item:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Divider() } item { Item(2) } // ... }
Usar acordos personalizados
Geralmente, as listas lentas têm muitos itens e ocupam mais do que o tamanho do contêiner de rolagem. Quando a lista é preenchida com poucos itens, o design pode ter requisitos mais específicos para definir como eles são posicionados na janela de visualização.
Para isso, use o
Arrangement
vertical personalizado e o transmita à LazyColumn
. No exemplo abaixo, o objeto TopWithFooter
só precisa implementar o método arrange
. Primeiro, ele vai posicionar
os itens um após o outro. Depois, se a altura total usada for menor que a
altura da janela de visualização, o rodapé vai ser posicionado na parte de baixo:
object TopWithFooter : Arrangement.Vertical { override fun Density.arrange( totalSize: Int, sizes: IntArray, outPositions: IntArray ) { var y = 0 sizes.forEachIndexed { index, size -> outPositions[index] = y y += size } if (y < totalSize) { val lastIndex = outPositions.lastIndex outPositions[lastIndex] = totalSize - sizes.last() } } }
Adicionar contentType
Para maximizar o desempenho do layout lento, a partir do Compose 1.2
você pode adicionar
contentType
às listas ou grades. Dessa forma, é possível especificar o tipo de conteúdo de cada
item do layout quando você estiver criando uma lista ou grade composta
por vários tipos de itens:
LazyColumn { items(elements, contentType = { it.type }) { // ... } }
Quando o
contentType
é informado,
o Compose consegue reutilizar as composições
somente entre itens do mesmo tipo. Como a reutilização é mais eficiente ao
fazer a composição de itens de estrutura semelhante, informar os tipos de conteúdo garante que
o Compose não tente compor um item do tipo A sobre um item
do tipo B completamente diferente. Isso ajuda a maximizar os benefícios da reutilização
de composições e o desempenho do layout lento.
Medir o desempenho
Só é possível medir de forma confiável a performance de um layout lento quando ele é executado no modo de lançamento com a otimização do R8 ativada. Em builds de depuração, a rolagem do layout lento pode parecer mais devagar. Para mais informações, consulte Performance do Compose.
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Migrar
RecyclerView
para a lista Lazy - Salvar o estado da interface no Compose
- Kotlin para Jetpack Compose