Muitos apps precisam exibir 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 a
partir da iteração em uma lista, da seguinte forma:
@Composable
fun MessageList(messages: List<Message>) {
Column {
messages.forEach { message ->
MessageRow(message)
}
}
}
É possível permitir a rolagem de Column
usando o modificador verticalScroll()
.
Consulte a documentação sobre Gestos
para ver mais informações.
Componentes lentos que podem ser compostos
Se você precisar exibir muitos itens (ou uma lista de tamanho desconhecido),
usar um layout como uma Column
pode causar problemas de desempenho, já que todos os
itens serão compostos e dispostos independentemente de estarem visíveis ou não.
O Compose fornece um conjunto de componentes que compõe e dispõe somente 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 que podem ser compostos 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
@Composable
fun MessageList(messages: List<Message>) {
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 ver mais detalhes.
Padding de conteúdo
Às vezes, você precisará adicionar padding ao redor das bordas do conteúdo. Os componentes
lentos permitem que você transmita alguns
PaddingValues
ao parâmetro contentPadding
para que sejam compatíveis com 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 superior e inferior do conteúdo.
O padding é aplicado ao conteúdo, não à
LazyColumn
em si. No exemplo acima, o primeiro item adicionará 8.dp
de padding à parte superior, o último item adicionará 8.dp
à parte inferior 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),
) {
// ...
}
Animações de itens
Se você já usou o widget RecyclerView, saberá que ele anima as mudanças nos itens de forma automática. Os layouts lentos ainda não oferecem esse recurso, o que significa que as mudanças de item causam um ajuste instantâneo. Acompanhe esse bug para rastrear as mudanças nesse recurso.
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:
// TODO: 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)
}
}
}
}
Grades (experimental)
O
componente que pode composto LazyVerticalGrid
oferece compatibilidade experimental para exibir itens em formato de grade.
O parâmetro cells
controla a forma como as células são organizadas em colunas. O exemplo
a seguir exibe itens em grade, usando
GridCells.Adaptive
para definir cada coluna com pelo menos 128.dp
de largura:
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PhotoGrid(photos: List<Photo>) {
LazyVerticalGrid(
cells = GridCells.Adaptive(minSize = 128.dp)
) {
items(photos) { photo ->
PhotoItem(photo)
}
}
}
Caso você saiba o número exato de colunas que serão usadas, forneça uma
instância de
GridCells.Fixed
contendo o número de colunas necessárias.
Como reagir de acordo com a posição de rolagem
Muitos apps precisam reagir e ouvir 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:
@OptIn(ExperimentalAnimationApi::class) // AnimatedVisibility
@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 componentes de IU que podem ser compostos, 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 == true }
.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
é compatível com 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 tranquila):
@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
sejam compatíveis com grandes listas de itens, carregando e exibindo pequenos pedaços da lista conforme
necessário. A Paging 3.0 e versões mais recentes são compatíveis com o 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 à compatibilidade da Paging em visualizações, é possível
exibir marcadores enquanto os dados são carregados, verificando se item
é null
:
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
@Composable
fun MessageList(pager: Pager<Int, Message>) {
val lazyPagingItems = pager.flow.collectAsLazyPagingItems()
LazyColumn {
items(lazyPagingItems) { message ->
if (message != null) {
MessageRow(message)
} else {
MessagePlaceholder()
}
}
}
}
Chaves de itens
Por padrão, o estado de cada item é vinculado à posição do item na
lista. 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:
@Composable
fun MessageList(messages: List<Message>) {
LazyColumn {
items(
items = messages,
key = { message ->
// Return a stable + unique key for the item
message.id
}
) { message ->
MessageRow(message)
}
}
}