Elenchi e griglie

Molte app devono visualizzare raccolte di elementi. Questo documento spiega come puoi farlo 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 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 dispongono 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 dispongono gli elementi e scorrono. LazyColumn produce un elenco a scorrimento verticale, mentre LazyRow produce un elenco a scorrimento orizzontale.

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

LazyListScope DSL

Il DSL di LazyListScope fornisce una serie di funzioni per descrivere gli elementi nel layout. Nella sua 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 diverse funzioni di estensione che 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 maggiori dettagli, consulta il riferimento LazyListScope.

Griglie lazy

I composable LazyVerticalGrid e LazyHorizontalGrid forniscono supporto per la visualizzazione degli elementi in una griglia. Una griglia verticale pigra mostrerà i suoi elementi in un contenitore scorrevole verticalmente, distribuito su più colonne, mentre le griglie orizzontali pigre avranno lo stesso comportamento sull'asse orizzontale.

Le griglie hanno le stesse potenti funzionalità API degli elenchi e utilizzano anche un 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 il modo in cui le celle vengono formate in colonne o righe. L'esempio seguente mostra gli elementi in una griglia, utilizzando GridCells.Adaptive per impostare ogni colonna in modo che sia larga almeno 128.dp:

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

LazyVerticalGrid ti consente di specificare una larghezza per gli elementi, quindi la griglia inserirà il maggior numero possibile di colonne. La larghezza rimanente viene distribuita equamente tra le colonne, dopo il calcolo del numero di colonne. Questo modo adattivo di dimensionamento è particolarmente utile per visualizzare set 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 determinati 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 dello span, è particolarmente utile quando utilizzi il dimensionamento adattivo, perché il numero di colonne non è fisso. Questo esempio mostra come fornire un intervallo di riga completo:

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

Griglia sfalsata pigra

LazyVerticalStaggeredGrid e LazyHorizontalStaggeredGrid sono composable che ti consentono di creare una griglia sfalsata di elementi caricati in modalità differita. Una griglia verticale sfalsata pigra mostra i suoi elementi in un contenitore a scorrimento verticale che si estende su più colonne e consente ai singoli elementi di avere altezze diverse. Le griglie orizzontali pigre hanno lo stesso comportamento sull'asse orizzontale con elementi di larghezza diversa.

Lo snippet seguente è un esempio di base di utilizzo di LazyVerticalStaggeredGrid con una larghezza 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 pigra

Per impostare un numero fisso di colonne, puoi utilizzare StaggeredGridCells.Fixed(columns) anziché StaggeredGridCells.Adaptive. In questo modo, la larghezza disponibile viene divisa per il numero di colonne (o righe per una griglia orizzontale) e ogni elemento occupa quella 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 Crea
Figura 2. Esempio di griglia verticale sfalsata pigra con colonne fisse

Spazio interno dei contenuti

A volte è necessario aggiungere un riempimento intorno ai bordi dei contenuti. I componenti lazy ti consentono di passare alcuni PaddingValues al parametro contentPadding per supportare questa operazione:

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

In questo esempio, aggiungiamo 16.dp di spaziatura interna ai bordi orizzontali (sinistro e destro), quindi 8.dp alla parte superiore e inferiore dei contenuti.

Tieni presente che questo spazio interno viene applicato ai contenuti, non al LazyColumn stesso. Nell'esempio precedente, il primo elemento aggiungerà un riempimento 8.dp alla parte superiore, l'ultimo elemento aggiungerà un riempimento 8.dp alla parte inferiore e tutti gli elementi avranno un riempimento 16.dp a sinistra e a destra.

Come altro esempio, puoi passare Scaffold's PaddingValues in LazyColumn's contentPadding. Consulta la guida da bordo a bordo.

Spaziatura dei contenuti

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

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

Analogamente per LazyRow:

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

Le griglie, invece, accettano sia disposizioni verticali che 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 viene associato alla posizione dell'elemento nell'elenco o nella griglia. Tuttavia, ciò può causare problemi se il set di dati cambia, poiché gli elementi che cambiano posizione perdono effettivamente qualsiasi stato memorizzato. Se immagini lo scenario di LazyRow all'interno di un LazyColumn, se la riga cambia la posizione dell'elemento, l'utente perderebbe la posizione di scorrimento all'interno della riga.

