Listy i siatki

Wiele aplikacji musi wyświetlać kolekcje elementów. Z tego dokumentu dowiesz się, jak sprawnie wykonać tę czynność 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. Te komponenty to 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 tworzy listę przewijaną w poziomie.

Komponenty leniwie wczytywane różnią się od większości układów w Compose. Zamiast akceptować parametr bloku treści @Composable, co pozwala aplikacjom bezpośrednio emitować elementy składane, komponenty leniwie udostępniają blok LazyListScope.(). Ten blok LazyListScope udostępnia język 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

LazyVerticalGridLazyHorizontalGridSkładniki umożliwiają wyświetlanie elementów w siatce. Pozioma 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.

Zrzut ekranu telefonu z siatką zdjęć

Parametr columnsLazyVerticalGrid i parametr rowsLazyHorizontalGrid określają, jak komórki są formowane w kolumny lub wiersze. W tym przykładzie elementy są wyświetlane w siatce, a kolumny mają szerokość co najmniej GridCells.Adaptive: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ć wystąpienie parametru GridCells.Fixed zawierające liczbę wymaganych kolumn.

Jeśli Twój projekt wymaga, aby tylko niektóre elementy miały wymiary niestandardowe, możesz użyć obsługi siatki, aby ustawić niestandardowe zakresy kolumn dla elementów. Określ zakres kolumny za pomocą parametru span metod LazyGridScope DSL itemitems. 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")
    }
    // ...
}

Siatka o zmiennym przesunięciu

LazyVerticalStaggeredGrid i LazyHorizontalStaggeredGrid to komponenty, które umożliwiają tworzenie leniwie wczytywanych, opóźnionych siatek elementów. W przypadku opóźnionego pionowego siatki o zmiennej wielkości elementy są wyświetlane w kontenerze, który można przewijać w kierunku pionowym. Kontener 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()
)

Rysunek 1. Przykład opóźnionego siatki pionowej z przesunięciem

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()
)
Siatka obrazów w edytorze z leniwym wczytywaniem
Rysunek 2. Przykład leniwej siatki pionowej z nieruchomymi kolumnami

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 ta przestrzeń jest dodawana do treści, a nie do samego LazyColumn. W przykładzie powyżej pierwszy element doda 8.dp do górnego marginesu, ostatni element doda 8.dp do dolnego marginesu, a wszystkie elementy będą miały 16.dp po lewej i prawej stronie.

Innym przykładem jest przekazanie wartości Scaffold do PaddingValues w LazyColumn.contentPadding Zapoznaj się z przejściem na krawędzię do krawędzi.

Odstępy między treściami

Aby dodać odstęp między elementami, możesz użyć elementu 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),
) {
    // ...
}

Siatka może jednak być ułożona zarówno w pionie, jak i w poziomie:

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 LazyRowLazyColumn, w którym wiersz zmienia 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 elementu podczas zmian w zbiorze danych:

LazyColumn {
    items(
        items = messages,
        key = { message ->
            // Return a stable + unique key for the item
            message.id
        }
    ) { message ->
        MessageRow(message)
    }
}

Dzięki temu Compose będzie prawidłowo zarządzać zmianami kolejności. Jeśli na przykład Twój element zawiera zapamiętany stan, ustawienia kluczy pozwolą komponentowi Compose zmienić ten stan wraz z elementem, gdy zmieni 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ć, gdy zostanie utworzona nowa wersja aktywności, lub nawet gdy przewiniesz od tego elementu i z powrotem.

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 mają tę samą funkcjonalność w przypadku zmiany kolejności elementów. Interfejs API jest prosty – wystarczy ustawić modyfikator animateItem w treści produktu:

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.

Przykład: animacja elementów na liście z opóźnionym wczytywaniem

Za pomocą Compose możesz animować zmiany elementów na listach opóźnionych. Połączone fragmenty kodu umożliwiają stosowanie animacji podczas dodawania, usuwania i zmieniania kolejności elementów listy opóźnionej.

Ten fragment kodu wyświetla listę ciągów znaków z animowanymi przejściami, gdy elementy są dodawane, usuwane lub zmieniają kolejność:

@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),
            )
        }
    }
}

Najważniejsze informacje o kodzie

  • ListAnimatedItems wyświetla listę ciągów znaków w LazyColumn z animowanymi przejściami podczas modyfikowania elementów.
  • Funkcja items przypisuje unikalny klucz do każdego elementu na liście. Compose używa klawiszy do śledzenia elementów i identyfikowania zmian ich pozycji.
  • ListItem określa układ każdego elementu listy. Przyjmuje parametr headlineContent, który określa główną treść elementu.
  • Modyfikator animateItem stosuje domyślne animacje do dodawania, usuwania i przenoszenia elementów.

Na poniższym fragmencie ekranu widać elementy sterujące służące do dodawania i usuwania elementów oraz sortowania wstępnie zdefiniowanej listy:

@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)
        }
    }
}

Najważniejsze informacje o kodzie

  • ListAnimatedItemsExample wyświetla ekran z elementami sterującymi dodawania, usuwania i sortowania elementów.
    • onAddItemonRemoveItem to wyrażenia lambda przekazywane do funkcji AddRemoveButtons w celu dodawania i usuwania elementów z listy.
    • resetOrder, onSortAlphabeticallyonSortByLength to wyrażenia lambda przekazywane do funkcji OrderButtons w celu zmiany kolejności elementów na liście.
  • AddRemoveButtons wyświetla przyciski „Dodaj” i „Usuń”. Włącza i wyłącza przyciski oraz obsługuje kliknięcia przycisków.
  • OrderButtons wyświetla przyciski do zmiany kolejności listy. Otrzymuje funkcje lambda służące do resetowania kolejności i sortowania listy według długości lub alfabetycznie.
  • ListAnimatedItems wywołuje element kompozytowany ListAnimatedItems, przekazując mu listę data, aby wyświetlić animowaną listę ciągów znaków. Wartość data jest zdefiniowana w innym miejscu.

