Elenchi e griglie

Molte app devono mostrare raccolte di elementi. Questo documento spiega come eseguire questa operazione in modo efficiente in Jetpack Compose.

Se sai che il tuo caso d'uso non richiede lo scorrimento, ti consigliamo di utilizzare un semplice Column o Row (a seconda della direzione) ed emettere i contenuti di ogni elemento iterando su un elenco nel seguente modo:

@Composable
fun MessageList(messages: List<Message>) {
    Column {
        messages.forEach { message ->
            MessageRow(message)
        }
    }
}

Possiamo rendere scorrevole il Column utilizzando il modificatore verticalScroll().

Elenchi lazy

Se devi visualizzare un numero elevato di elementi (o un elenco di lunghezza sconosciuta), l'utilizzo di un layout come Column può causare problemi di prestazioni, poiché tutti gli elementi verranno composti e disposti indipendentemente dal fatto che siano visibili o meno.

Compose fornisce un insieme di componenti che compongono e organizzano solo gli elementi visibili nell'area visibile del componente. Questi componenti includono LazyColumn e LazyRow.

Come suggerisce il nome, la differenza tra LazyColumn e LazyRow è l'orientamento in cui vengono disposti gli elementi e lo scorrimento. LazyColumn genera un elenco con scorrimento verticale, mentre LazyRow genera un elenco con scorrimento orizzontale.

I componenti lazy sono diversi dalla maggior parte dei layout in Compose. Invece di accettare un parametro di blocco dei contenuti @Composable, che consente alle app di emettere direttamente i composabili, i componenti lazy forniscono un blocco LazyListScope.(). Questo blocco LazyListScope offre un DSL che consente alle app di descrivere i contenuti degli elementi. Il componente Lazy è quindi responsabile dell'aggiunta dei contenuti di ogni elemento in base al layout e alla posizione di scorrimento.

LazyListScope DSL

Il DSL di LazyListScope fornisce una serie di funzioni per descrivere gli elementi nel layout. Nella forma più semplice, item() aggiunge un singolo elemento, mentre items(Int) aggiunge più elementi:

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

Esistono anche una serie di funzioni di estensione che ti consentono di aggiungere raccolte di elementi, ad esempio un List. Queste estensioni ci consentono di eseguire facilmente la migrazione dell'esempio Column riportato sopra:

/**
 * import androidx.compose.foundation.lazy.items
 */
LazyColumn {
    items(messages) { message ->
        MessageRow(message)
    }
}

Esiste anche una variante della funzione di estensione items() chiamata itemsIndexed(), che fornisce l'indice. Per ulteriori dettagli, consulta la documentazione di riferimento LazyListScope.

Griglie lazy

I composabili LazyVerticalGrid e LazyHorizontalGrid supportano la visualizzazione degli elementi in una griglia. Una griglia verticale lazy visualizza i suoi elementi in un contenitore scorrevole verticalmente, su più colonne, mentre le griglie orizzontali lazy avranno lo stesso comportamento sull'asse orizzontale.

Le griglie hanno le stesse potenti funzionalità dell'API degli elenchi e utilizzano anche un linguaggio DSL molto simile, LazyGridScope.(), per descrivere i contenuti.

Screenshot di uno smartphone che mostra una griglia di foto

Il parametro columns in LazyVerticalGrid e il parametro rows in LazyHorizontalGrid controllano la modalità di formazione delle celle in colonne o righe. L'esempio riportato di seguito mostra gli elementi in una griglia, utilizzando GridCells.Adaptive per impostare ogni colonna su una larghezza minima di 128.dp:

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 128.dp)
) {
    items(photos) { photo ->
        PhotoItem(photo)
    }
}

LazyVerticalGrid ti consente di specificare una larghezza per gli elementi, in modo che la griglia adatti il numero più elevato possibile di colonne. L'eventuale larghezza rimanente viene distribuita equamente tra le colonne dopo il calcolo del numero di colonne. Questo metodo di adattamento delle dimensioni è particolarmente utile per visualizzare insiemi di elementi su schermi di dimensioni diverse.

