Listy i siatki ładowane na żądanie

Wiele aplikacji musi wyświetlać kolekcje elementów. Z tego dokumentu dowiesz się, jak skutecznie to zrobić w Jetpack Compose.

Jeśli wiesz, że Twój przypadek użycia nie wymaga przewijania, możesz użyć prostego elementu Column lub Row (w zależności od kierunku) i wyemitować 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 sprawić, że element Column będzie przewijany, używając modyfikatora verticalScroll().

Listy leniwe

Jeśli musisz 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 będą komponowane i układane niezależnie od tego, czy są widoczne.

Compose udostępnia zestaw komponentów, które tworzą i układają tylko te elementy, które są widoczne w obszarze widoku komponentu. Są to:LazyColumn i LazyRow.

Jak sama nazwa wskazuje, różnica między LazyColumn a LazyRow polega na orientacji, w której wyświetlają elementy i umożliwiają przewijanie. LazyColumn tworzy listę przewijaną w pionie, a LazyRow tworzy listę przewijaną w poziomie.

Komponenty Lazy różnią się od większości układów w Compose. Zamiast akceptować parametr @Composable bloku treści, który umożliwia aplikacjom bezpośrednie emitowanie komponentów, komponenty Lazy udostępniają blok LazyListScope.(). Ten LazyListScope blok zawiera język DSL, który umożliwia aplikacjom opisanie zawartości produktu. Komponent Lazy odpowiada za dodawanie treści każdego elementu zgodnie z układem i pozycją przewijania.

LazyListScope DSL

Język DSL LazyListScope udostępnia wiele funkcji do opisywania elementów w układzie. W najprostszym przypadku funkcja item() dodaje 1 element, a funkcja 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ż wiele funkcji rozszerzeń, które umożliwiają dodawanie kolekcji elementów, np. List. Te rozszerzenia umożliwiają łatwe przeniesienie powyższego przykładu Column:

/**
 * import androidx.compose.foundation.lazy.items
 */
LazyColumn {
    items(messages) { message ->
        MessageRow(message)
    }
}

Istnieje też wariant funkcji rozszerzenia items() o nazwie itemsIndexed(), który podaje indeks. Więcej informacji znajdziesz w LazyListScope.

Siatki leniwe

Kompozycje LazyVerticalGridLazyHorizontalGrid umożliwiają wyświetlanie elementów w siatce. Kompozycja LazyVerticalGrid wyświetla elementy w kontenerze z możliwością przewijania w pionie, rozciągniętym na kilka kolumn, a kompozycje LazyHorizontalGrid działają w ten sam sposób na osi poziomej.

Siatki mają te same zaawansowane możliwości interfejsu API co listy i używają bardzo podobnego języka DSL –LazyGridScope.() do opisywania treści.

Zrzut ekranu telefonu przedstawiający siatkę zdjęć

Parametr columnsLazyVerticalGrid i parametr rowsLazyHorizontalGrid określają, jak komórki są tworzone w kolumnach lub wierszach. W tym przykładzie elementy są wyświetlane w siatce, a za pomocą GridCells.Adaptive ustawiono 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 następnie siatka dopasuje jak najwięcej kolumn. Pozostała szerokość jest rozdzielana równomiernie między kolumny po obliczeniu ich liczby. Ten adaptacyjny sposób określania rozmiaru jest szczególnie przydatny do wyświetlania zestawów produktów na ekranach o różnych rozmiarach.

Jeśli znasz dokładną liczbę kolumn, których chcesz użyć, możesz zamiast tego podać instancję GridCells.Fixed zawierającą liczbę wymaganych kolumn.

Jeśli Twój projekt wymaga, aby tylko niektóre elementy miały niestandardowe wymiary, możesz użyć obsługi siatki, aby podać niestandardowe zakresy kolumn dla elementów. Określ zakres kolumn za pomocą parametru span metod LazyGridScope DSL item i items. maxLineSpan, jedna z wartości zakresu, jest szczególnie przydatna, gdy używasz dopasowywania 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")
    }
    // ...
}