.

Per contrastare questo problema, puoi fornire una chiave stabile e univoca per ogni elemento, fornendo un blocco al parametro key. Fornire una chiave stabile consente di mantenere lo stato dell'elemento coerente tra le 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, l'impostazione delle chiavi consentirebbe 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 sui tipi che puoi utilizzare come chiavi degli elementi. 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 primitive, enumerazioni o Parcelable.

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

La chiave deve essere supportata da Bundle in modo che rememberSaveable all'interno del composable dell'elemento possa essere ripristinato quando l'attività viene ricreata o anche quando scorri lontano da questo elemento e poi 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 automaticamente le modifiche degli elementi. I layout pigri forniscono la stessa funzionalità per il riordino degli elementi. L'API è semplice: devi solo impostare il modificatore animateItem per i 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 specifiche di animazione personalizzate:

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 le chiavi per i tuoi elementi in modo che sia possibile trovare la nuova posizione dell'elemento spostato.

Esempio: animare gli elementi negli elenchi pigri

Con Compose, puoi animare le modifiche agli elementi negli elenchi pigri. Se utilizzati insieme, i seguenti snippet implementano le animazioni quando si aggiungono, rimuovono e riordinano gli elementi delle liste pigre.

Questo snippet mostra un elenco di stringhe con transizioni animate quando gli elementi vengono aggiunti, rimossi o riordinati:

@Composable
fun ListAnimatedItems(
    items: List<String>,
    modifier: Modifier = Modifier
) {
    LazyColumn(modifier) {
        // Use a unique key per item, so that animations work as expected.
        items(items, key = { it }) {
            ListItem(
                headlineContent = { Text(it) },
                modifier = Modifier
                    .animateItem(
                        // Optionally add custom animation specs
                    )
                    .fillParentMaxWidth()
                    .padding(horizontal = 8.dp, vertical = 0.dp),
            )
        }
    }
}

Punti chiave del codice

  • ListAnimatedItems mostra un elenco di stringhe in un LazyColumn con transizioni animate quando gli elementi vengono modificati.
  • La funzione items assegna una chiave univoca a ogni elemento dell'elenco. Compose utilizza le chiavi per monitorare gli elementi e identificare le modifiche alle loro posizioni.
  • ListItem definisce il layout di ogni elemento dell'elenco. Accetta un parametro headlineContent che definisce i contenuti principali dell'elemento.
  • Il modificatore animateItem applica le animazioni predefinite alle aggiunte, alle rimozioni e agli spostamenti degli elementi.

Il seguente snippet mostra una schermata che include i controlli per aggiungere e rimuovere elementi, nonché per ordinare un elenco predefinito:

@Composable
private fun ListAnimatedItemsExample(
    data: List<String>,
    modifier: Modifier = Modifier,
    onAddItem: () -> Unit = {},
    onRemoveItem: () -> Unit = {},
    resetOrder: () -> Unit = {},
    onSortAlphabetically: () -> Unit = {},
    onSortByLength: () -> Unit = {},
) {
    val canAddItem = data.size < 10
    val canRemoveItem = data.isNotEmpty()

    Scaffold(modifier) { paddingValues ->
        Column(
            modifier = Modifier
                .padding(paddingValues)
                .fillMaxSize()
        ) {
            // Buttons that change the value of displayedItems.
            AddRemoveButtons(canAddItem, canRemoveItem, onAddItem, onRemoveItem)
            OrderButtons(resetOrder, onSortAlphabetically, onSortByLength)

            // List that displays the values of displayedItems.
            ListAnimatedItems(data)
        }
    }
}

Punti chiave del codice

  • ListAnimatedItemsExample mostra una schermata che include i controlli per aggiungere, rimuovere e ordinare gli elementi.
    • onAddItem e onRemoveItem sono espressioni lambda passate a AddRemoveButtons per aggiungere e rimuovere elementi dall'elenco.
    • resetOrder, onSortAlphabetically e onSortByLength sono espressioni lambda trasmesse a OrderButtons per modificare l'ordine degli elementi nell'elenco.
  • AddRemoveButtons vengono visualizzati i pulsanti "Aggiungi" e "Rimuovi". Attiva/disattiva i pulsanti e gestisce i clic sui pulsanti.
  • OrderButtons mostra i pulsanti per riordinare l'elenco. Riceve le funzioni lambda per reimpostare l'ordine e ordinare l'elenco per lunghezza o in ordine alfabetico.
  • ListAnimatedItems chiama il componibile ListAnimatedItems, passando l'elenco data per visualizzare l'elenco animato di stringhe. data è definito altrove.