Se conosci il numero esatto di colonne da utilizzare, puoi fornire un'istanza di GridCells.Fixed contenente il numero di colonne richieste.

Se il tuo design richiede che solo alcuni elementi abbiano dimensioni non standard, puoi utilizzare il supporto della griglia per fornire intervalli di colonne personalizzati per gli elementi. Specifica l'intervallo di colonne con il parametro span dei metodi LazyGridScope DSL item e items. maxLineSpan, uno dei valori dell'ambito di span, è particolarmente utile quando utilizzi le dimensioni adattabili, perché il numero di colonne non è fisso. Questo esempio mostra come specificare un intervallo di righe completo:

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 30.dp)
) {
    item(span = {
        // LazyGridItemSpanScope:
        // maxLineSpan
        GridItemSpan(maxLineSpan)
    }) {
        CategoryCard("Fruits")
    }
    // ...
}

Griglia sfalsata lazy

LazyVerticalStaggeredGrid e LazyHorizontalStaggeredGrid sono composabili che ti consentono di creare una griglia sfalsata con caricamento differito di elementi. Una griglia sfalsata verticale lazy mostra i suoi elementi in un contenitore scorrevole verticalmente che si estende su più colonne e consente ai singoli elementi di avere altezze diverse. Le griglie orizzontali lazy hanno lo stesso comportamento sull'asse orizzontale con elementi di larghezza diversa.

Lo snippet seguente è un esempio di base dell'utilizzo di LazyVerticalStaggeredGrid con una larghezza di 200.dp per elemento:

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

Figura 1. Esempio di griglia verticale sfalsata lazy

Per impostare un numero fisso di colonne, puoi utilizzare StaggeredGridCells.Fixed(columns) anziché StaggeredGridCells.Adaptive. La larghezza disponibile viene divisa per il numero di colonne (o righe per una griglia orizzontale) e ogni elemento occupa questa larghezza (o altezza per una griglia orizzontale):

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()
)
Griglia sfalsata di immagini con caricamento lento in Scrivi
Figura 2. Esempio di griglia verticale lazy sfalsata con colonne fisse

Spaziatura interna dei contenuti

A volte è necessario aggiungere spaziatura ai bordi dei contenuti. I componenti lazy consentono di passare alcuni PaddingValues al parametro contentPadding per supportare questa funzionalità:

LazyColumn(
    contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
) {
    // ...
}

In questo esempio, aggiungiamo 16.dp di spaziatura ai bordi orizzontali (sinistra e a destra) e 8.dp alla parte superiore e inferiore dei contenuti.

Tieni presente che questo spazio viene applicato ai contenuti, non al LazyColumn stesso. Nell'esempio precedente, il primo elemento aggiungerà un padding di 8.dp in alto, l'ultimo aggiungerà 8.dp in basso e tutti gli elementi avranno un padding di 16.dp a sinistra e a destra.

Spaziatura dei contenuti

Per aggiungere spazi tra gli elementi, puoi utilizzare Arrangement.spacedBy(). L'esempio seguente aggiunge 4.dp di spazio tra ogni elemento:

LazyColumn(
    verticalArrangement = Arrangement.spacedBy(4.dp),
) {
    // ...
}

Analogamente per LazyRow:

LazyRow(
    horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
    // ...
}

Le griglie, invece, accettano sia le disposizioni verticali che quelle orizzontali:

LazyVerticalGrid(
    columns = GridCells.Fixed(2),
    verticalArrangement = Arrangement.spacedBy(16.dp),
    horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
    items(photos) { item ->
        PhotoItem(item)
    }
}

Chiavi elemento

Per impostazione predefinita, lo stato di ogni elemento è associato alla posizione dell'elemento nell'elenco o nella griglia. Tuttavia, questo può causare problemi se il set di dati cambia, poiché gli elementi che cambiano posizione perdono effettivamente lo stato memorizzato. Se immagini lo scenario di LazyRow all'interno di un LazyColumn, se la riga modifica la posizione dell'elemento, l'utente perderà la posizione di scorrimento all'interno della riga.

.

