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 wyświetlać 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 sprawić, że element Column będzie można przewijać, używając modyfikatora verticalScroll().

Listy Lazy

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 spowodować problemy z wydajnością, ponieważ wszystkie elementy zostaną utworzone i rozmieszczone niezależnie od tego, czy są widoczne.

Compose udostępnia zestaw komponentów, które tworzą i rozmieszczają tylko elementy widoczne w widocznym obszarze komponentu. Te komponenty to LazyColumn i LazyRow.

Jak sugeruje nazwa, różnica między LazyColumn i LazyRow polega na orientacji, w której układają elementy i przewijają. 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 bloku treści @Composable, który umożliwia aplikacjom bezpośrednie emitowanie funkcji kompozycyjnych, komponenty Lazy udostępniają blok LazyListScope.(). Ten blok LazyListScope zawiera DSL, który umożliwia aplikacjom opisywanie zawartości elementu. Komponent Lazy odpowiada za dodawanie zawartości każdego elementu zgodnie z wymaganiami układu i pozycji przewijania.

LazyListScope DSL

DSL LazyListScope udostępnia szereg funkcji do opisywania elementów w układzie. W najprostszym przypadku item() dodaje pojedynczy element, a items(Int) – 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. Te rozszerzenia pozwalają nam łatwo przenieść nasz przykład Column z powyższego fragmentu:

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

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

Leniwe siatki

Komponenty LazyVerticalGrid i LazyHorizontalGrid obsługują wyświetlanie elementów w siatce. Siatka LazyVerticalGrid wyświetla elementy w kontenerze, który można przewijać w pionie, i obejmuje wiele kolumn. Siatka LazyHorizontalGrid działa tak samo, ale w 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 z siatką zdjęć

Parametr columns w LazyVerticalGrid i parametr rows w LazyHorizontalGrid określają, jak komórki są formowane w kolumny lub wiersze. W poniższym 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 pozwala określić szerokość 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 elementów na ekranach o różnych rozmiarach.

Jeśli znasz dokładną liczbę kolumn, które mają być użyte, 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 określić niestandardowe rozpiętości kolumn dla elementów. Określ rozpiętość kolumn za pomocą parametru span metod LazyGridScope DSL item i items. maxLineSpan, jedna z wartości zakresu rozpiętości, jest szczególnie przydatna, gdy używasz rozmiaru adaptacyjnego, ponieważ liczba kolumn nie jest stała. Ten przykład pokazuje, jak ustawić pełną szerokość wiersza:

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

Leniwa siatka z przesunięciem

LazyVerticalStaggeredGrid i LazyHorizontalStaggeredGrid to elementy kompozycyjne, które umożliwiają utworzenie leniwie ładowanej, przesuniętej siatki elementów. Leniwa 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 stosowanie różnych wysokości poszczególnych elementów. Leniwe siatki poziome zachowują się 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 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 leniwej siatki pionowej z przesunięciem

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 przypisuje każdemu elementowi 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 z przesunięciem w Compose
Rysunek 2. Przykład leniwej, przesuniętej siatki pionowej ze stałymi kolumnami

Dopełnienie treści

Czasami trzeba dodać dopełnienie wokół krawędzi treści. Komponenty Lazy umożliwiają przekazywanie niektórych PaddingValues do parametru contentPadding, aby to obsługiwać:

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

W tym przykładzie dodajemy dopełnienie 16.dp do krawędzi poziomych (lewej i prawej) oraz 8.dp do górnej i dolnej krawędzi treści.

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

Inny przykład: możesz przekazać PaddingValues z Scaffold do contentPadding w LazyColumn. 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ć Arrangement.spacedBy(). W przykładzie poniżej dodano 4.dp odstępu między elementami:

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

Podobnie w przypadku LazyRow:

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

Siatki mogą być jednak układane 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 elementów

Domyślnie stan każdego elementu jest powiązany z pozycją elementu na liście lub w siatce. Może to jednak powodować problemy, jeśli zbiór danych się zmieni, ponieważ elementy, które zmienią pozycję, utracą zapamiętany stan. Wyobraź sobie sytuację, w której LazyRow znajduje się w LazyColumn. Jeśli wiersz zmieni pozycję elementu, użytkownik utraci pozycję przewijania w wierszu.

Aby temu zapobiec, możesz podać stabilny i unikalny klucz dla każdego elementu, podając blok parametrowi 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)
    }
}