Lazy staggered grid

LazyVerticalStaggeredGridLazyHorizontalStaggeredGrid to funkcje kompozycyjne, które umożliwiają tworzenie wczytywanej na żądanie, rozłożonej w czasie siatki elementów. Lenistwo pionowa siatka z przesunięciem wyświetla elementy w kontenerze z możliwością przewijania w pionie, który obejmuje wiele kolumn i umożliwia poszczególnym elementom różną wysokość. Lenistwo w przypadku siatek poziomych działa tak samo na osi poziomej w przypadku elementów o różnej szerokości.

Poniższy fragment kodu to podstawowy przykład użycia LazyVerticalStaggeredGrid z 200.dp szerokością 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 leniwej, rozłożonej w czasie siatki pionowej

Aby ustawić stałą liczbę kolumn, możesz użyć StaggeredGridCells.Fixed(columns) zamiast StaggeredGridCells.Adaptive. Dzieli dostępną szerokość przez liczbę kolumn (lub wierszy w przypadku siatki poziomej) i sprawia, że 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()
)
Leniwa siatka obrazów w Compose
Rysunek 2. Przykład leniwej, rozłożonej w czasie siatki pionowej o stałych kolumnach

Wypełnienie treści

Czasami trzeba dodać do treści dopełnienie. Komponenty lazy umożliwiają przekazywanie niektórych PaddingValues do parametru contentPadding:

LazyColumn(
    contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
) {
    // ...
}

W tym przykładzie dodajemy 16.dp do poziomych krawędzi (lewej i prawej), a następnie 8.dp do góry i dołu treści.

Pamiętaj, że ten margines jest stosowany do treści, a nie do samego elementu LazyColumn. W powyższym przykładzie pierwszy element doda do góry dopełnienie 8.dp, ostatni element doda do dołu dopełnienie 8.dp, a wszystkie elementy będą miały dopełnienie 16.dp po lewej i prawej stronie.

Możesz na przykład przekazać Scaffold PaddingValues do LazyColumn contentPadding. Zapoznaj się z przewodnikiem dotyczącym wyświetlania od krawędzi do krawędzi.

Odstępy między treściami

Aby dodać odstępy między elementami, możesz użyć elementu Arrangement.spacedBy(). W przykładzie poniżej dodano 4.dp spacji między poszczególnymi elementami:

LazyColumn(
    verticalArrangement = Arrangement.spacedBy(4.dp),
) {
    // ...
}

Podobnie w przypadku LazyRow:

LazyRow(
    horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
    // ...
}

Siatki akceptują 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 powiązany z jego pozycją na liście lub w siatce. Może to jednak powodować problemy, jeśli zbiór danych ulegnie zmianie, ponieważ elementy, które zmienią pozycję, utracą zapamiętany stan. Jeśli wyobrazisz sobie sytuację, w której LazyRow znajduje się w LazyColumn, a pozycja elementu w wierszu ulegnie zmianie, użytkownik straci pozycję przewijania w wierszu.

Aby temu zapobiec, możesz podać stabilny i niepowtarzalny klucz dla każdego produktu, podając blok do parametru key. Podanie stabilnego klucza umożliwia zachowanie spójności stanu elementu 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)
    }
}

Dzięki kluczom Compose może prawidłowo obsługiwać zmiany kolejności. Jeśli na przykład element zawiera zapamiętany stan, ustawienie kluczy umożliwi bibliotece Compose przeniesienie tego stanu 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órych możesz używać jako kluczy produktów. Typ klucza musi być obsługiwany przez Bundle, czyli mechanizm Androida, który zachowuje stany, gdy aktywność jest ponownie tworzona. Bundle obsługuje typy takie jak typy proste, wyliczenia czy obiekty Parcelable.