Per risolvere il problema, puoi fornire una chiave stabile e univoca per ogni elemento, fornendo un blocco al parametro key. Fornendo una chiave stabile, lo stato dell'elemento sarà coerente nelle modifiche del set di dati:

LazyColumn {
    items(
        items = messages,
        key = { message ->
            // Return a stable + unique key for the item
            message.id
        }
    ) { message ->
        MessageRow(message)
    }
}

Fornendo le chiavi, aiuti Compose a gestire correttamente i riordini. Ad esempio, se l'elemento contiene lo stato memorizzato, le chiavi di impostazione consentono a Compose di spostare questo stato insieme all'elemento quando la sua posizione cambia.

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = remember {
            Random.nextInt()
        }
    }
}

Tuttavia, esiste una limitazione per i tipi che puoi utilizzare come chiavi elemento. Il tipo di chiave deve essere supportato da Bundle, il meccanismo di Android per mantenere gli stati quando l'attività viene ricreata. Bundle supporta tipi come primitivi, enumerazioni o Parcelable.

LazyColumn {
    items(books, key = {
        // primitives, enums, Parcelable, etc.
    }) {
        // ...
    }
}

La chiave deve essere supportata da Bundle in modo che il rememberSaveable all'interno del composable dell'elemento possa essere ripristinato quando l'attività viene ricreata o anche quando scorri da questo elemento e torni indietro.

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = rememberSaveable {
            Random.nextInt()
        }
    }
}

Animazioni degli elementi

Se hai utilizzato il widget RecyclerView, saprai che anima le modifiche degli elementi automaticamente. I layout lazy offrono la stessa funzionalità per il riassetto degli elementi. L'API è semplice: devi solo impostare il modificatore animateItem nei contenuti dell'articolo:

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()) {
            // ...
        }
    }
}

Se necessario, puoi anche fornire una specifica di animazione personalizzata:

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)
            )
        ) {
            // ...
        }
    }
}

Assicurati di fornire chiavi per gli elementi in modo da poter trovare la nuova posizione dell'elemento spostato.

Intestazioni fisse (sperimentale)

Il pattern "intestazione fissa" è utile per visualizzare elenchi di dati raggruppati. Di seguito puoi vedere un esempio di "elenco di contatti", raggruppato in base all'iniziale di ogni contatto:

Video di uno smartphone che scorre verso l&#39;alto e verso il basso in un elenco contatti

Per ottenere un'intestazione fissa con LazyColumn, puoi utilizzare la funzione sperimentale stickyHeader() fornendo i contenuti dell'intestazione:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ListWithHeader(items: List<Item>) {
    LazyColumn {
        stickyHeader {
            Header()
        }

        items(items) { item ->
            ItemRow(item)
        }
    }
}

Per creare un elenco con più intestazioni, come l'esempio "elenco contatti" riportato sopra, puoi:

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

Reagire alla posizione di scorrimento

Molte app devono reagire e ascoltare le modifiche alla posizione di scorrimento e al layout degli elementi. I componenti Lazy supportano questo caso d'uso eseguendo il sollevamento del LazyListState:

@Composable
fun MessageList(messages: List<Message>) {
    // Remember our own LazyListState
    val listState = rememberLazyListState()

    // Provide it to LazyColumn
    LazyColumn(state = listState) {
        // ...
    }
}

Per casi d'uso semplici, in genere le app devono conoscere solo le informazioni sul primo elemento visibile. Per questo, LazyListState fornisce le proprietà firstVisibleItemIndex e firstVisibleItemScrollOffset.

Se utilizziamo l'esempio di un pulsante che viene mostrato e nascosto in base al fatto che l'utente abbia superato il primo elemento:

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

La lettura dello stato direttamente nella composizione è utile quando devi aggiornare altri composabili dell'interfaccia utente, ma esistono anche scenari in cui l'evento non deve essere gestito nella stessa composizione. Un esempio comune è l'invio di un evento di analisi dopo che l'utente ha superato un determinato punto. Per gestire questo problema in modo efficiente, possiamo utilizzare un snapshotFlow():

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