Podając klucze, pomagasz Compose prawidłowo obsługiwać zmiany kolejności. Jeśli na przykład element zawiera zapamiętany stan, ustawienie kluczy pozwoli Compose przenieść ten stan razem 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 elementów. Typ klucza musi być obsługiwany przez Bundle, czyli mechanizm Androida do zachowywania stanów podczas ponownego tworzenia aktywności. Bundle obsługuje typy takie jak typy podstawowe, typy wyliczeniowe czy 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 elemencie kompozycyjnym, gdy aktywność zostanie ponownie utworzona, a nawet gdy przewiniesz ekran 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 automatycznie animuje on zmiany elementów. Leniwe układy zapewniają tę samą funkcjonalność w przypadku zmiany kolejności elementów. Interfejs API jest prosty – wystarczy ustawić modyfikator animateItem dla zawartoś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)
            )
        ) {
            // ...
        }
    }
}

Upewnij się, że podajesz klucze do elementów, aby można było znaleźć nową pozycję przeniesionego elementu.

Przykład: animowanie elementów na listach z opóźnionym ładowaniem

W Compose możesz animować zmiany elementów na listach leniwych. Gdy użyjesz ich razem, te fragmenty kodu będą implementować animacje podczas dodawania, usuwania i zmieniania 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 definiuje układ każdego elementu listy. Przyjmuje parametr headlineContent, który definiuje główną treść elementu.
  • Modyfikator animateItem stosuje domyślne animacje do dodawania, usuwania i przenoszenia elementów.

Poniższy fragment kodu przedstawia ekran, który zawiera elementy sterują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 do dodawania, usuwania i sortowania elementów.
    • onAddItem i onRemoveItem to wyrażenia lambda przekazywane do AddRemoveButtons w celu dodawania i usuwania elementów z listy.
    • resetOrder, onSortAlphabetically i onSortByLength to wyrażenia lambda przekazywane do OrderButtons w celu zmiany kolejności elementów na liście.
  • AddRemoveButtons wyświetla przyciski „Dodaj” i „Usuń”. Włącza/wyłącza przyciski i obsługuje kliknięcia przycisków.
  • 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 tekstowych. data jest zdefiniowany w innym miejscu.

Ten fragment kodu tworzy interfejs z przyciskami Dodaj element i Usuń 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 listy.
  • Parametry canAddItem i canRemoveItem określają stan włączenia przycisków. Jeśli zasada canAddItem lub canRemoveItem ma wartość Fałsz, odpowiedni przycisk jest wyłączony.
  • Parametry onAddItem i onRemoveItem 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 wybranie metody sortowania na liście lub zresetowanie kolejności listy. Komponent SegmentedButton umożliwia wybranie jednej opcji z listy.
  • resetOrder, orderAlphabetically i orderByLength 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ą zmieniane:

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

Przyklejone nagłówki

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

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

Aby uzyskać przyklejony nagłówek w LazyColumn, możesz użyć stickyHeader().

, która dostarcza treść nagłówka:

@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 „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 oraz je śledzić. Komponenty Lazy obsługują ten przypadek użycia przez podniesienie 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ą tylko informacji o pierwszym widocznym elemencie. W tym celu LazyListState udostępnia właściwości firstVisibleItemIndex i firstVisibleItemScrollOffset.

Jeśli weźmiemy przykład 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 elementy kompozycyjne interfejsu, ale są też scenariusze, w których zdarzenie nie musi być obsługiwane w tej samej kompozycji. Typowym przykładem jest wysyłanie zdarzenia analitycznego, gdy użytkownik przewinie stronę poza określony punkt. 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 elementach, które są obecnie wyświetlane, oraz o 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ć pozycję przewijania. LazyListState obsługuje tę funkcję za pomocą funkcji scrollToItem(), która „natychmiast” przyciąga pozycję przewijania, oraz animateScrollToItem(), która przewija 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 stronicowania umożliwia aplikacjom obsługę dużych list elementów, wczytywanie i wyświetlanie niewielkich fragmentów listy w razie potrzeby. Paging 3.0 i nowsze wersje obsługują Compose dzięki bibliotece 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 obiekt LazyPagingItems do items() w LazyColumn. Podobnie jak w przypadku obsługi stronicowania w widokach, możesz wyświetlać obiekty zastępcze podczas wczytywania danych, sprawdzając, czy item to null:

@Composable
fun MessageList(pager: Pager<Int, Message>) {
    val lazyPagingItems = pager.flow.collectAsLazyPagingItems()

    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it.id }
        ) { index ->
            val message = lazyPagingItems[index]
            if (message != null) {
                MessageRow(message)
            } else {
                MessagePlaceholder()
            }
        }
    }
}

Wskazówki dotyczące korzystania z układów Lazy

Aby układy Lazy działały zgodnie z oczekiwaniami, możesz skorzystać z kilku wskazówek.

Unikaj używania elementów o rozmiarze 0 pikseli.