LazyColumn {
    items(books, key = {
        // primitives, enums, Parcelable, etc.
    }) {
        // ...
    }
}

Klucz musi być obsługiwany przez Bundle, aby element rememberSaveable w komponowalnym elemencie można było przywrócić po ponownym utworzeniu aktywności, a nawet po przewinięciu 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 zmiany elementów automatycznie. Układy leniwe zapewniają taką 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 dla elementów, aby można było znaleźć nową pozycję przeniesionego elementu.

Przykład: animowanie elementów na listach ładowanych na żądanie

W Compose możesz animować zmiany elementów na listach wczytywanych na żądanie. Poniższe fragmenty kodu razem implementują animacje podczas dodawania, usuwania i zmiany kolejności elementów listy leniwej.

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

@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, gdy elementy są modyfikowane.
  • Funkcja items przypisuje unikalny klucz do każdego elementu na liście. Compose używa kluczy do śledzenia elementów i identyfikowania zmian w ich pozycjach.
  • ListItem określa układ każdego elementu listy. Przyjmuje parametr headlineContent, który określa główną treść produktu.
  • Modyfikator animateItem stosuje domyślne animacje do dodawania, usuwania i przenoszenia elementów.

Poniższy fragment kodu przedstawia ekran z elementami sterującymi do dodawania i usuwania elementów oraz sortowania predefiniowanej 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 do 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, onSortAlphabetically i onSortByLength to wyrażenia lambda, które są przekazywane do funkcji OrderButtons, aby zmienić kolejność elementów na liście.
  • AddRemoveButtons wyświetla przyciski „Dodaj” i „Usuń”. Włącza i wyłącza przyciski oraz obsługuje ich kliknięcia.
  • OrderButtons wyświetla przyciski do zmiany kolejności na liście. Otrzymuje funkcje lambda do resetowania kolejności i sortowania listy według długości lub alfabetycznie.
  • ListAnimatedItems wywołuje element kompozycyjny ListAnimatedItems, przekazując 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 wiersz przycisków do dodawania i usuwania elementów z listy.
  • Parametry canAddItemcanRemoveItem określają stan włączenia przycisków. Jeśli wartości canAddItem lub canRemoveItem to 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 (Resetuj, AlfabetycznieDł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 umożliwia wybranie jednej opcji z listy.
  • 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 powyższych fragmentów kodu po zmianie kolejności elementów:

Rysunek 1. Lista, która animuje przejścia elementów, gdy są one dodawane, usuwane lub sortowane.

Nagłówki przyklejone

Wzorzec „przyklejonego nagłówka” jest przydatny podczas wyświetlania list pogrupowanych danych. Poniżej znajdziesz przykład „listy kontaktów” pogrupowanej według inicjałów każdego kontaktu:

Film przedstawiający przewijanie listy kontaktów na telefonie w górę i w dół

Aby uzyskać przyklejony nagłówek za pomocą LazyColumn, możesz użyć eksperymentalnej funkcji stickyHeader(), podając treść nagłówka:

@Composable
fun ListWithHeader(items: List<Item>) {
    LazyColumn {
        stickyHeader {
            Header()
        }

        items(items) { item ->
            ItemRow(item)
        }
    }
}

Aby uzyskać listę z kilkoma nagłówkami, taką jak w przykładzie „lista kontaktów” powyżej, możesz wykonać te czynności:

// This ideally would be done in the ViewModel
val grouped = contacts.groupBy { it.firstName[0] }

@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ć na zmiany pozycji przewijania i układu elementów. Komponenty Lazy obsługują ten przypadek użycia przez przeniesienie elementu 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ą tylko informacji o pierwszym widocznym elemencie. W tym celu usługa LazyListState udostępnia właściwości firstVisibleItemIndexfirstVisibleItemScrollOffset.

Jeśli użyjemy przykładu wyświetlania 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()
        }
    }
}

