Elenchi e griglie pigri

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

Se sai che il tuo caso d'uso non richiede lo scorrimento, puoi utilizzare un semplice Column o Row (a seconda della direzione) ed emettere i contenuti di ogni elemento eseguendo l'iterazione 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 vengono disposti gli elementi e lo scorrimento. 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 del blocco di contenuti @Composable, consentendo alle app di emettere direttamente elementi componibili, 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, come richiesto dal layout e dalla 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 del nostro 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 di LazyListScope.

Griglie lazy

I componibili LazyVerticalGrid e LazyHorizontalGrid forniscono il supporto per la visualizzazione degli elementi in una griglia. Una griglia verticale lazy visualizzerà i suoi elementi in un contenitore scorrevole verticalmente, esteso su più colonne, mentre le griglie orizzontali lazy 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 abbia una larghezza di 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 si adatterà al maggior numero possibile di colonne. La larghezza rimanente viene distribuita equamente tra le colonne, dopo aver calcolato il numero di colonne. Questo modo adattivo di dimensionare è 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 determinati elementi abbiano dimensioni non standard, puoi utilizzare il supporto della griglia per fornire estensioni di colonna personalizzate 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 estensione, è particolarmente utile quando utilizzi il dimensionamento adattivo, perché il numero di colonne non è fisso. Questo esempio mostra come fornire un'estensione di riga completa:

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

Griglia sfalsata lazy

LazyVerticalStaggeredGrid e LazyHorizontalStaggeredGrid sono componibili che ti consentono di creare una griglia di elementi con caricamento lento e sfalsata. Una griglia verticale con layout sfalsato con caricamento lento 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 larghezze diverse.

Il seguente snippet è 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 lazy

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 di immagini con caricamento lento e sfalsata in Compose
Figura 2. Esempio di griglia verticale con caricamento lento e colonne fisse

Spaziatura interna dei contenuti

A volte è necessario aggiungere spaziatura interna intorno ai bordi dei contenuti. I componenti lazy ti 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 interna ai bordi orizzontali (sinistro e destro) e poi 8.dp alla parte superiore e inferiore dei contenuti.

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

Un altro esempio è il passaggio di PaddingValues di Scaffold in contentPadding di LazyColumn. Consulta la guida edge-to-edge.

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 disposizioni sia verticali che orizzontali:

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

Chiavi degli elementi

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 cambia la posizione dell'elemento, l'utente perde la posizione di scorrimento all'interno della riga.

Per evitare questo problema, puoi fornire una chiave univoca e stabile per ogni elemento, fornendo un blocco al parametro key. Fornire una chiave stabile consente di mantenere lo stato degli elementi 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 riordinamenti. Ad esempio, se l'elemento contiene uno 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, enum 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 via 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 automaticamente le modifiche agli elementi. I layout lazy offrono la stessa funzionalità per il riordinamento degli elementi. L'API è semplice: devi solo impostare il modificatore animateItem sui contenuti dell'elemento:

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

Esempio: anima gli elementi negli elenchi lazy

Con Compose, puoi animare le modifiche agli elementi negli elenchi con caricamento lento. Se utilizzati insieme, i seguenti snippet implementano le animazioni durante l'aggiunta, la rimozione e il riordinamento degli elementi di un elenco lazy.

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 sul 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 animazioni predefinite alle aggiunte, alle rimozioni e agli spostamenti degli elementi.

Il seguente snippet presenta una schermata che incorpora 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 sul codice

  • ListAnimatedItemsExample presenta una schermata che incorpora i controlli per aggiungere, rimuovere e ordinare gli elementi.
    • onAddItem e onRemoveItem sono espressioni lambda che vengono passate a AddRemoveButtons per aggiungere e rimuovere elementi dall'elenco.
    • resetOrder, onSortAlphabetically e onSortByLength sono espressioni lambda che vengono passate a OrderButtons per modificare l'ordine degli elementi nell'elenco.
  • AddRemoveButtons mostra 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 in base alla 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 una UI 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 sul 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 impostati su 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 Lunghezza):

@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 sul codice

  • OrderButtons mostra un SingleChoiceSegmentedButtonRow per consentire agli utenti di selezionare un metodo di ordinamento nell'elenco o di 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

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

Video di uno smartphone che scorre verso l'alto e verso il basso un elenco contatti

