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.
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 determinazione delle dimensioni adattabili è 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() )
Per impostare un numero fisso di colonne, puoi utilizzareStaggeredGridCells.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() )
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 riavvolgimento degli articoli.
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:
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 visualizzando 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 aumentata la loro altezza, i layout lazy eliminano 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 dimensioni predefinite 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 prestazioni, 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 programma 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.
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Eseguire la migrazione di
RecyclerView
in un elenco lazy - Salvare lo stato dell'interfaccia utente in Scrivi
- Kotlin per Jetpack Compose