Odczytywanie stanu bezpośrednio w kompozycji jest przydatne, gdy musisz zaktualizować inne komponenty interfejsu, ale są też sytuacje, 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 sobie z tym poradzić, 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 aktualnie wyświetlanych elementach i ich granicach na ekranie za pomocą właściwości layoutInfo. Więcej informacji znajdziesz w sekcji dotyczącej klasy LazyListLayoutInfo.

Kontrolowanie pozycji przewijania

Oprócz reagowania na pozycję przewijania przydatne jest też, aby aplikacje mogły kontrolować tę pozycję. LazyListState obsługuje to za pomocą funkcji scrollToItem() , która „natychmiast” zmienia pozycję przewijania, oraz animateScrollToItem() , która przewija za pomocą animacji (tzw. płynne przewijanie):

@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 Paging umożliwia aplikacjom obsługę dużych list elementów, wczytywanie i wyświetlanie małych fragmentów listy w razie potrzeby. Biblioteka Paging w wersji 3.0 i nowszych obsługuje Jetpack Compose za pomocą biblioteki androidx.paging:paging-compose.

Aby wyświetlić listę treści podzielonych na strony, możemy użyć funkcji rozszerzenia collectAsLazyPagingItems(), a następnie przekazać zwrócony element LazyPagingItems do items() w naszym LazyColumn. Podobnie jak w przypadku obsługi stronicowania w widokach, możesz wyświetlać symbole 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 korzystania z układów leniwych

Aby mieć pewność, że układy Lazy działają zgodnie z oczekiwaniami, możesz wziąć pod uwagę kilka wskazówek.

Unikaj elementów o rozmiarze 0 pikseli

Może się to zdarzyć w sytuacjach, w których na przykład oczekujesz asynchronicznego pobrania danych, takich jak obrazy, aby później wypełnić nimi elementy listy. Spowoduje to, że układ Lazy skomponuje wszystkie elementy w pierwszym pomiarze, ponieważ ich wysokość wynosi 0 pikseli i wszystkie zmieszczą się w widocznym obszarze. Gdy elementy zostaną wczytane i zwiększą swoją wysokość, układy Lazy odrzucą wszystkie pozostałe elementy, które zostały niepotrzebnie skomponowane za pierwszym razem, ponieważ nie mieszczą się w obszarze widocznym. Aby tego uniknąć, ustaw domyślny rozmiar produktów, aby układ Lazy mógł prawidłowo obliczyć, ile produktów zmieś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 wczytaniu danych, warto zadbać o to, aby rozmiar elementów pozostał taki sam przed i po wczytaniu, np. dodając kilka elementów zastępczych. 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 z możliwością przewijania bez zdefiniowanego rozmiaru są zagnieżdżone w innym elemencie nadrzędnym z możliwością przewijania w tym samym kierunku. Na przykład próba zagnieżdżenia elementu podrzędnego LazyColumn bez stałej wysokości w pionowo przewijanym elemencie nadrzędnym Column:

// throws IllegalStateException
Column(
    modifier = Modifier.verticalScroll(state)
) {
    LazyColumn {
        // ...
    }
}

Ten sam efekt można osiągnąć, umieszczając wszystkie funkcje kompozycyjne w jednej funkcji nadrzędnej LazyColumn i używając jej DSL do przekazywania różnych typów treści. Umożliwia to emitowanie pojedynczych elementów, a także wielu elementów listy w jednym miejscu:

LazyColumn {
    item {
        Header()
    }
    items(data) { item ->
        PhotoItem(item)
    }
    item {
        Footer()
    }
}

Pamiętaj, że przypadki, w których zagnieżdżasz układy o różnych kierunkach, np. przewijany element nadrzędny Row i element podrzędny LazyColumn, są dozwolone:

Row(
    modifier = Modifier.horizontalScroll(scrollState)
) {
    LazyColumn {
        // ...
    }
}