LazyListState fornisce anche informazioni su tutti gli elementi attualmente visualizzati e sui relativi limiti sullo schermo tramite la proprietà layoutInfo. Per ulteriori informazioni, consulta la classe LazyListLayoutInfo.

Controllare la posizione di scorrimento

Oltre a reagire alla posizione di scorrimento, è utile anche per le app poter controllare la posizione di scorrimento. LazyListState supporta questa funzionalità tramite la funzione scrollToItem(), che "blocca immediatamente" la posizione di scorrimento e animateScrollToItem(), che scorre utilizzando un'animazione (nota anche come scorrimento fluido):

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

Set di dati di grandi dimensioni (paginazione)

La libreria di paginazione consente alle app di supportare elenchi di elementi di grandi dimensioni, caricando e mostrando piccoli blocchi dell'elenco in base alle necessità. Paging 3.0 e versioni successive forniscono il supporto di Compose tramite la libreriaandroidx.paging:paging-compose.

Per visualizzare un elenco di contenuti paginati, possiamo utilizzare la funzione di estensione collectAsLazyPagingItems() e poi passare il valore LazyPagingItems riferito a items() nel nostro LazyColumn. Analogamente al supporto della paginazione nelle visualizzazioni, puoi visualizzare i segnaposto durante il caricamento dei dati controllando 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()
            }
        }
    }
}

Suggerimenti per l'utilizzo dei layout lazy

Esistono alcuni suggerimenti che puoi tenere in considerazione per assicurarti che i layout lazy funzionino come previsto.

Evita di utilizzare elementi di dimensioni pari a 0 pixel

Questo può accadere in scenari in cui, ad esempio, prevedi di recuperare in modo asincrono alcuni dati, come le immagini, per completare gli elementi dell'elenco in un secondo momento. Ciò causerebbe al layout lazy di comporre tutti i suoi elementi nella prima misurazione, poiché la loro altezza è pari a 0 pixel e potrebbero essere inseriti tutti nell'area visibile. Una volta caricati gli elementi e ampliata la loro altezza, i layout lazy rimuoverebbero tutti gli altri elementi che sono stati composti inutilmente la prima volta perché non possono adattarsi all'area visibile. Per evitare questo problema, devi impostare le dimensioni predefinite per gli elementi, in modo che il layout lazy possa eseguire il calcolo corretto del numero di elementi che possono effettivamente essere visualizzati nell'area visibile:

@Composable
fun Item(imageUrl: String) {
    AsyncImage(
        model = rememberAsyncImagePainter(model = imageUrl),
        modifier = Modifier.size(30.dp),
        contentDescription = null
        // ...
    )
}

Se conosci le dimensioni approssimative degli elementi dopo il caricamento asincrono dei dati, è buona norma assicurarti che rimangano invariate prima e dopo il caricamento, ad esempio aggiungendo alcuni segnaposto. In questo modo, manterrai la posizione di scorrimento corretta.

Evita di nidificare componenti scorrevoli nella stessa direzione

Questo vale solo per i casi in cui vengono nidificati elementi secondari scorrevoli senza una dimensione predefinita all'interno di un altro elemento principale scorrevole nella stessa direzione. Ad esempio, prova a nidificare un elemento secondario LazyColumn senza altezza fissa all'interno di un elemento principale Column scorrevole verticalmente:

// throws IllegalStateException
Column(
    modifier = Modifier.verticalScroll(state)
) {
    LazyColumn {
        // ...
    }
}

Invece, puoi ottenere lo stesso risultato inserendo tutti i composabili in un elemento principale LazyColumn e utilizzando il relativo DSL per trasmettere diversi tipi di contenuti. In questo modo puoi emettere singoli elementi e più elementi dell'elenco, tutto in un'unica posizione:

LazyColumn {
    item {
        Header()
    }
    items(data) { item ->
        PhotoItem(item)
    }
    item {
        Footer()
    }
}

Tieni presente che sono consentiti casi in cui nidifichi layout con direzioni diverse, ad esempio un elemento principale Row scorrevole e un elemento secondario LazyColumn:

Row(
    modifier = Modifier.horizontalScroll(scrollState)
) {
    LazyColumn {
        // ...
    }
}

nonché nei casi in cui utilizzi ancora gli stessi layout di direzione, ma imposti anche una dimensione fissa per gli elementi secondari nidificati:

Column(
    modifier = Modifier.verticalScroll(scrollState)
) {
    LazyColumn(
        modifier = Modifier.height(200.dp)
    ) {
        // ...
    }
}

Fai attenzione a non inserire più elementi in un elemento

In questo esempio, il secondo elemento lambda emette 2 elementi in un blocco:

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp)
) {
    item { Item(0) }
    item {
        Item(1)
        Item(2)
    }
    item { Item(3) }
    // ...
}

I layout lazy gestiranno questo problema come previsto: disporranno gli elementi uno dopo l'altro come se fossero elementi diversi. Tuttavia, esistono alcuni problemi.

Quando vengono emessi più elementi all'interno di un elemento, vengono gestiti come un'unica entità, il che significa che non possono più essere composti singolarmente. Se un elemento diventa visibile sullo schermo, tutti gli elementi corrispondenti devono essere composti e misurati. Ciò può influire negativamente sul rendimento se usato in modo eccessivo. Nel caso estremo di mettere tutti gli elementi in un elemento, viene completamente vanificato lo scopo dell'utilizzo dei layout lazy. Oltre ai potenziali problemi di rendimento, l'inserimento di più elementi in un elemento interferirà anche con scrollToItem() e animateScrollToItem().

Tuttavia, esistono casi d'uso validi per inserire più elementi in un elemento, come avere divisori all'interno di un elenco. Non vuoi che i separatori modifichino gli indici di scorrimento, poiché non devono essere considerati elementi indipendenti. Inoltre, il rendimento non sarà interessato perché i divisori sono piccoli. È probabile che un divisore debba essere visualizzato quando è visibile l'elemento precedente, in modo da poter far parte dell'elemento precedente:

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp)
) {
    item { Item(0) }
    item {
        Item(1)
        Divider()
    }
    item { Item(2) }
    // ...
}

Valuta la possibilità di utilizzare arrangiamenti personalizzati

In genere, gli elenchi lazy contengono molti elementi e occupano più delle dimensioni del contenitore scorrevole. Tuttavia, quando l'elenco è compilato con pochi elementi, il design può avere requisiti più specifici per il posizionamento degli elementi nel viewport.

Per farlo, puoi utilizzare il verticale personalizzato Arrangement e passarlo a LazyColumn. Nell'esempio seguente, l'oggetto TopWithFooter deve implementare solo il metodo arrange. Innanzitutto, posizionerà gli elementi uno dopo l'altro. In secondo luogo, se l'altezza totale utilizzata è inferiore all'altezza del viewport, il piè di pagina verrà posizionato in basso:

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

Valuta la possibilità di aggiungere contentType

A partire da Compose 1.2, per massimizzare il rendimento del layout Lazy, ti consigliamo di aggiungere contentType ai tuoi elenchi o alle tue griglie. In questo modo puoi specificare il tipo di contenuti per ogni elemento del layout, nei casi in cui stai componendo un elenco o una griglia composta da più tipi di elementi diversi:

LazyColumn {
    items(elements, contentType = { it.type }) {
        // ...
    }
}

Quando fornisci contentType, Compose è in grado di riutilizzare le composizioni solo tra gli elementi dello stesso tipo. Poiché il riutilizzo è più efficiente quando componi elementi di struttura simile, fornire i tipi di contenuti garantisce che Compose non provi a comporre un elemento di tipo A sopra un elemento di tipo B completamente diverso. In questo modo, puoi massimizzare i vantaggi del riutilizzo della composizione e il rendimento del layout lazy.

Misurare il rendimento

Puoi misurare in modo affidabile il rendimento di un layout lazy solo quando esegui il codice in modalità di rilascio e con l'ottimizzazione R8 abilitata. Nelle build di debug, lo scorrimento del layout lazy potrebbe sembrare più lento. Per ulteriori informazioni, consulta Rendimento di Compose.