Ten fragment kodu tworzy interfejs z przyciskami Dodaj elementUsuń element:

@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")
        }
    }
}

Najważniejsze informacje o kodzie

  • AddRemoveButtons wyświetla rząd przycisków umożliwiających dodawanie i usuwanie elementów na liście.
  • Parametry canAddItemcanRemoveItem określają stan włączenia przycisków. Jeśli canAddItem lub canRemoveItem mają wartość Fałsz, odpowiedni przycisk jest wyłączony.
  • Parametry onAddItemonRemoveItem to funkcje lambda, które są wykonywane, gdy użytkownik kliknie odpowiedni przycisk.

Na koniec ten fragment kodu wyświetla 3 przyciski do sortowania listy (Reset, Alfabetycznie i Długość):

@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)
                }
            }
        }
    }
}

Najważniejsze informacje o kodzie

  • OrderButtons wyświetla SingleChoiceSegmentedButtonRow, aby umożliwić użytkownikom wybór metody sortowania na liście lub zresetowanie kolejności listy. Komponent SegmentedButton pozwala wybrać jedną opcję z listy opcji.
  • resetOrder, orderAlphabeticallyorderByLength to funkcje lambda, które są wykonywane po wybraniu odpowiedniego przycisku.
  • Zmienna stanu selectedIndex śledzi wybraną opcję.

Wynik

Ten film pokazuje wynik działania poprzednich fragmentów kodu, gdy elementy są w innej kolejności:

Rysunek 1. lista, która animuje przejścia elementów podczas ich dodawania, usuwania lub sortowania.

Przyklejone nagłówki (funkcja eksperymentalna)

Wzór „przyklejony nagłówek” jest przydatny podczas wyświetlania listy pogrupowanych danych. Poniżej możesz zobaczyć przykład „listy kontaktów” pogrupowanych według pierwszej litery nazwiska:

Film przedstawiający telefon, na którym przewijana jest lista kontaktów

Aby uzyskać przypinany nagłówek za pomocą 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 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ładu 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 prostych przypadkach aplikacje zwykle potrzebują informacji tylko o pierwszym widocznym elemencie. W tym celu usługa LazyListState udostępnia właściwości firstVisibleItemIndexfirstVisibleItemScrollOffset.

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 efektywnie 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. Biblioteka androidx.paging:paging-compose w wersji 3.0 i nowszej obsługuje Compose.

Aby wyświetlić listę treści podzielonych na strony, możemy użyć funkcji rozszerzenia collectAsLazyPagingItems(), a potem przekazać zwróconą wartość LazyPagingItems do funkcji items() w naszym LazyColumn. Podobnie jak w przypadku obsługi podziału na strony w widokach możesz wyświetlać puste miejsca 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 korzystania z layoutów typu Lazy

Oto kilka wskazówek, które pomogą Ci zapewnić prawidłowe działanie układów z lazy load.

Unikaj elementów o wymiarach 0 pikseli

Może się to zdarzyć na przykład wtedy, gdy 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 technologii Lazy Containers odrzuciłyby wszystkie pozostałe 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ł poprawnie 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
        // ...
    )
}

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 zachować 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 elementy podrzędne, które można przewijać, bez zdefiniowanego rozmiaru są zagnieżdżane w elementach nadrzędnych, które można przewijać 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 pionowej:

// 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 oraz 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 {
        // ...
    }
}

Dotyczy to również przypadków, w których nadal używasz układów o tej samej orientacji, ale dodatkowo ustawiasz stały rozmiar dla zagnieżdżonych elementów podrzędnych:

Column(
    modifier = Modifier.verticalScroll(scrollState)
) {
    LazyColumn(
        modifier = Modifier.height(200.dp)
    ) {
        // ...
    }
}

Umieszczanie wielu elementów w jednym elemencie

W tym przykładzie druga funkcja 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 emitowanych jest kilka elementów w ramach jednego elementu, są one traktowane jako jedna entyfikacja, co oznacza, że nie można ich już komponować indywidualnie. Jeśli jeden element jest widoczny na ekranie, wszystkie elementy odpowiadające temu elementowi muszą zostać zdefiniowane i zmierzone. Może to obniżyć wydajność, jeśli jest nadużywane. W skrajnym przypadku umieszczenia wszystkich elementów w jednym elemencie całkowicie zatracony zostaje sens korzystania z layoutów typu Lazy. 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 jest uzasadnione, na przykład w przypadku rozdzielaczy na liście. Nie chcesz, aby separatory zmieniały indeksy przewijania, ponieważ nie powinny być uważane za elementy niezależne. Nie wpłynie to na wydajność, ponieważ rozdzielacze są małe. Podział będzie widoczny, gdy widoczny będzie element poprzedzający, więc będzie można go umieścić w poprzednim elemencie:

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, w Twoim projekcie mogą obowiązywać 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()
        }
    }
}

Rozważ dodanie contentType

Aby zmaksymalizować wydajność układu opartego na technologii Lazy, od wersji Compose 1.2 warto dodawać do list lub siatek komponent contentType. 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 wykorzystanie 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 całkowicie 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

Skuteczność układu opartego na technologii Lazy można wiarygodnie mierzyć tylko wtedy, gdy jest on uruchamiany 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 Otrzymywanie wiadomości e-mail.

Dodatkowe materiały