Oprócz przypadków, w których nadal używasz tych samych układów kierunkowych, ale ustawiasz też stały rozmiar zagnieżdżonych elementów podrzędnych:

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

Uważaj, aby nie umieszczać wielu elementów w jednym elemencie

W tym przykładzie funkcja lambda drugiego elementu emituje 2 elementy w jednym bloku:

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp)
) {
    item { Item(0) }
    item {
        Item(1)
        Item(2)
    }
    item { Item(3) }
    // ...
}

Układy leniwe poradzą sobie z tym zgodnie z oczekiwaniami – będą układać elementy jeden po drugim, tak jakby były różnymi elementami. Ma to jednak kilka wad.

Gdy wiele elementów jest emitowanych w ramach jednego elementu, są one traktowane jako jedna encja, co oznacza, że nie można ich już tworzyć indywidualnie. Jeśli jeden element stanie się widoczny na ekranie, wszystkie elementy odpowiadające temu elementowi muszą zostać skomponowane i zmierzone. Jeśli jest nadmiernie używana, może obniżyć wydajność. W ekstremalnym przypadku umieszczenia wszystkich elementów w jednym elemencie całkowicie niweczy to cel używania układów leniwych. Oprócz potencjalnych problemów z wydajnością umieszczenie większej liczby elementów w jednym produkcie będzie też zakłócać działanie scrollToItem()animateScrollToItem().

Istnieją jednak uzasadnione przypadki umieszczania wielu elementów w jednym elemencie, np. separatorów na liście. Nie chcesz, aby separatory zmieniały indeksy przewijania, ponieważ nie powinny być traktowane jako niezależne elementy. Nie ma to też wpływu na wydajność, ponieważ separatory są małe. Separator prawdopodobnie musi być widoczny, gdy widoczny jest element poprzedzający, więc może być częścią poprzedniego elementu:

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp)
) {
    item { Item(0) }
    item {
        Item(1)
        Divider()
    }
    item { Item(2) }
    // ...
}

Rozważ użycie aranżacji niestandardowych

Zwykle listy leniwe zawierają wiele elementów i zajmują więcej miejsca niż kontener przewijania. Jeśli jednak lista zawiera niewiele elementów, projekt może mieć bardziej szczegółowe wymagania dotyczące ich rozmieszczenia w obszarze widoku.

Aby to osiągnąć, możesz użyć niestandardowej kategorii Arrangement i przekazać ją do parametru LazyColumn. W tym przykładzie obiekt TopWithFooter musi implementować tylko metodę arrange. Po pierwsze, elementy będą umieszczane jeden po drugim. Po drugie, jeśli łączna wykorzystana wysokość jest mniejsza niż wysokość widocznego obszaru, stopka zostanie umieszczona u dołu:

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 Lazy, od wersji Compose 1.2 warto dodać contentType do list lub siatek. Umożliwia to określenie typu treści dla każdego elementu układu w przypadku tworzenia listy lub siatki składającej się z wielu różnych typów elementów:

LazyColumn {
    items(elements, contentType = { it.type }) {
        // ...
    }
}

Gdy podasz parametr contentType, Compose będzie mógł ponownie wykorzystywać kompozycje tylko w przypadku elementów tego samego typu. Ponieważ ponowne używanie jest bardziej wydajne, gdy tworzysz elementy o podobnej strukturze, podanie typów treści gwarantuje, że funkcja Compose nie będzie próbować tworzyć elementu typu A na zupełnie innym elemencie typu B. Pomaga to maksymalizować korzyści z ponownego używania kompozycji i wydajność układu Lazy.

Pomiar skuteczności

Skuteczność układu Lazy można wiarygodnie zmierzyć tylko w trybie wydania i przy włączonej optymalizacji R8. W wersjach debugowania przewijanie układu Lazy Layout może być wolniejsze. Więcej informacji znajdziesz w artykule Skuteczność kompozycji.

Dodatkowe materiały