Wiele aplikacji musi wyświetlać kolekcje elementów. Ten dokument wyjaśnia, jak to zrobić w Jetpack Compose.
Jeśli wiesz, że w Twoim przypadku nie potrzeba przewijania, możesz użyć prostego Column
lub Row
(w zależności od kierunku) i wyświetlać zawartość każdego elementu, iterując listę w ten sposób:
@Composable fun MessageList(messages: List<Message>) { Column { messages.forEach { message -> MessageRow(message) } } }
Możemy zwiększyć zakres przewijania Column
dzięki modyfikatorowi verticalScroll()
.
Więcej informacji znajdziesz w dokumentacji gestów.
Leniwe listy
Jeśli musisz wyświetlać dużą liczbę elementów (lub listę o nieznanej długości), używanie układu takiego jak Column
może powodować problemy z wydajnością, ponieważ wszystkie elementy zostaną utworzone i określone, czy będą widoczne.
Tworzenie obejmuje zestaw komponentów, które tworzą i rozkładają tylko elementy widoczne w widocznym obszarze komponentu. Te komponenty to m.in. LazyColumn
i LazyRow
.
Jak sama nazwa wskazuje, metodą LazyColumn
i LazyRow
jest orientacja, w której rozmieszczają elementy i przewijają. LazyColumn
tworzy listę przewijaną w pionie, a LazyRow
– listę przewijaną w poziomie.
Komponenty Leniwego różnią się od większości układów w obszarze tworzenia wiadomości. Zamiast akceptowania parametru blokowania treści @Composable
, który pozwala aplikacjom bezpośrednio emitować elementy kompozycyjne, komponenty Lazy zawierają blok LazyListScope.()
. Ten blok LazyListScope
udostępnia DSL, który umożliwia aplikacjom opisanie zawartości elementu. Następnie komponent leniwy odpowiada za dodanie treści każdego elementu zgodnie z wymaganiami dotyczącymi układu i pozycji przewijania.
LazyListScope
LDL
DSL interfejsu LazyListScope
udostępnia wiele funkcji do opisywania elementów w układzie. Najprostsza metoda item()
dodaje 1 element, a items(Int)
– wiele:
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") } }
Dostępnych jest też wiele funkcji rozszerzeń, które umożliwiają dodawanie kolekcji elementów, np. List
. Te rozszerzenia pozwalają nam łatwo przenieść opisany wyżej przykład Column
:
/** * import androidx.compose.foundation.lazy.items */ LazyColumn { items(messages) { message -> MessageRow(message) } }
Istnieje też wariant funkcji rozszerzenia items()
o nazwie itemsIndexed()
, która dostarcza indeks. Więcej informacji znajdziesz w dokumentacji LazyListScope
.
Leniwe siatki
Komponenty kompozycyjne LazyVerticalGrid
i LazyHorizontalGrid
umożliwiają wyświetlanie elementów w siatce. Elementy leniwej pionowej siatki wyświetlają się w kontenerze z możliwością przewijania w pionie, rozpiętym między wieloma kolumnami, a leniwe poziome siatki działają tak samo na osi poziomej.
Siatki mają te same zaawansowane funkcje interfejsów API co listy, ale do opisywania treści wykorzystują bardzo podobną technologię DSL – LazyGridScope.()
.
Parametry columns
w LazyVerticalGrid
i rows
w LazyHorizontalGrid
określają sposób tworzenia kolumn i wierszy z komórek. W tym przykładzie elementy są wyświetlane w siatce przy użyciu parametru GridCells.Adaptive
, który pozwala ustawić szerokość każdej kolumny na co najmniej 128.dp
:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 128.dp) ) { items(photos) { photo -> PhotoItem(photo) } }
LazyVerticalGrid
umożliwia określenie szerokości elementów, a siatka będzie pasować do jak największej liczby kolumn. Po obliczeniu liczby kolumn pozostała szerokość jest równomiernie rozkładana między kolumny.
Ten adaptacyjny sposób dostosowywania rozmiaru jest szczególnie przydatny przy wyświetlaniu zestawów produktów na ekranach o różnych rozmiarach.
Jeśli znasz dokładną liczbę kolumn do użycia, możesz zamiast tego udostępnić wystąpienie obiektu GridCells.Fixed
zawierającego liczbę wymaganych kolumn.
Jeśli Twój projekt wymaga, by tylko niektóre elementy miały niestandardowe wymiary,
możesz użyć obsługi siatki, by dodać niestandardowe rozpiętości kolumn dla elementów.
Określ rozpiętość kolumny za pomocą parametru span
metod LazyGridScope DSL
item
i items
.
maxLineSpan
, jedna z wartości zakresu spanu, jest szczególnie przydatna, gdy używasz rozmiarów adaptacyjnych, ponieważ liczba kolumn nie jest stała.
Ten przykład pokazuje, jak podać pełne spany wierszy:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 30.dp) ) { item(span = { // LazyGridItemSpanScope: // maxLineSpan GridItemSpan(maxLineSpan) }) { CategoryCard("Fruits") } // ... }
Leniwa siatka szeregowa
LazyVerticalStaggeredGrid
i LazyHorizontalStaggeredGrid
to funkcje kompozycyjne umożliwiające tworzenie leniwej, ułożonej w kolejności siatki elementów.
Leniwa pionowa siatka szeregowa wyświetla elementy w przewijanym w pionie kontenerze, który rozciąga się na wiele kolumn i pozwala poszczególnym elementom o różnej wysokości. Leniwe poziome siatki działają tak samo na osi poziomej w przypadku elementów o różnej szerokości.
Ten fragment kodu to podstawowy przykład użycia atrybutu LazyVerticalStaggeredGrid
o szerokości 200.dp
przypadającej 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ć parametru StaggeredGridCells.Fixed(columns)
zamiast StaggeredGridCells.Adaptive
.
W ten sposób dostępna szerokość zostanie podzielona przez liczbę kolumn (wierszy w siatce poziomej) i każdy element będzie miał tę szerokość (czyli wysokość w przypadku poziomej siatki):
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() )
Dopełnienie treści
Czasami trzeba dodać dopełnienie na krawędziach treści. Leniwe komponenty umożliwiają przekazanie do parametru contentPadding
części PaddingValues
, aby umożliwić obsługę tego poziomu:
LazyColumn( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), ) { // ... }
W tym przykładzie dodajemy 16.dp
dopełnienia do poziomych krawędzi (lewej i prawej), a następnie 8.dp
na górze i na dole treści.
Pamiętaj, że to dopełnienie jest zastosowane do treści, a nie do samej treści LazyColumn
. W przykładzie powyżej pierwszy element będzie miał dopełnienie 8.dp
na górze, ostatni element na dole, a wszystkie elementy będą mieć dopełnienie 16.dp
z lewej i prawej strony.8.dp
Odstępy między treściami
Aby dodać odstępy między elementami, możesz użyć Arrangement.spacedBy()
.
W podanym niżej przykładzie między poszczególnymi elementami dodano 4.dp
spacji:
LazyColumn( verticalArrangement = Arrangement.spacedBy(4.dp), ) { // ... }
Analogicznie w przypadku LazyRow
:
LazyRow( horizontalArrangement = Arrangement.spacedBy(4.dp), ) { // ... }
Siatki obsługują jednak zarówno układ pionowy, jak i poziomy:
LazyVerticalGrid( columns = GridCells.Fixed(2), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { items(photos) { item -> PhotoItem(item) } }
Klucze elementów
Domyślnie stan każdego elementu jest zależny od pozycji elementu na liście lub siatce. Może to jednak powodować problemy w przypadku zmiany zbioru danych, ponieważ elementy, które zmieniają swoją pozycję, tracą zapamiętany stan. Jeśli wyobrazisz sobie, jak wygląda sytuacja z funkcją LazyRow
w elemencie LazyColumn
, to gdy wiersz zmieni pozycję elementu, użytkownik straci swoją pozycję przewijania w obrębie wiersza.
Aby z nim walczyć, możesz podać stabilny i unikalny klucz do każdego elementu, który będzie blokował parametr key
. Zapewnienie stabilnego klucza sprawia, że stan elementu jest spójny w przypadku 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, ułatwisz tworzenie wiadomości w celu prawidłowej obsługi zmian kolejności. Jeśli na przykład element zawiera zapamiętany stan, ustawienie kluczy pozwoli utworzyć klucz w tym stanie razem z elementem, gdy zmieni się jego pozycja.
LazyColumn { items(books, key = { it.id }) { val rememberedValue = remember { Random.nextInt() } } }
Obowiązuje jednak jedno ograniczenie typów kluczy elementów, których możesz używać.
Typ klucza musi być obsługiwany przez Bundle
, mechanizm Androida do zachowywania stanów podczas odtwarzania aktywności. Bundle
obsługuje takie typy elementów jak podstawowe, wyliczenia i elementy Parcelable.
LazyColumn { items(books, key = { // primitives, enums, Parcelable, etc. }) { // ... } }
Klucz musi być obsługiwany w systemie Bundle
, aby można było przywrócić rememberSaveable
wewnątrz elementu kompozycyjnego, gdy aktywność zostanie odtworzona, a nawet gdy przewiniesz stronę w dół i przewiniesz stronę do tyłu.
LazyColumn { items(books, key = { it.id }) { val rememberedValue = rememberSaveable { Random.nextInt() } } }
Animacje elementów
Jeśli używasz widżetu RecyclerView, wiesz, że automatycznie animuje on zmiany produktów.
Leniwe układy mają tę samą funkcję w przypadku zmiany kolejności elementów.
Interfejs API jest prosty – wystarczy ustawić modyfikator animateItemPlacement
na zawartość elementu:
LazyColumn { items(books, key = { it.id }) { Row(Modifier.animateItemPlacement()) { // ... } } }
Możesz nawet udostępnić niestandardową specyfikację animacji, jeśli chcesz:
LazyColumn { items(books, key = { it.id }) { Row( Modifier.animateItemPlacement( tween(durationMillis = 250) ) ) { // ... } } }
Pamiętaj, by podać klucze do elementów, aby można było znaleźć nową pozycję przenoszonego elementu.
Poza zmianą kolejności pracujemy obecnie nad animacjami dodawania i usuwania elementów. Postępy możesz sprawdzić w numerze 150812265.
Przyklejone nagłówki (funkcja eksperymentalna)
Wzorzec „przyklejony nagłówek” jest przydatny przy wyświetlaniu list pogrupowanych danych. Poniżej znajdziesz przykład „listy kontaktów”, pogrupowanej według inicjałów każdego kontaktu:
Aby uzyskać przyklejony nagłówek za pomocą funkcji LazyColumn
, możesz użyć eksperymentalnej funkcji stickyHeader()
, podając treść nagłówka:
@OptIn(ExperimentalFoundationApi::class) @Composable fun ListWithHeader(items: List<Item>) { LazyColumn { stickyHeader { Header() } items(items) { item -> ItemRow(item) } } }
Aby utworzyć listę z wieloma nagłówkami, jak w powyższym przykładzie z listą kontaktów, 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) } } } }
Reakcja na pozycję przewijania
Wiele aplikacji musi reagować na zmiany pozycji przewijania i układu elementów oraz reagować na nie.
Komponenty Lazy obsługują ten przypadek użycia przez podniesienie obiektu 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ą jedynie informacji o pierwszym widocznym elemencie. W tym przypadku LazyListState
podaje właściwości firstVisibleItemIndex
i firstVisibleItemScrollOffset
.
Jeśli użyjemy przykładu pokazywania i ukrywania przycisku na podstawie tego, czy użytkownik przewinął pierwszy element:
@OptIn(ExperimentalAnimationApi::class) @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() } } }
Odczytywanie stanu bezpośrednio w ramach kompozycji jest przydatne, gdy musisz zaktualizować inne funkcje kompozycyjne interfejsu. Są jednak również sytuacje, w których zdarzenie nie musi być obsługiwane w tej samej kompozycja. Typowym przykładem takiej sytuacji jest wysyłanie przez użytkownika zdarzenia Analytics, gdy użytkownik przewinie widok poza określony punkt. Aby skutecznie to zrobić, 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() } }
Za pomocą właściwości layoutInfo
LazyListState
podaje też informacje o wszystkich aktualnie wyświetlanych elementach oraz o ich granicach na ekranie. Więcej informacji znajdziesz w klasie LazyListLayoutInfo
.
Kontrolowanie pozycji przewijania
Oprócz reagowania na pozycję przewijania aplikacje mogą też decydować o położeniu przewijania.
LazyListState
obsługuje to za pomocą funkcji scrollToItem()
, która „natychmiast” przyciąga pozycję przewijania oraz animateScrollToItem()
, która przewija się przy użyciu animacji (nazywanej 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 (stronicowanie)
Biblioteka stron docelowych umożliwia aplikacjom obsługę dużych list elementów oraz wczytywanie i wyświetlanie ich niewielkich fragmentów. Strona Stron w wersji 3.0 i nowszych umożliwia tworzenie wiadomości za pomocą biblioteki androidx.paging:paging-compose
.
Aby wyświetlić listę treści podzielonych na strony, możemy użyć funkcji rozszerzenia collectAsLazyPagingItems()
, a potem przekazać zwrócone wartości LazyPagingItems
do items()
w LazyColumn
. Podobnie jak w przypadku obsługi stron w widokach, podczas wczytywania danych możesz wyświetlać zmienne, sprawdzając, czy item
to 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 korzystania z leniwych układów
Oto kilka wskazówek, które warto wziąć pod uwagę, aby mieć pewność, że Twoje leniwe układy będą działać zgodnie z oczekiwaniami.
Unikaj używania elementów o rozmiarze 0 pikseli.
Może się to zdarzyć w sytuacjach, gdy np. chcesz asynchronicznie pobierać niektóre dane, takie jak obrazy, aby na późniejszym etapie wypełnić elementy listy. Spowodowałoby to złożenie wszystkich elementów w układzie leniwego w pierwszym pomiarze, ponieważ ich wysokość wynosi 0 pikseli i mogłyby zmieścić się w widocznym obszarze. Po wczytaniu elementów i rozszerzeniu ich wysokości leniwe układy odrzucają wtedy wszystkie pozostałe elementy, które niepotrzebnie zostały utworzone, ponieważ w rzeczywistości nie mieszczą się w widocznym obszarze. Aby tego uniknąć, ustaw domyślne rozmiary elementów, tak by układ leniwy mógł właściwie obliczyć, ile z nich może się zmieścić 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 wczytaniu danych asynchronicznie, zadbaj o to, by rozmiar tych elementów nie zmieniał się przed załadowaniem i po jego wczytaniu, na przykład dodając kilka obiektów zastępczych. 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 zagnieżdżania elementów podrzędnych z możliwością przewijania bez wstępnie zdefiniowanego rozmiaru wewnątrz innego elementu nadrzędnego z możliwością przewijania. Przykład: próba zagnieżdżenia podrzędnego elementu LazyColumn
bez stałej wysokości w elemencie nadrzędnym Column
z możliwością przewijania w pionie:
// throws IllegalStateException Column( modifier = Modifier.verticalScroll(state) ) { LazyColumn { // ... } }
Ten sam efekt można osiągnąć przez umieszczenie wszystkich funkcji kompozycyjnych wewnątrz jednego elementu nadrzędnego LazyColumn
i użycie DSL do przekazywania różnych typów treści. Umożliwia to wysyłanie zarówno pojedynczych elementów, jak i wielu elementów listy w jednym miejscu:
LazyColumn { item { Header() } items(data) { item -> PhotoItem(item) } item { Footer() } }
Pamiętaj, że gdy zagnieżdżasz różne układy kierunków, na przykład element nadrzędny Row
z możliwością przewijania i element podrzędny LazyColumn
, są dozwolone:
Row( modifier = Modifier.horizontalScroll(scrollState) ) { LazyColumn { // ... } }
Także w przypadkach, w których 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 umieszczenie 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) } // ... }
Leniwe układy poradziły sobie z tym zgodnie z oczekiwaniami – rozmieszczają elementy jeden po drugim, jakby były to różne elementy. Istnieją jednak pewne problemy.
Gdy w ramach 1 elementu jest wysyłanych wiele elementów, są one traktowane jako jeden element, co oznacza, że nie można już tworzyć ich oddzielnie. Jeśli 1 element staje się widoczny na ekranie, należy utworzyć i zmierzyć wszystkie odpowiadające mu elementy. Nadmierne użycie
może negatywnie wpłynąć na wydajność. W skrajnym przypadku umieszczenie wszystkich elementów w jednym elemencie
całkowicie nie pozwala na użycie leniwego układu. Oprócz potencjalnych problemów z wydajnością umieszczenie większej liczby elementów w jednym elemencie wpłynie też na działanie scrollToItem()
i animateScrollToItem()
.
Istnieją jednak sposoby umieszczania wielu elementów w jednym elemencie, np. separatory w liście. Nie chcemy, by separatory zmieniały indeksy przewijania, ponieważ nie powinny one być uznawane za elementy niezależne. Nie wpłynie to też na skuteczność, bo separatory są małe. Separator zwykle musi być widoczny w momencie, gdy element jest widoczny, tak aby mógł być częścią poprzedniego elementu:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Divider() } item { Item(2) } // ... }
Zastanów się nad dostosowaniem układu
Zazwyczaj leniwe listy zawierają wiele elementów i zajmują więcej miejsca niż rozmiar przewijanego kontenera. Jeśli jednak lista zawiera niewielką liczbę elementów, Twój projekt może mieć bardziej szczegółowe wymagania dotyczące ich umiejscowienia w widocznym obszarze.
Aby to osiągnąć, możesz użyć niestandardowej branży Arrangement
i przekazać ją do LazyColumn
. W poniższym przykładzie obiekt TopWithFooter
musi zaimplementować tylko metodę arrange
. Po pierwsze, elementy
umieszcza się jeden po drugim. Po drugie, jeśli łączna używana wysokość jest mniejsza niż wysokość widocznego obszaru, 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() } } }
Rozważ dodanie contentType
Począwszy od funkcji Utwórz 1.2, aby zmaksymalizować wydajność układu Leniwego, możesz zacząć dodawać do list lub siatek parametr contentType
. Dzięki temu możesz określić typ treści dla każdego elementu układu, gdy tworzysz listę lub siatkę złożoną z wielu różnych typów elementów:
LazyColumn { items(elements, contentType = { it.type }) { // ... } }
Gdy podasz contentType
, funkcja tworzenia treści będzie mogła ponownie wykorzystywać kompozycje tylko między elementami tego samego typu. Ponowne wykorzystanie jest wydajniejsze, gdy tworzysz elementy o podobnej strukturze, dlatego podanie typów treści pozwoli uniknąć próby utworzenia elementu typu A na podstawie zupełnie innego elementu typu B. Pomaga to w maksymalnym wykorzystaniu
ponownego użycia kompozycji i skuteczności leniwego układu.
Pomiar skuteczności
Możesz miarodajnie mierzyć wydajność leniwego układu tylko wtedy, gdy działa on w trybie wydania i ma włączoną optymalizację R8. W kompilacjach do debugowania przewijanie przez leniwy układ może działać wolniej. Więcej informacji na ten temat znajdziesz w artykule Wydajność tworzenia wiadomości.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy JavaScript jest wyłączony
- Przenieś grupę
RecyclerView
na listę leniwych - Zapisywanie stanu interfejsu użytkownika w momencie tworzenia
- Kotlin dla Jetpack Compose