Może się tak zdarzyć na przykład wtedy, gdy chcesz asynchronicznie pobrać dane, takie jak obrazy, aby później wypełnić elementy listy. Spowodowałoby to, że układ Lazy utworzyłby wszystkie elementy w pierwszym pomiarze, ponieważ ich wysokość wynosi 0 pikseli i wszystkie zmieściłyby się w widocznym obszarze. Po załadowaniu elementów i zwiększeniu ich wysokości układy Lazy odrzucą wszystkie pozostałe elementy, które zostały niepotrzebnie utworzone za pierwszym razem, ponieważ nie mieszczą się w obszarze widocznym. Aby tego uniknąć, ustaw domyślny rozmiar elementów, dzięki czemu układ Lazy będzie mógł prawidłowo obliczyć, ile elementów może się zmieścić w widocznym obszarze:

@Composable
fun Item(imageUrl: String) {
    AsyncImage(
        model = rememberAsyncImagePainter(model = imageUrl),
        modifier = Modifier.size(30.dp),
        contentDescription = null
        // ...
    )
}

Jeśli znasz przybliżony rozmiar elementów po asynchronicznym załadowaniu danych, dobrą praktyką jest upewnienie się, że rozmiar elementów pozostaje taki sam przed załadowaniem i po nim, na przykład przez dodanie obiektó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, które można przewijać, bez wstępnie zdefiniowanego rozmiaru są zagnieżdżone w innym elemencie nadrzędnym, który można przewijać w tym samym kierunku. Na przykład próba zagnieżdżenia elementu podrzędnego LazyColumn bez stałej wysokości w elemencie nadrzędnym Column, który można przewijać w pionie:

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

Zamiast tego ten sam efekt można osiągnąć, umieszczając wszystkie elementy kompozycyjne w jednym elemencie nadrzędnym LazyColumn i używając jego DSL do przekazywania różnych typów treści. Dzięki temu możesz emitować pojedyncze elementy, a także wiele 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, na przykład przewijany element nadrzędny Row i element podrzędny LazyColumn, są dozwolone:

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

Możesz też używać tych samych układów kierunku, ale ustawić stały rozmiar zagnieżdżonych elementów podrzędnych:

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

Unikaj umieszczania wielu elementów w jednym elemencie

W tym przykładzie druga funkcja lambda 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 Lazy będą obsługiwać to zgodnie z oczekiwaniami – będą układać elementy jeden po drugim, tak jakby były to różne elementy. Jednak takie rozwiązanie ma kilka wad.

Gdy wiele elementów jest emitowanych jako część jednego elementu, są one traktowane jako jedna encja, co oznacza, że nie można ich już komponować pojedynczo. Jeśli jeden element stanie się widoczny na ekranie, wszystkie elementy odpowiadające temu elementowi muszą zostać skomponowane i zmierzone. Jeśli będziesz używać ich zbyt często, może to negatywnie wpłynąć na skuteczność. W skrajnym przypadku, gdy wszystkie elementy zostaną umieszczone w jednym elemencie, całkowicie mija się to z celem korzystania z układów Lazy. Oprócz potencjalnych problemów z wydajnością umieszczenie większej liczby elementów w jednym elemencie będzie też zakłócać działanie scrollToItem() i animateScrollToItem().

Istnieją jednak uzasadnione przypadki użycia, w których można umieścić wiele elementów w jednym elemencie, np. separatory 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 powinien być widoczny, gdy widoczny jest element przed nim, 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 aranżacji

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

Aby to zrobić, możesz użyć niestandardowej branży Arrangement i przekazać ją do LazyColumn. W poniższym 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 na dole:

object TopWithFooter : Arrangement.Vertical {
    override fun Density.arrange(
        totalSize: Int,
        sizes: IntArray,
        outPositions: IntArray
    ) {
        var y = 0
        sizes.forEachIndexed { index, size ->
            outPositions[index] = y
            y += size
        }
        if (y < totalSize) {
            val lastIndex = outPositions.lastIndex
            outPositions[lastIndex] = totalSize - sizes.last()
        }
    }
}

Rozważ dodanie: contentType

Począwszy od Compose 1.2, aby zmaksymalizować wydajność układu Lazy, rozważ dodanie do list lub siatek elementu contentType. Dzięki temu możesz określić typ treści każdego elementu układu, gdy 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 używać kompozycji tylko między elementami tego samego typu. Ponieważ ponowne użycie jest bardziej efektywne, gdy tworzysz elementy o podobnej strukturze, podanie typów treści gwarantuje, że Compose nie spróbuje utworzyć elementu typu A na podstawie zupełnie innego elementu typu B. Pomaga to zmaksymalizować korzyści płynące z ponownego użycia kompozycji i wydajności układu Lazy.

Pomiar skuteczności

Wydajność układu Lazy można wiarygodnie zmierzyć tylko w trybie wersji i przy włączonej optymalizacji R8. W kompilacjach do debugowania przewijanie układu Lazy może wydawać się wolniejsze. Więcej informacji znajdziesz w artykule o wydajności Compose.

Dodatkowe materiały