Questo snippet crea un'interfaccia utente con i pulsanti Aggiungi elemento ed Elimina elemento:

@Composable
private fun AddRemoveButtons(
    canAddItem: Boolean,
    canRemoveItem: Boolean,
    onAddItem: () -> Unit,
    onRemoveItem: () -> Unit
) {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.Center
    ) {
        Button(enabled = canAddItem, onClick = onAddItem) {
            Text("Add Item")
        }
        Spacer(modifier = Modifier.padding(25.dp))
        Button(enabled = canRemoveItem, onClick = onRemoveItem) {
            Text("Delete Item")
        }
    }
}

Punti chiave del codice

  • AddRemoveButtons mostra una riga di pulsanti per eseguire operazioni di aggiunta e rimozione nell'elenco.
  • I parametri canAddItem e canRemoveItem controllano lo stato di attivazione dei pulsanti. Se canAddItem o canRemoveItem sono false, il pulsante corrispondente è disattivato.
  • I parametri onAddItem e onRemoveItem sono lambda che vengono eseguiti quando l'utente fa clic sul pulsante corrispondente.

Infine, questo snippet mostra tre pulsanti per ordinare l'elenco (Reimposta, Alfabetico e Durata):

@Composable
private fun OrderButtons(
    resetOrder: () -> Unit,
    orderAlphabetically: () -> Unit,
    orderByLength: () -> Unit
) {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.Center
    ) {
        var selectedIndex by remember { mutableIntStateOf(0) }
        val options = listOf("Reset", "Alphabetical", "Length")

        SingleChoiceSegmentedButtonRow {
            options.forEachIndexed { index, label ->
                SegmentedButton(
                    shape = SegmentedButtonDefaults.itemShape(
                        index = index,
                        count = options.size
                    ),
                    onClick = {
                        Log.d("AnimatedOrderedList", "selectedIndex: $selectedIndex")
                        selectedIndex = index
                        when (options[selectedIndex]) {
                            "Reset" -> resetOrder()
                            "Alphabetical" -> orderAlphabetically()
                            "Length" -> orderByLength()
                        }
                    },
                    selected = index == selectedIndex
                ) {
                    Text(label)
                }
            }
        }
    }
}

Punti chiave del codice

  • OrderButtons mostra un SingleChoiceSegmentedButtonRow che consente agli utenti di selezionare un metodo di ordinamento nell'elenco o reimpostare l'ordine dell'elenco. Un componente SegmentedButton ti consente di selezionare una singola opzione da un elenco di opzioni.
  • resetOrder, orderAlphabetically e orderByLength sono funzioni lambda che vengono eseguite quando viene selezionato il pulsante corrispondente.
  • La variabile di stato selectedIndex tiene traccia dell'opzione selezionata.

Risultato

Questo video mostra il risultato degli snippet precedenti quando gli elementi vengono riordinati:

Figura 1. Un elenco che anima le transizioni degli elementi quando vengono aggiunti, rimossi o ordinati.

Intestazioni fisse (sperimentale)

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

Video di uno smartphone che scorre su e giù 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 ottenere un elenco con più intestazioni, come nell'esempio dell'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 tramite l'hoisting di LazyListState:

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

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

Per i casi d'uso semplici, le app in genere 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 allo scorrimento dell'utente oltre 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 composable dell'interfaccia utente, ma ci sono anche scenari in cui l'evento non deve essere gestito nella stessa composizione. Un esempio comune è l'invio di un evento Analytics quando l'utente ha superato un determinato punto. Per gestire questa situazione 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 saperne di più, consulta il corso LazyListLayoutInfo.

Controllare la posizione di scorrimento

Oltre a reagire alla posizione di scorrimento, è utile anche che le app possano controllare la posizione di scorrimento. LazyListState lo supporta tramite la funzione scrollToItem(), che sposta "immediatamente" la posizione di scorrimento, e animateScrollToItem(), che scorre utilizzando un'animazione (nota anche come scorrimento uniforme):

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

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

