Listas

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:

Vídeo de um smartphone rolando para cima e para baixo em uma lista de contatos

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.

Captura de tela de um smartphone mostrando uma grade de fotos

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)
        }
    }
}