Wiele aplikacji musi wyświetlać kolekcje elementów. Z tego dokumentu dowiesz się, jak sprawnie wykonywać te czynności w Jetpack Compose.
Jeśli wiesz, że Twój przypadek użycia nie wymaga przewijania, możesz użyć prostego Column
lub Row
(w zależności od kierunku) i wygenerować zawartość każdego elementu, iterując po liście w ten sposób:
@Composable fun MessageList(messages: List<Message>) { Column { messages.forEach { message -> MessageRow(message) } } }
Możemy umożliwić przewijanie elementu Column
za pomocą modyfikatora verticalScroll()
.
Lazy lists
Jeśli musisz wyświetlić dużą liczbę elementów (lub listę o nieznanej długości), używanie układu takiego jak Column
może spowodować problemy z wydajnością, ponieważ wszystkie elementy zostaną skomponowane i ułożone, niezależnie od tego, czy są widoczne.
Compose udostępnia zestaw komponentów, które tylko tworzą i umieszcza elementy, które są widoczne w widocznym obszarze komponentu. Są to między innymi LazyColumn
i LazyRow
.
Jak sugeruje nazwa, różnica między wersjami LazyColumn
i LazyRow
polega na orientacji, w jakiej są one wyświetlane i przewijane. LazyColumn
tworzy listę przewijaną w pionie, a LazyRow
– listę przewijaną w poziomie.
Komponenty Leniwe różnią się od większości układów w funkcji tworzenia wiadomości. Zamiast przyjmować parametr bloku treści @Composable
, co pozwala aplikacjom bezpośrednio emitować elementy składane, komponenty leniwie wczytywane udostępniają blok LazyListScope.()
. Ten blok LazyListScope
udostępnia DSL, który pozwala aplikacjom opisywać zawartość elementu. Komponent leniwy odpowiada za dodawanie treści każdego elementu zgodnie z wymaganiami układu i pozycją przewijania.
LazyListScope
DSL
DSL w LazyListScope
udostępnia kilka funkcji do opisywania elementów w układzie. Najprostsza forma:
item()
dodaje pojedynczy element, a
items(Int)
dodaje wiele elementów:
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") } }
Istnieje też kilka funkcji rozszerzeń, które umożliwiają dodawanie kolekcji elementów, np. List
. Dzięki tym rozszerzeniom możemy łatwo przenieść nasz przykład Column
z powyżej:
/** * import androidx.compose.foundation.lazy.items */ LazyColumn { items(messages) { message -> MessageRow(message) } }
Istnieje też wariant funkcji rozszerzenia items()
o nazwie itemsIndexed()
, która zwraca indeks. Więcej informacji znajdziesz w dokumentacji LazyListScope
.
Leniwe siatki
Komponenty LazyVerticalGrid
i LazyHorizontalGrid
umożliwiają wyświetlanie elementów w siatce. Leniwa siatka pionowa wyświetla elementy w kontenerze z możliwością przewijania w pionie, rozłożonym na wiele kolumn. Leniwe poziome siatki będą wyglądać tak samo na osi poziomej.
Sieci mają te same zaawansowane funkcje interfejsu API co listy i także używają bardzo podobnego języka DSL (LazyGridScope.()
) do opisywania treści.
Parametr columns
w LazyVerticalGrid
i parametr rows
w LazyHorizontalGrid
określają, jak komórki są formowane w kolumny lub wiersze. Poniższy przykład pokazuje elementy w siatce z użyciem parametru GridCells.Adaptive
, aby ustawić szerokość każdej kolumny na co najmniej 128.dp
:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 128.dp) ) { items(photos) { photo -> PhotoItem(photo) } }
LazyVerticalGrid
pozwala określić szerokość elementów, a siatka dopasuje do nich jak najwięcej kolumn. Po obliczeniu liczby kolumn pozostała szerokość jest rozdzielana równomiernie między kolumny.
Ten adaptacyjny sposób dostosowywania rozmiaru jest szczególnie przydatny w przypadku wyświetlania zestawów elementów na różnych rozmiarach ekranu.
Jeśli znasz dokładną liczbę kolumn, które mają być użyte, możesz zamiast tego podać instancję parametru GridCells.Fixed
zawierającego liczbę wymaganych kolumn.
Jeśli Twój projekt wymaga niestandardowych wymiarów tylko niektórych elementów, możesz użyć obsługi siatki, aby ustawić niestandardowe zakresy kolumn dla tych elementów.
Określ zakres kolumny za pomocą parametru span
metod LazyGridScope DSL
item
i items
.
maxLineSpan
,
jedna z wartości zakresu zasięgu, jest szczególnie przydatna, gdy używasz dostosowania rozmiaru, ponieważ liczba kolumn nie jest stała.
Ten przykład pokazuje, jak podać pełny zakres wiersza:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 30.dp) ) { item(span = { // LazyGridItemSpanScope: // maxLineSpan GridItemSpan(maxLineSpan) }) { CategoryCard("Fruits") } // ... }
Leniwa rozłożona siatka
LazyVerticalStaggeredGrid
i
LazyHorizontalStaggeredGrid
to komponenty, które umożliwiają tworzenie wczytywanych z opóźnieniem siatek elementów z przesunięciem.
W przypadku opóźnionego pionowego siatki o zmiennej wielkości elementy są wyświetlane w przesuwanym pionowo kontenerze, który obejmuje kilka kolumn i umożliwia wyświetlanie poszczególnych elementów o różnej wysokości. Lazy horizontal grids zachowują się tak samo na osi poziomej w przypadku elementów o różnej szerokości.
Ten fragment kodu to podstawowy przykład użycia obiektu LazyVerticalStaggeredGrid
z szerokością 200.dp
na element:
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() )
Aby ustawić stałą liczbę kolumn, możesz użyć właściwości StaggeredGridCells.Fixed(columns)
zamiast StaggeredGridCells.Adaptive
.
Dostępna szerokość jest dzielona przez liczbę kolumn (lub wierszy w przypadku siatki poziomej), a każdy element zajmuje tę szerokość (lub wysokość w przypadku siatki poziomej):
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() )
wypełnienie treści;
Czasami trzeba dodać wypełnienie wokół krawędzi treści. Komponenty z opóźnionym wczytywaniem umożliwiają przekazywanie niektórych parametrów PaddingValues
do parametru contentPadding
, aby obsługiwać te funkcje:
LazyColumn( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), ) { // ... }
W tym przykładzie dodajemy 16.dp
wypełnienie do krawędzi poziomych (po lewej i po prawej), a potem 8.dp
do górnej i dolnej krawędzi treści.
Pamiętaj, że to wypełnienie jest stosowane do treści, a nie do samego LazyColumn
. W przykładzie powyżej pierwszy element doda 8.dp
do górnej krawędzi, ostatni element doda 8.dp
do dolnej krawędzi, a wszystkie elementy będą miały 16.dp
do lewej i prawej krawędzi.
Odstępy między treściami
Aby dodać odstępy między elementami, możesz użyć funkcji Arrangement.spacedBy()
.
W przykładzie poniżej między każdym elementem jest dodawane 4.dp
miejsca:
LazyColumn( verticalArrangement = Arrangement.spacedBy(4.dp), ) { // ... }
Podobnie w przypadku LazyRow
:
LazyRow( horizontalArrangement = Arrangement.spacedBy(4.dp), ) { // ... }
W siatkach akceptowane są zarówno rozmieszczenie pionowe, jak i poziome:
LazyVerticalGrid( columns = GridCells.Fixed(2), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { items(photos) { item -> PhotoItem(item) } }
Klucze produktów
Domyślnie stan każdego elementu jest powiązany z jego pozycją na liście lub siatce. Może to jednak spowodować problemy, jeśli zestaw danych ulegnie zmianie, ponieważ elementy, które zmieniają pozycję, tracą zapamiętany stan. Jeśli wyobrazimy sobie scenariusz LazyRow
w ramach LazyColumn
, to jeśli wiersz zmieni pozycję elementu, użytkownik straci pozycję przewijania w wierszu.
Aby temu zapobiec, możesz podać stabilny i niepowtarzalny klucz dla każdego elementu, podając blok dla parametru key
. Podanie stabilnego klucza umożliwia zachowanie spójności stanu produktu podczas zmian w zbiorze danych:
LazyColumn { items( items = messages, key = { message -> // Return a stable + unique key for the item message.id } ) { message -> MessageRow(message) } }
Podając klucze, pomagasz usłudze Compose prawidłowo obsługiwać zmiany kolejności. Jeśli na przykład Twój element zawiera zapamiętany stan, ustawienia kluczy pozwolą komponentowi Compose przenosić ten stan wraz z elementem, gdy zmienia się jego pozycja.
LazyColumn { items(books, key = { it.id }) { val rememberedValue = remember { Random.nextInt() } } }
Istnieje jednak jedno ograniczenie dotyczące typów, które można używać jako kluczy elementów.
Typ klucza musi być obsługiwany przez Bundle
, czyli mechanizm Androida do przechowywania stanów podczas ponownego tworzenia aktywności. Bundle
obsługuje typy takie jak prymitywne, enums czy Parcelables.
LazyColumn { items(books, key = { // primitives, enums, Parcelable, etc. }) { // ... } }
Klucz musi być obsługiwany przez Bundle
, aby rememberSaveable
w komponowalnym elemencie można było przywrócić podczas ponownego tworzenia aktywności lub nawet po przewinięciu z tego elementu i powrocie do niego.
LazyColumn { items(books, key = { it.id }) { val rememberedValue = rememberSaveable { Random.nextInt() } } }
Animacje elementów
Jeśli używasz widżetu RecyclerView, wiesz, że animuje on automatycznie zmiany elementów.
Skróty zapewniają taką samą funkcjonalność w przypadku zmiany kolejności elementów.
Interfejs API jest prosty – wystarczy ustawić modyfikator animateItem
w treści elementu:
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()) { // ... } } }
W razie potrzeby możesz nawet podać niestandardową specyfikację animacji:
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) ) ) { // ... } } }
Pamiętaj, aby podać klucze elementów, dzięki czemu będzie można znaleźć nową pozycję przeniesionego elementu.
Przyklejone nagłówki (funkcja eksperymentalna)
Wzór „przyklejony nagłówek” jest przydatny podczas wyświetlania listy pogrupowanych danych. Poniżej znajdziesz przykładową „listę kontaktów” pogrupowaną według inicjału każdego kontaktu:
Aby uzyskać nagłówek przypinany za pomocą LazyColumn
, możesz użyć eksperymentalnej funkcji stickyHeader()
, podając zawartość nagłówka:
@OptIn(ExperimentalFoundationApi::class) @Composable fun ListWithHeader(items: List<Item>) { LazyColumn { stickyHeader { Header() } items(items) { item -> ItemRow(item) } } }
Aby uzyskać listę z wieloma nagłówkami, tak jak w przykładzie „lista kontaktów” powyżej, możesz:
// 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) } } } }
Reagowanie na pozycję przewijania
Wiele aplikacji musi reagować i słuchać zmian pozycji przewijania i układów elementów.
Komponenty Lazy obsługują ten przypadek użycia, podnosząc LazyListState
:
@Composable fun MessageList(messages: List<Message>) { // Remember our own LazyListState val listState = rememberLazyListState() // Provide it to LazyColumn LazyColumn(state = listState) { // ... } }
W prostych przypadkach aplikacje zwykle potrzebują informacji tylko o pierwszym widocznym elemencie. W tym celu usługa LazyListState
udostępnia właściwości firstVisibleItemIndex
i firstVisibleItemScrollOffset
.
Jeśli weźmiemy pod uwagę przykład pokazywania i ukrywania przycisku w zależności od tego, czy użytkownik przewinął pierwszy element:
@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() } } }
Czytanie stanu bezpośrednio w kompozycji jest przydatne, gdy trzeba zaktualizować inne komponenty interfejsu użytkownika, ale są też scenariusze, w których zdarzenie nie musi być obsługiwane w tej samej kompozycji. Typowym przykładem jest wysyłanie zdarzenia Analytics, gdy użytkownik przewinie stronę do określonego miejsca. Aby sprawnie zarządzać tymi informacjami, możemy użyć:
snapshotFlow()
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .map { index -> index > 0 } .distinctUntilChanged() .filter { it } .collect { MyAnalyticsService.sendScrolledPastFirstItemEvent() } }
LazyListState
udostępnia też informacje o wszystkich elementach, które są obecnie wyświetlane, oraz ich granicach na ekranie za pomocą właściwości layoutInfo
. Więcej informacji znajdziesz w klasie LazyListLayoutInfo
.
Kontrolowanie pozycji przewijania
Oprócz reagowania na pozycję przewijania aplikacje mogą też kontrolować tę pozycję.
LazyListState
obsługuje to za pomocą funkcji scrollToItem()
, która „natychmiast” ustawia pozycję przewijania, oraz animateScrollToItem()
, która przewija za pomocą animacji (zwanej też płynnym przewijaniem):
@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) } } ) }
Duże zbiory danych (przewijanie stron)
Biblioteka stron umożliwia aplikacjom obsługę dużych list elementów, wczytując i wyświetlając małe fragmenty listy w miarę potrzeby. Paging 3.0 i nowsze wersje obsługują Compose za pomocą biblioteki androidx.paging:paging-compose
.
Aby wyświetlić listę treści z podziałem na strony, możemy użyć funkcji rozszerzenia collectAsLazyPagingItems()
, a następnie przekazać zwrócone dane LazyPagingItems
do items()
w LazyColumn
. Podobnie jak w przypadku obsługi podziału na strony w widokach możesz wyświetlać wskaźniki zastępcze podczas wczytywania danych, sprawdzając, czy item
ma wartość 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() } } } }
Wskazówki dotyczące używania leniwego układu
Oto kilka wskazówek, które pomogą Ci zadbać o prawidłowe działanie układów z lazy load.
Unikaj elementów o wymiarach 0 pikseli
Może się to zdarzyć, gdy na przykład oczekujesz asynchronicznego pobierania niektórych danych, np. obrazów, aby uzupełnić elementy listy na późniejszym etapie. Spowoduje to, że układ Lazy utworzy wszystkie elementy w pierwszym pomiarze, ponieważ ich wysokość wynosi 0 pikseli i wszystkie zmieszczą się w widocznym obszarze. Gdy elementy zostaną załadowane, a ich wysokość zostanie rozszerzona, układy oparte na metodzie Lazy Discard odrzuciłyby wszystkie inne elementy, które zostały niepotrzebnie utworzone podczas pierwszego wczytywania, ponieważ nie mieszczą się one w widocznym obszarze. Aby tego uniknąć, ustaw domyślne rozmiary elementów, aby układ Lazy mógł prawidłowo obliczyć, ile elementów mieści się w widocznym obszarze:
@Composable fun Item(imageUrl: String) { AsyncImage( model = rememberAsyncImagePainter(model = imageUrl), modifier = Modifier.size(30.dp), contentDescription = null // ... ) }
Jeśli znasz przybliżony rozmiar elementów po asynchronicznym wczytywaniu danych, warto zadbać o to, by rozmiar elementów pozostawał taki sam przed wczytaniem i po nim. Możesz na przykład dodać obiekty zastępcze. Pomoże to utrzymać prawidłową pozycję przewijania.
Unikaj zagnieżdżania komponentów, które można przewijać w tym samym kierunku
Dotyczy to tylko przypadków zagnieżdżania elementów podrzędnych z możliwością przewijania bez zdefiniowanego rozmiaru wewnątrz innego elementu nadrzędnego z możliwością przewijania w tym samym kierunku. Na przykład próba umieszczenia elementu podrzędnego LazyColumn
bez stałej wysokości wewnątrz elementu nadrzędnego Column
, który można przewijać w kierunku pionowym:
// throws IllegalStateException Column( modifier = Modifier.verticalScroll(state) ) { LazyColumn { // ... } }
Taki sam efekt można uzyskać, opakowując wszystkie komponenty w jednym elemencie nadrzędnym LazyColumn
i używając jego interfejsu DSL do przekazywania różnych typów treści. Umożliwia to przesyłanie pojedynczych elementów oraz wielu elementów listy w jednym miejscu:
LazyColumn { item { Header() } items(data) { item -> PhotoItem(item) } item { Footer() } }
Pamiętaj, że jeśli zagnieżdżasz różne układy kierunku, np. przewijany element nadrzędny Row
i element podrzędny LazyColumn
, są dozwolone:
Row( modifier = Modifier.horizontalScroll(scrollState) ) { LazyColumn { // ... } }
Dodatkowo w sytuacjach, gdy nadal używasz tych samych układów kierunku, ale ustawiasz stały rozmiar dla zagnieżdżonych elementów podrzędnych:
Column( modifier = Modifier.verticalScroll(scrollState) ) { LazyColumn( modifier = Modifier.height(200.dp) ) { // ... } }
Uważaj na umieszczanie wielu elementów w jednym elemencie
W tym przykładzie drugi element lambda emituje 2 elementy w jednym bloku:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Item(2) } item { Item(3) } // ... }
Lazy layouty będą działać zgodnie z oczekiwaniami – rozmieszczą elementy jeden po drugim, tak jakby były to różne elementy. Występują jednak pewne problemy.
Gdy w jednym elemencie pojawia się wiele elementów, są one traktowane jako jeden element, co oznacza, że nie mogą się już składać z osobna. Jeśli jeden element jest widoczny na ekranie, wszystkie elementy odpowiadające temu elementowi muszą zostać zdefiniowane i zmierzone. Jeśli używasz ich zbyt często, może to obniżyć wydajność. W skrajnym przypadku umieszczenia wszystkich elementów w jednym elemencie całkowicie zatracony zostaje sens korzystania z nieaktywnych układów. Umieszczenie większej liczby elementów w jednym elemencie może wpłynąć nie tylko na potencjalne problemy z wydajnością, ale też na działanie usług scrollToItem()
i animateScrollToItem()
.
Istnieją jednak przypadki, w których umieszczanie wielu elementów w jednym elemencie ma sens, np. w przypadku rozdzielaczy w liście. Nie chcesz, aby rozdzielacze zmieniały indeksy przewijania, ponieważ nie powinny być uważane za elementy niezależne. Nie będzie to również miało wpływu na wydajność, ponieważ przegrody są małe. Podział będzie widoczny, gdy widoczny będzie poprzedni element, dzięki czemu będzie można go umieścić w ramach poprzedniego elementu:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Divider() } item { Item(2) } // ... }
Rozważ użycie niestandardowych układów
Zwykle listy typu Lazy mają wiele elementów i zajmują więcej miejsca niż element przewijania. Jeśli jednak lista zawiera niewiele elementów, Twój projekt może mieć bardziej szczegółowe wymagania dotyczące ich rozmieszczenia w widoku.
Aby to zrobić, możesz użyć niestandardowej branży Arrangement
i przekazać ją do funkcji LazyColumn
. W tym przykładzie obiekt TopWithFooter
musi tylko zaimplementować metodę arrange
. Po pierwsze, umieści elementy jeden po drugim. Po drugie, jeśli łączna wysokość użyta jest niższa niż wysokość widoku, stopka zostanie umieszczona na dole:
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() } } }
Zastanów się nad dodaniem tych elementów: contentType
Jeśli chcesz maksymalnie zwiększyć wydajność układu Lazy, zacznij od Compose 1.2, rozważ dodanie contentType
do list lub siatek. Dzięki temu możesz określić typ treści dla każdego elementu układu, gdy tworzysz listę lub siatkę składającą się z różnych typów elementów:
LazyColumn { items(elements, contentType = { it.type }) { // ... } }
Gdy podasz parametr contentType
, Compose będzie mógł ponownie używać kompozycji tylko między elementami tego samego typu. Ponieważ ponowne używanie jest bardziej efektywne, gdy komponujesz elementy o podobnej strukturze, podanie typów treści zapewnia, że usługa Compose nie będzie próbować tworzyć elementu typu A na zupełnie innym elemencie typu B. Pomaga to zmaksymalizować korzyści płynące z ponownego używania kompozycji i skuteczności układu opartego na opóźnionym ładowaniu.
Pomiar skuteczności
Wydajność układu leniwego można wiarygodnie mierzyć tylko wtedy, gdy działa on w trybie zwalniania i ma włączoną optymalizację R8. W przypadku wersji debugujących przewijanie może być wolniejsze. Więcej informacji na ten temat znajdziesz w artykule Skuteczność funkcji tworzenia wiadomości.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy obsługa JavaScript jest wyłączona
- Migracja
RecyclerView
na listę Lazy - Zapisywanie stanu interfejsu w narzędziu Compose
- Kotlin w Jetpack Compose