Per ottenere un'intestazione fissa con LazyColumn, puoi utilizzare stickyHeader()

fornendo i contenuti dell'intestazione:

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

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

Per ottenere un elenco con più intestazioni, come nell'esempio "elenco contatti" riportato sopra, puoi procedere come segue:

// This ideally would be done in the ViewModel
val grouped = contacts.groupBy { it.firstName[0] }

@Composable
fun ContactsList(grouped: Map<Char, List<Contact>>) {
    LazyColumn {
        grouped.forEach { (initial, contactsForInitial) ->
            stickyHeader {
                CharacterHeader(initial)
            }

            items(contactsForInitial) { contact ->
                ContactListItem(contact)
            }
        }
    }
}

Reazione 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 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, in genere le app devono conoscere solo le informazioni relative al primo elemento visibile. A questo scopo, LazyListState fornisce le proprietà firstVisibleItemIndex e firstVisibleItemScrollOffset.

Se utilizziamo l'esempio di visualizzazione e nascondimento di un pulsante 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 componibili 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 una volta che l'utente ha fatto scorrere la pagina oltre un certo 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 la classe LazyListLayoutInfo.

Controllo della posizione di scorrimento

Oltre a reagire alla posizione di scorrimento, è utile anche che le app siano in grado di controllare la posizione di scorrimento. LazyListState supporta questa funzionalità tramite la funzione scrollToItem(), che aggancia "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 (impaginazione)

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

Per visualizzare un elenco di contenuti impaginati, possiamo utilizzare la funzione di estensione collectAsLazyPagingItems() e poi passare l'oggetto LazyPagingItems restituito 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

Ecco 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 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 adattarli tutti alla finestra visibile. Una volta caricati gli elementi e ampliata la loro altezza, i layout Lazy scartano tutti gli altri elementi che sono stati composti inutilmente la prima volta, in quanto non possono effettivamente adattarsi alla viewport. Per evitare questo problema, devi impostare le dimensioni predefinite per gli elementi, in modo che il layout Lazy possa calcolare correttamente 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
        // ...
    )
}

Se conosci le dimensioni approssimative degli elementi dopo il caricamento asincrono dei dati, una buona prassi è assicurarti 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

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, se provi a nidificare un elemento secondario LazyColumn senza un'altezza fissa all'interno di un elemento principale Column con scorrimento verticale:

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

Invece, lo stesso risultato può essere ottenuto aggregando tutti i composable all'interno di un unico LazyColumn principale e utilizzando il relativo DSL per passare diversi tipi di contenuti. In questo modo è possibile emettere singoli elementi, nonché più elementi di un 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 con direzioni diverse, ad esempio un elemento principale scorrevole Row e un elemento secondario LazyColumn:

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

Inoltre, 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 unico elemento

In questo esempio, la seconda funzione lambda degli elementi genera due 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 questa situazione come previsto: disporranno gli elementi uno dopo l'altro come se fossero elementi diversi. Tuttavia, questa operazione presenta un paio di problemi.

Quando vengono emessi più elementi come parte 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 all'elemento devono essere composti e misurati. Se utilizzato in modo eccessivo, questo può influire negativamente sulle prestazioni. Nel caso estremo di inserire tutti gli elementi in un unico elemento, lo scopo di utilizzare i layout Lazy viene completamente vanificato. Oltre ai 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, come l'inserimento di 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 vengono influenzate perché i divisori sono piccoli. È probabile che un divisore debba essere visibile quando l'elemento precedente è visibile, quindi può 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 disposizioni personalizzate

In genere, gli elenchi Lazy contengono molti elementi e occupano più spazio rispetto alle dimensioni del contenitore di scorrimento. Tuttavia, se l'elenco contiene pochi elementi, il tuo design potrebbe avere requisiti più specifici per il loro posizionamento nell'area visibile.

Per farlo, puoi utilizzare la verticale personalizzata Arrangement e passarla 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 dell'area 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 le prestazioni del layout Lazy, valuta la possibilità di aggiungere contentType alle tue liste 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 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 con una struttura simile, fornire i tipi di contenuti garantisce che Compose non tenti di 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 le prestazioni del layout Lazy.

Misurazione delle prestazioni

Puoi misurare in modo affidabile le prestazioni di un layout Lazy solo quando l'esecuzione avviene 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 Prestazioni di Compose.

Risorse aggiuntive