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,
użyj prostego Column
lub Row
(w zależności od kierunku) i emituj treść każdego elementu przez
powtarzanie listy w taki 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()
.
Leniwe listy
Jeśli chcesz wyświetlić dużą liczbę elementów (lub listę o nieznanej długości),
użycie układu takiego jak Column
może powodować problemy z wydajnością, ponieważ wszystkie elementy są skomponowane i rozmieszczone niezależnie od tego, czy są widoczne.
Narzędzie Utwórz obejmuje zestaw komponentów, które tylko komponują i rozmieszczają elementy,
są widoczne w widocznym obszarze komponentu. Komponenty te obejmują:
LazyColumn
oraz
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
tworzy poziomy
listę przewijaną.
Komponenty leniwie wczytywane różnią się od większości układów w Compose. Zamiast
parametr blokady treści @Composable
, co pozwala aplikacjom bezpośrednio
emitują elementy kompozycyjne, komponenty Lazy oferują blok LazyListScope.()
. Ten blok LazyListScope
udostępnia DSL, który umożliwia aplikacjom opisywanie zawartości 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. Ogólnie rzecz biorąc,
item()
doda jeden element,
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") } }
Dostępnych jest też wiele funkcji rozszerzeń, które umożliwiają dodawanie
kolekcji elementów, takich jak List
. Dzięki tym rozszerzeniom
przeprowadź migrację naszego przykładu Column
opisanego 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. Pionowa siatka Lazy wyświetla elementy w kontenerze, który można przewijać w pionie, obejmując wiele kolumn, a pozioma siatka Lazy zachowuje się 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 rows
parametr w
LazyHorizontalGrid
.
kontrolować sposób tworzenia kolumn lub wierszy z komórek. Poniżej
przykład wyświetla elementy w postaci siatki przy użyciu
GridCells.Adaptive
aby ustawić dla każdej kolumny szerokość 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. Pozostała szerokość jest równomiernie rozłożona
między kolumnami po obliczeniu liczby kolumn.
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 do użycia, możesz zamiast tego podać
wystąpienie
GridCells.Fixed
zawierający liczbę wymaganych kolumn.
Jeśli przy projektowaniu tylko niektóre elementy mają niestandardowe wymiary,
możesz użyć siatki, by podać niestandardowe rozpiętości kolumn dla elementów.
Określ zakres kolumny za pomocą parametru span
metody 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 rozpiętość wierszy:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 30.dp) ) { item(span = { // LazyGridItemSpanScope: // maxLineSpan GridItemSpan(maxLineSpan) }) { CategoryCard("Fruits") } // ... }
Leniwa rozłożona siatka
LazyVerticalStaggeredGrid
oraz
LazyHorizontalStaggeredGrid
to elementy kompozycyjne, które umożliwiają tworzenie leniwie ładowanych, rozłożonych w czasie siatki elementów.
W przypadku opóźnionego pionowego siatki o zmiennej szerokoś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. W przypadku leniwych siatek poziomych elementy o różnej szerokości zachowują się tak samo na osi poziomej.
Ten fragment kodu to podstawowy przykład użycia elementu LazyVerticalStaggeredGrid
o 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() )
Do ustawienia stałej liczby kolumn możesz użyć opcji
StaggeredGridCells.Fixed(columns)
zamiast StaggeredGridCells.Adaptive
.
Spowoduje to podzielenie dostępnej szerokości przez liczbę kolumn (lub wierszy dla
poziomą siatkę) i każdy element ma taką szerokość (lub wysokość
siatka pozioma):
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ć wypełnienie wokół krawędzi treści. Lenistwo
umożliwiają przekazywanie części
PaddingValues
do parametru contentPadding
, aby to obsłużyć:
LazyColumn( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), ) { // ... }
W tym przykładzie do poziomych krawędzi (po lewej i po lewej) dodamy dopełnienie (16.dp
)
w prawo), a potem 8.dp
na górę i dół 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
dopełnienie na górze, ostatni element doda 8.dp
na dole, a wszystkie elementy
będzie zawierać dopełnienie 16.dp
po lewej i prawej stronie.
Odstępy między treściami
Aby dodać odstępy między elementami, użyj funkcji
Arrangement.spacedBy()
W przykładzie poniżej umieszczamy 4.dp
odstępu między elementami:
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() } } }
Istnieją jednak ograniczenia dotyczące typów kluczy, których możesz 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 elementy podstawowe,
wyliczenia i klasy Parcelable.
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ą tę samą funkcjonalność w przypadku zmiany kolejności elementów.
Interfejs API jest prosty – wystarczy ustawić parametr
animateItemPlacement
modyfikator 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.
Oprócz zmiany kolejności animacje elementów dotyczące dodawania i usuwania są obecnie w fazie rozwoju. Postępy możesz śledzić w sprawie 150812265.
Przyklejone nagłówki (funkcja eksperymentalna)
Wzorzec „przyklejonego nagłówka” jest przydatny przy wyświetlaniu list zgrupowanych danych. Poniżej znajduje się przykładowa „lista kontaktów” pogrupowana według inicjał:
Aby utworzyć przyklejony nagłówek za pomocą funkcji LazyColumn
, możesz skorzystać z eksperymentu
stickyHeader()
.
, podając zawartość 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 przykładzie powyżej z „listą kontaktów”, możesz zrobić:
// 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ć i słuchać zmian pozycji przewijania i układów elementów.
Komponenty leniwie wczytywane 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 przypadku prostych zastosowań aplikacje zwykle potrzebują informacji tylko o pierwszym widocznym elemencie. Do tego celu
LazyListState
zapewnia
firstVisibleItemIndex
oraz
firstVisibleItemScrollOffset
usług.
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 zdarzają się też sytuacje, w których zdarzenie nie musi być obsługiwane w tej samej kompozycji. Typowym przykładem jest wysyłanie zdarzenia Analytics po przewinięciu przez użytkownika określonego punktu. Aby rozwiązać ten problem
możemy używać
snapshotFlow()
:
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .map { index -> index > 0 } .distinctUntilChanged() .filter { it } .collect { MyAnalyticsService.sendScrolledPastFirstItemEvent() } }
LazyListState
zawiera również informacje o wszystkich elementach, które są obecnie
oraz ich granice na ekranie, za pomocą funkcji
layoutInfo
.
usłudze. 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ą systemu scrollToItem()
która „natychmiast”
pozycja przewijania i animateScrollToItem()
które przewijają się za pomocą 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ługa dużych list elementów, ładowanie i wyświetlanie małych fragmentów jako
niezbędną. 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
collectAsLazyPagingItems()
i przekazać zwróconą funkcję
LazyPagingItems
do items()
w naszym LazyColumn
. Podobnie jak w przypadku obsługi podziału na strony w widokach możesz wyświetlać substytuty podczas wczytywania danych, sprawdzając, czy item
jest 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 layoutów typu Lazy
Aby mieć pewność, że leniwe układy będą działać prawidłowo, możesz wziąć pod uwagę kilka wskazówek.
Unikaj używania elementów o rozmiarze 0 pikseli.
Może się tak zdarzyć w sytuacjach, gdy na przykład oczekuje się, że asynchronicznie Pobrać niektóre dane, np. obrazy, by później wypełnić elementy listy. W rezultacie układ Lazy skomponowałby wszystkie elementy bo ich wysokość wynosi 0 pikseli i mieści się widoczny obszar. 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, tak by układ Lazy mógł czyli jaka jest liczba elementów, które w rzeczywistości mieszczą się w widocznym obszarze:
@Composable fun Item(imageUrl: String) { AsyncImage( model = rememberAsyncImagePainter(model = imageUrl), modifier = Modifier.size(30.dp), contentDescription = null // ... ) }
Gdy znasz przybliżony rozmiar elementów po asynchronicznym załadowaniu danych, warto zadbać o to, aby ich rozmiar był taki sam przed i po załadowaniu, na przykład przez dodanie placeholderów. 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, gdy w ramach elementu nadrzędnego z możliwością przewijania w tym samym kierunku umieszczane są elementy podrzędne z możliwością przewijania bez zdefiniowanego rozmiaru. 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 emitowanie pojedynczych elementów i wielu elementów listy w jednym miejscu:
LazyColumn { item { Header() } items(data) { item -> PhotoItem(item) } item { Footer() } }
Pamiętaj, że dozwolone są przypadki, w których zagnieżdżasz różne układy kierunków, na przykład układ nadrzędny Row
i podrzędny LazyColumn
, które można przewijać:
Row( modifier = Modifier.horizontalScroll(scrollState) ) { LazyColumn { // ... } }
oraz w sytuacjach, w których nadal używasz tych samych układów kierunku, ale masz ustawiony stały rozmiar dla zagnieżdżonych elementów podrzędnych:
Column( modifier = Modifier.verticalScroll(scrollState) ) { LazyColumn( modifier = Modifier.height(200.dp) ) { // ... } }
Nie umieszczaj 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 radzą sobie z tym zgodnie z oczekiwaniami – najpierw układają elementy po drugie, jakby były różnymi elementami. Występują jednak pewne problemy.
Jeśli w ramach jednego elementu emitowanych jest wiele elementów, są one traktowane jako
jeden element, co oznacza, że nie można już ich tworzyć pojedynczo. Jeśli jeden
staje się widoczny na ekranie, a następnie wszystkie elementy odpowiadające parametrowi
taki element musi być skomponowany i zmierzony. Nadmierne używanie tego typu elementów może obniżyć wydajność. W ekstremalnym przypadku umieszczenia wszystkich elementów w jednym elemencie
całkowicie rezygnuje z używania leniwego układu. Oprócz potencjalnych problemów z wydajnością umieszczanie większej liczby elementów w jednym elemencie może też zakłócać działanie funkcji 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 separatory zmieniały przewijanie indeksów, ponieważ nie powinny one być uznawane za niezależne elementy. Nie wpłynie to na wydajność, ponieważ rozdzielacze są małe. Separator powinien być prawdopodobnie jeszcze przed wyświetleniem elementu, więc mogą być częścią poprzedniego. element:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Divider() } item { Item(2) } // ... }
Zastanów się nad wykorzystaniem niestandardowych aranżacji
Zwykle listy typu Lazy mają wiele elementów i zajmują więcej miejsca niż element przewijania. Jeśli jednak lista zawiera mało pozycji, mogą zawierać bardziej szczegółowe wymagania dotyczące ich pozycji w widocznym obszarze.
Aby to osiągnąć, użyj kategorii niestandardowej
Arrangement
i przekaże ją LazyColumn
. W poniższym przykładzie TopWithFooter
wystarczy zaimplementować metodę arrange
. Po pierwsze, elementy będą ustawiane jeden po drugim. Po drugie, jeśli całkowita używana wysokość jest mniejsza niż
wysokość widocznego obszaru spowoduje umieszczenie stopki 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
Aby zmaksymalizować wydajność układu opartego na technologii Lazy, od wersji Compose 1.2 możesz dodawać do list lub siatek komponent contentType
. Dzięki temu możesz określić typ treści
elementu układu, w przypadku, gdy tworzysz listę lub siatkę składającą się
wiele różnych typów produktów:
LazyColumn { items(elements, contentType = { it.type }) { // ... } }
Gdy podasz adres URL
contentType
W funkcji tworzenia można ponownie używać tylko kompozycji
między elementami tego samego typu. Ponowne wykorzystywanie jest bardziej wydajne,
nie zawierają elementów o podobnej strukturze, dodanie typu treści zapewnia
W trakcie tworzenia wiadomości nie jest podejmowana próba utworzenia elementu typu A na podstawie
inny element typu B. Pomaga to zmaksymalizować korzyści płynące z kompozycji
ponowne wykorzystanie treści
i wydajność układu.
Pomiar skuteczności
Skuteczność układu opartego na technologii Lazy można wiarygodnie mierzyć tylko w trybie wydania i z włączoną optymalizacją R8. W kompilowanych wersjach 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 wyświetla się, gdy JavaScript jest wyłączony
- Migracja listy
RecyclerView
na listę Lazy - Zapisywanie stanu interfejsu w sekcji Utwórz
- Kotlin w Jetpack Compose