Per visualizzare un elenco di contenuti suddivisi in pagine, possiamo utilizzare la funzione di estensione collectAsLazyPagingItems() e quindi passare il valore LazyPagingItems restituito a items() in 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 pigri

Esistono alcuni suggerimenti che puoi prendere in considerazione per assicurarti che i layout pigri funzionino come previsto.

Evita di utilizzare elementi di dimensioni pari a 0 pixel

Ciò può verificarsi in scenari in cui, ad esempio, prevedi di recuperare in modo asincrono alcuni dati come le immagini per compilare gli elementi dell'elenco in un secondo momento. In questo modo, il layout Lazy comporrebbe tutti i suoi elementi nella prima misurazione, poiché la loro altezza è di 0 pixel e potrebbe inserirli tutti nell'area visibile. Una volta caricati gli elementi e la loro altezza espansa, i layout pigri scartano tutti gli altri elementi che sono stati composti inutilmente la prima volta, in quanto non possono adattarsi all'area visibile. Per evitare questo problema, devi impostare il dimensionamento predefinito per gli elementi, in modo che il layout pigro possa eseguire il calcolo corretto di quanti elementi possono effettivamente essere inseriti nell'area visibile:

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

Quando conosci le dimensioni approssimative degli elementi dopo il caricamento asincrono dei dati, una buona pratica è assicurarsi che le dimensioni degli elementi rimangano invariate prima e dopo il caricamento, ad esempio aggiungendo alcuni segnaposto. In questo modo, la posizione di scorrimento corretta verrà mantenuta.

Evita di nidificare componenti scorrevoli nella stessa direzione

Ciò si applica solo ai casi in cui si annidano elementi secondari scorrevoli senza una dimensione predefinita all'interno di un altro elemento principale scorrevole nella stessa direzione. Ad esempio, se provi a inserire un elemento secondario LazyColumn senza un'altezza fissa all'interno di un elemento principale Column scorrevole verticalmente:

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

Lo stesso risultato può essere ottenuto racchiudendo tutti i composable all'interno di un LazyColumn principale e utilizzando il relativo DSL per passare diversi tipi di contenuti. Ciò consente di emettere singoli elementi, nonché più elementi di elenco, tutto in un unico posto:

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

Tieni presente che sono consentiti i casi in cui nidifichi layout di direzione diversi, ad esempio un elemento principale scorrevole Row e un elemento secondario LazyColumn:

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

Oltre ai 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 unico elemento

In questo esempio, la lambda del secondo elemento emette due elementi in un blocco:

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

I layout pigri gestiranno questo aspetto come previsto: disporranno gli elementi uno dopo l'altro come se fossero elementi diversi. Tuttavia, ci sono un paio di problemi.

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

Tuttavia, esistono casi d'uso validi per inserire più elementi in un unico elemento, ad esempio i divisori all'interno di un elenco. Non vuoi che i divisori cambino gli indici di scorrimento, in quanto non devono essere considerati elementi indipendenti. Inoltre, le prestazioni non saranno interessate perché i divisori sono piccoli. Un divisore dovrà probabilmente essere visibile quando l'elemento precedente è visibile, in modo che possa 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

Di solito, gli elenchi pigri contengono molti elementi e occupano uno spazio maggiore rispetto alle dimensioni del contenitore di scorrimento. Tuttavia, quando l'elenco è popolato da pochi elementi, il design può avere requisiti più specifici per il posizionamento nel riquadro visibile.

Per farlo, puoi utilizzare un 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 riquadro visibile, 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, valuta la possibilità di aggiungere contentType a elenchi o 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 costituita da più tipi diversi di elementi:

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

Quando fornisci contentType, Compose è in grado di riutilizzare le composizioni solo tra gli elementi dello stesso tipo. Il riutilizzo è più efficiente quando componi elementi di struttura simile. La fornitura dei tipi di contenuti garantisce che Compose non tenti di comporre un elemento di tipo A sopra un elemento completamente diverso di tipo B. 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 viene eseguito in modalità di rilascio e con l'ottimizzazione R8 abilitata. Nelle build di debug, lo scorrimento del layout Lazy potrebbe apparire più lento. Per ulteriori informazioni, leggi Rendimento della composizione.

Risorse aggiuntive