Listy i siatki

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().

Lazy lists

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. Te komponenty to:LazyColumn iLazyRow.

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 – 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ą LazyListScope.() blok. Ten LazyListScope blok zawiera język DSL, który umożliwia aplikacjom opisanie zawartości produktu. Komponent Lazy jest następnie odpowiedzialny za dodawanie treści każdego elementu zgodnie z układem i pozycją przewijania.

LazyListScope DSL

Język DSL LazyListScope udostępnia szereg funkcji do opisywania elementów w układzie. W najprostszym przypadku polecenie item() dodaje jeden element, a polecenie 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.

Leniwe siatki

Kompozycje LazyVerticalGridLazyHorizontalGrid umożliwiają wyświetlanie elementów w siatce. Kompozycja LazyVerticalGrid wyświetla elementy w kontenerze przewijanym w pionie, rozciągniętym na wiele kolumn, a kompozycje LazyHorizontalGrid zachowują się tak samo na osi poziomej.

Siatki mają te same zaawansowane możliwości interfejsu API co listy i korzystają z 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ą 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 po równo między kolumny po obliczeniu ich liczby. Ten adaptacyjny sposób określania rozmiaru jest szczególnie przydatny do wyświetlania zestawów elementó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 rozmiaru adaptacyjnego, ponieważ liczba kolumn nie jest stała. Ten przykład pokazuje, jak podać pełny zakres wiersza:

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 30.dp)
) {
    item(span = {
        // LazyGridItemSpanScope:
        // maxLineSpan
        GridItemSpan(maxLineSpan)
    }) {
        CategoryCard("Fruits")
    }
    // ...
}

Leniwa siatka kaskadowa

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

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, naprzemiennej 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 dodamy 16.dp dopełnienia do krawędzi poziomych (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 8.dp do góry, ostatni element doda 8.dp do dołu, a wszystkie elementy będą miały 16.dp po lewej i prawej stronie.

Możesz też przekazać Scaffold's PaddingValues do LazyColumn's 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. Wyobraź sobie sytuację, w której LazyRow znajduje się w LazyColumn. Jeśli pozycja elementu w wierszu ulegnie zmianie, użytkownik utraci 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 podaniu kluczy 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żna 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 można było przywrócić rememberSaveable w komponencie elementu, gdy aktywność zostanie utworzona ponownie, a nawet gdy przewiniesz widok poza ten element i wrócisz 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

Za pomocą 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 zmieniana jest ich 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, 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 funkcję kompozycyjną ListAnimatedItems, przekazując listę data, aby wyświetlić animowaną listę ciągów znaków. 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 umożliwiających dodawanie i usuwanie 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, 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 A 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.

Przyklejone nagłówki (funkcja eksperymentalna)

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 poszczególnych kontaktów:

Film przedstawiający telefon, na którym przewija się w górę i w dół lista kontaktów

Aby uzyskać przyklejony nagłówek z LazyColumn, możesz użyć eksperymentalnej funkcji stickyHeader() LazyColumn, 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 wykonać te czynności:

// 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ć 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 firstVisibleItemIndex i firstVisibleItemScrollOffset.

Jeśli użyjemy przykładu wyświetlania i ukrywania przycisku w zależności od tego, czy użytkownik przewinął widok poza 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 skutecznie 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 przydatna jest też możliwość kontrolowania tej pozycji przez aplikacje. LazyListState obsługuje to za pomocą funkcji scrollToItem() , która „natychmiast” zmienia pozycję przewijania, oraz funkcji 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 androidx.paging:paging-compose w Pagingu 3.0 i nowszych wersjach zapewnia obsługę 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 ich wysokość się zwiększy, układy Lazy odrzucą wszystkie pozostałe elementy, które zostały niepotrzebnie skomponowane za pierwszym razem, ponieważ nie mieszczą się w widocznym obszarze. 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
        // ...
    )
}

Jeśli znasz przybliżony rozmiar elementów po asynchronicznym wczytaniu danych, dobrym rozwiązaniem jest upewnienie się, że rozmiar elementów pozostaje taki sam przed i po wczytaniu, np. przez dodanie niektórych 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 element nadrzędny Column z możliwością przewijania w pionie:

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

Ten sam efekt można osiągnąć, umieszczając wszystkie funkcje kompozycyjne w jednym elemencie nadrzędnym LazyColumn i używając jego 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 dozwolone są przypadki, w których zagnieżdżasz układy o różnych kierunkach, np. przewijany element nadrzędny Row i element podrzędny LazyColumn:

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 produkcie

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. Istnieje jednak kilka problemów związanych z takim postępowaniem.

Gdy wiele elementów jest emitowanych w ramach jednego elementu, są one traktowane jako jedna encja, co oznacza, że nie można ich już komponować oddzielnie. 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 wpłynie to też na wydajność, ponieważ separatory są małe. Separator prawdopodobnie będzie musiał 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 niestandardowych układów

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 wyświetlania.

Aby to osiągnąć, możesz użyć niestandardowej branży Arrangement i przekazać ją do parametru LazyColumn. W poniższym przykładzie obiekt TopWithFooter musi zaimplementować 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. Dzięki temu możesz określić typ treści dla każdego elementu układu, jeśli tworzysz listę lub siatkę składającą się z wielu różnych typów elementów:

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

Gdy podasz contentType, Compose może ponownie wykorzystywać kompozycje tylko między elementami tego samego typu. 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 mierzyć tylko wtedy, gdy jest on uruchomiony w trybie wydania i z włączoną optymalizacją R8. W wersjach debugowania przewijanie układu Lazy Layout może być wolniejsze. Więcej informacji znajdziesz w artykule Skuteczność kompozycji.

Dodatkowe materiały