Lazy Lists und Lazy Grids

In vielen Apps müssen Sammlungen von Elementen angezeigt werden. In diesem Dokument wird erläutert, wie Sie dies in Jetpack Compose effizient tun können.

Wenn Sie wissen, dass in Ihrem Anwendungsfall kein Scrollen erforderlich ist, können Sie eine einfache Column oder Row (je nach Richtung) verwenden und den Inhalt der einzelnen Elemente ausgeben, indem Sie auf folgende Weise eine Liste durchlaufen:

@Composable
fun MessageList(messages: List<Message>) {
    Column {
        messages.forEach { message ->
            MessageRow(message)
        }
    }
}

Mit dem Modifikator verticalScroll() können wir die Column scrollbar machen.

Verzögerte Listen

Wenn Sie eine große Anzahl von Elementen (oder eine Liste mit unbekannter Länge) anzeigen müssen, kann die Verwendung eines Layouts wie Column zu Leistungsproblemen führen, da alle Elemente zusammengestellt und angeordnet werden, unabhängig davon, ob sie sichtbar sind oder nicht.

Compose bietet eine Reihe von Komponenten, die nur Elemente zusammensetzen und anordnen, die im Darstellungsbereich der Komponente sichtbar sind. Zu diesen Komponenten gehören LazyColumn und LazyRow.

Wie der Name schon sagt, besteht der Unterschied zwischen LazyColumn und LazyRow in der Ausrichtung, in der die Elemente angeordnet und gescrollt werden. LazyColumn erzeugt eine vertikal scrollbare Liste und LazyRow eine horizontal scrollbare Liste.

Die Lazy-Komponenten unterscheiden sich von den meisten Layouts in Compose. Anstatt einen @Composable-Inhaltsblockparameter zu akzeptieren, mit dem Apps direkt Composables ausgeben können, bieten die Lazy-Komponenten einen LazyListScope.()-Block. Dieser LazyListScope-Block bietet eine DSL, mit der Apps die Inhalte der Elemente beschreiben können. Die Lazy-Komponente ist dann dafür verantwortlich, den Inhalt der einzelnen Elemente entsprechend dem Layout und der Scrollposition hinzuzufügen.

LazyListScope DSL

Die DSL von LazyListScope bietet eine Reihe von Funktionen zum Beschreiben von Elementen im Layout. Im einfachsten Fall fügt item() ein einzelnes Element und items(Int) mehrere Elemente hinzu:

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

Es gibt auch eine Reihe von Erweiterungsfunktionen, mit denen Sie Sammlungen von Elementen hinzufügen können, z. B. List. Mit diesen Erweiterungen können wir unser Column-Beispiel von oben ganz einfach migrieren:

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

Es gibt auch eine Variante der Erweiterungsfunktion items() namens itemsIndexed(), die den Index bereitstellt. Weitere Informationen finden Sie in der Referenz zu LazyListScope.

Lazy-Raster

Die Composables LazyVerticalGrid und LazyHorizontalGrid unterstützen die Anzeige von Elementen in einem Raster. In einem Lazy Vertical Grid werden die Elemente in einem vertikal scrollbaren Container angezeigt, der sich über mehrere Spalten erstreckt. Lazy Horizontal Grids verhalten sich auf der horizontalen Achse genauso.

Raster haben dieselben leistungsstarken API-Funktionen wie Listen und verwenden auch eine sehr ähnliche DSL (LazyGridScope.()) zum Beschreiben des Inhalts.

Screenshot eines Smartphones mit einem Raster von Fotos

Der Parameter columns in LazyVerticalGrid und der Parameter rows in LazyHorizontalGrid steuern, wie Zellen in Spalten oder Zeilen angeordnet werden. Im folgenden Beispiel werden Elemente in einem Raster angezeigt. Dabei wird GridCells.Adaptive verwendet, um für jede Spalte eine Breite von mindestens 128.dp festzulegen:

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 128.dp)
) {
    items(photos) { photo ->
        PhotoItem(photo)
    }
}

Mit LazyVerticalGrid können Sie eine Breite für Elemente angeben. Das Raster passt dann so viele Spalten wie möglich an. Die verbleibende Breite wird gleichmäßig auf die Spalten verteilt, nachdem die Anzahl der Spalten berechnet wurde. Diese adaptive Größenanpassung ist besonders nützlich, um Gruppen von Elementen auf verschiedenen Bildschirmgrößen anzuzeigen.

Wenn Sie die genaue Anzahl der zu verwendenden Spalten kennen, können Sie stattdessen eine Instanz von GridCells.Fixed mit der Anzahl der erforderlichen Spalten angeben.

Wenn in Ihrem Design nur bestimmte Elemente von den Standardabmessungen abweichen sollen, können Sie das Raster verwenden, um benutzerdefinierte Spaltenbreiten für diese Elemente festzulegen. Geben Sie die Spaltenbreite mit dem Parameter span der Methoden LazyGridScope DSL item und items an. maxLineSpan, einer der Werte des Bereichs, ist besonders nützlich, wenn Sie die adaptive Größenanpassung verwenden, da die Anzahl der Spalten nicht festgelegt ist. In diesem Beispiel wird gezeigt, wie ein vollständiger Zeilenbereich angegeben wird:

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

Lazy-Staggered-Raster

LazyVerticalStaggeredGrid und LazyHorizontalStaggeredGrid sind Composables, mit denen Sie ein gestaffeltes Raster von Elementen mit Lazy Loading erstellen können. In einem Lazy Vertical Staggered Grid werden die Elemente in einem vertikal scrollbaren Container angezeigt, der sich über mehrere Spalten erstreckt und in dem einzelne Elemente unterschiedlich hoch sein können. Lazy-Horizontal-Grids verhalten sich auf der horizontalen Achse genauso, nur dass die Elemente unterschiedliche Breiten haben.

Das folgende Snippet ist ein einfaches Beispiel für die Verwendung von LazyVerticalStaggeredGrid mit einer Breite von 200.dp pro 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()
)

Abbildung 1. Beispiel für ein vertikales Lazy-Staggered-Grid

Wenn Sie eine feste Anzahl von Spalten festlegen möchten, können Sie StaggeredGridCells.Fixed(columns) anstelle von StaggeredGridCells.Adaptive verwenden. Dabei wird die verfügbare Breite durch die Anzahl der Spalten (oder Zeilen bei einem horizontalen Raster) geteilt und jedes Element nimmt diese Breite (oder Höhe bei einem horizontalen Raster) ein:

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()
)
Lazy-Staggered-Raster von Bildern in Compose
Abbildung 2. Beispiel für ein vertikales Lazy-Staggered-Grid mit fester Spaltenanzahl

Inhaltsabstand

Manchmal müssen Sie an den Rändern des Inhalts einen Innenabstand hinzufügen. Mit den Lazy-Komponenten können Sie einige PaddingValues an den Parameter contentPadding übergeben, um dies zu unterstützen:

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

In diesem Beispiel fügen wir den horizontalen Rändern (links und rechts) einen Abstand von 16.dp und dem oberen und unteren Rand des Inhalts einen Abstand von 8.dp hinzu.

Dieser Innenabstand wird auf den Inhalt und nicht auf das LazyColumn selbst angewendet. Im obigen Beispiel wird dem ersten Element 8.dp-Abstand oben, dem letzten Element 8.dp-Abstand unten und allen Elementen 16.dp-Abstand links und rechts hinzugefügt.

Ein weiteres Beispiel: Sie können die PaddingValues von Scaffold an die contentPadding von LazyColumn übergeben. Weitere Informationen findest du im Edge-to-Edge-Leitfaden.

Abstand zwischen Inhalten

Wenn Sie Abstände zwischen Elementen einfügen möchten, können Sie Arrangement.spacedBy() verwenden. Im folgenden Beispiel wird zwischen den einzelnen Elementen ein Abstand von 4.dp hinzugefügt:

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

Ähnlich für LazyRow:

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

Raster hingegen akzeptieren sowohl vertikale als auch horizontale Anordnungen:

LazyVerticalGrid(
    columns = GridCells.Fixed(2),
    verticalArrangement = Arrangement.spacedBy(16.dp),
    horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
    items(photos) { item ->
        PhotoItem(item)
    }
}

Schlüssel für das Feedelement

Standardmäßig wird der Status jedes Elements anhand der Position des Elements in der Liste oder im Raster bestimmt. Dies kann jedoch zu Problemen führen, wenn sich das Dataset ändert, da Elemente, die ihre Position ändern, effektiv jeden gespeicherten Status verlieren. Stellen Sie sich das Szenario von LazyRow innerhalb von LazyColumn vor: Wenn sich die Elementposition der Zeile ändert, verliert der Nutzer seine Scrollposition innerhalb der Zeile.

Um dies zu verhindern, können Sie für jedes Element einen stabilen und eindeutigen Schlüssel angeben, indem Sie einen Block für den Parameter key bereitstellen. Wenn Sie einen stabilen Schlüssel bereitstellen, kann der Elementstatus bei Dataset-Änderungen konsistent bleiben:

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

Durch die Angabe von Schlüsseln kann Compose Neuanordnungen korrekt verarbeiten. Wenn Ihr Element beispielsweise einen gespeicherten Status enthält, können Sie mit dem Festlegen von Schlüsseln dafür sorgen, dass Compose diesen Status zusammen mit dem Element verschiebt, wenn sich seine Position ändert.

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = remember {
            Random.nextInt()
        }
    }
}

Es gibt jedoch eine Einschränkung, welche Typen Sie als Elementschlüssel verwenden können. Der Typ des Schlüssels muss von Bundle unterstützt werden, dem Android-Mechanismus zum Beibehalten der Status, wenn die Aktivität neu erstellt wird. Bundle unterstützt Typen wie Primitive, Enums oder Parcelables.

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

Der Schlüssel muss von Bundle unterstützt werden, damit der rememberSaveable im composable-Element wiederhergestellt werden kann, wenn die Aktivität neu erstellt wird oder wenn Sie von diesem Element weg- und wieder zurückscrollen.

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = rememberSaveable {
            Random.nextInt()
        }
    }
}

Elementanimationen

Wenn Sie das RecyclerView-Widget verwendet haben, wissen Sie, dass es Elementänderungen automatisch animiert. Lazy-Layouts bieten dieselbe Funktionalität für die Neuanordnung von Elementen. Die API ist einfach – Sie müssen nur den Modifikator animateItem für den Elementinhalt festlegen:

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

Sie können sogar eine benutzerdefinierte Animationsspezifikation angeben.

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

Stellen Sie sicher, dass Sie für Ihre Elemente Schlüssel festlegen, damit die neue Position des verschobenen Elements gefunden werden kann.

Beispiel: Elemente in Lazy Lists animieren

Mit Compose können Sie Änderungen an Elementen in Lazy Lists animieren. In Kombination implementieren die folgenden Snippets Animationen beim Hinzufügen, Entfernen und Neuanordnen von Elementen in Lazy Lists.

In diesem Snippet wird eine Liste von Strings mit animierten Übergängen angezeigt, wenn Elemente hinzugefügt, entfernt oder neu angeordnet werden:

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

Wichtige Punkte zum Code

  • ListAnimatedItems zeigt eine Liste von Strings in einem LazyColumn mit animierten Übergängen an, wenn Elemente geändert werden.
  • Die Funktion items weist jedem Element in der Liste einen eindeutigen Schlüssel zu. Compose verwendet die Schlüssel, um die Elemente zu verfolgen und Änderungen an ihren Positionen zu erkennen.
  • Mit ListItem wird das Layout der einzelnen Listenelemente definiert. Es wird ein headlineContent-Parameter verwendet, der den Hauptinhalt des Elements definiert.
  • Mit dem Modifikator animateItem werden Standardanimationen auf das Hinzufügen, Entfernen und Verschieben von Elementen angewendet.

Das folgende Snippet zeigt einen Bildschirm mit Steuerelementen zum Hinzufügen und Entfernen von Elementen sowie zum Sortieren einer vordefinierten Liste:

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

Wichtige Punkte zum Code

  • ListAnimatedItemsExample zeigt einen Bildschirm mit Steuerelementen zum Hinzufügen, Entfernen und Sortieren von Elementen an.
    • onAddItem und onRemoveItem sind Lambda-Ausdrücke, die an AddRemoveButtons übergeben werden, um Elemente zur Liste hinzuzufügen und daraus zu entfernen.
    • resetOrder, onSortAlphabetically und onSortByLength sind Lambda-Ausdrücke, die an OrderButtons übergeben werden, um die Reihenfolge der Elemente in der Liste zu ändern.
  • AddRemoveButtons zeigt die Schaltflächen „Hinzufügen“ und „Entfernen“ an. Sie aktiviert/deaktiviert die Schaltflächen und verarbeitet Schaltflächenklicks.
  • OrderButtons zeigt die Schaltflächen zum Neuanordnen der Liste an. Sie empfängt die Lambda-Funktionen zum Zurücksetzen der Reihenfolge und zum Sortieren der Liste nach Länge oder alphabetisch.
  • ListAnimatedItems ruft die kombinierbare Funktion ListAnimatedItems auf und übergibt die Liste data, um die animierte Liste von Strings anzuzeigen. data ist an anderer Stelle definiert.

Mit diesem Snippet wird eine Benutzeroberfläche mit den Schaltflächen Element hinzufügen und Element löschen erstellt:

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

Wichtige Punkte zum Code

  • AddRemoveButtons zeigt eine Reihe von Schaltflächen an, mit denen Sie Elemente in der Liste hinzufügen und entfernen können.
  • Die Parameter canAddItem und canRemoveItem steuern den Aktivierungsstatus der Schaltflächen. Wenn canAddItem oder canRemoveItem „false“ ist, wird die entsprechende Schaltfläche deaktiviert.
  • Die Parameter onAddItem und onRemoveItem sind Lambdas, die ausgeführt werden, wenn der Nutzer auf den entsprechenden Button klickt.

Schließlich werden in diesem Snippet drei Buttons zum Sortieren der Liste angezeigt (Zurücksetzen, Alphabetisch und Länge):

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

Wichtige Punkte zum Code

  • OrderButtons zeigt ein SingleChoiceSegmentedButtonRow an, über das Nutzer eine Sortiermethode für die Liste auswählen oder die Reihenfolge der Liste zurücksetzen können. Mit einer SegmentedButton-Komponente können Sie eine einzelne Option aus einer Liste von Optionen auswählen.
  • resetOrder, orderAlphabetically und orderByLength sind Lambda-Funktionen, die ausgeführt werden, wenn der entsprechende Button ausgewählt wird.
  • Die Zustandsvariable selectedIndex verfolgt die ausgewählte Option.

Ergebnis

Dieses Video zeigt das Ergebnis der vorherigen Snippets, wenn Elemente neu angeordnet werden:

Abbildung 1: Eine Liste, in der Übergänge zwischen Elementen animiert werden, wenn Elemente hinzugefügt, entfernt oder sortiert werden.

Fixierte Kopfzeilen

Das Muster „Fixierte Kopfzeile“ ist hilfreich, wenn Listen mit gruppierten Daten angezeigt werden. Unten sehen Sie ein Beispiel für eine Kontaktliste, die nach dem Anfangsbuchstaben der einzelnen Kontakte gruppiert ist:

Video eines Smartphones, auf dem eine Kontaktliste nach oben und unten gescrollt wird

Wenn Sie einen Sticky Header mit LazyColumn erstellen möchten, können Sie die stickyHeader() verwenden.

Funktion auf und geben Sie den Header-Inhalt an:

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

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

Wenn Sie eine Liste mit mehreren Headern erstellen möchten, wie im Beispiel „Kontaktliste“ oben, können Sie so vorgehen:

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

Auf Scrollposition reagieren

Viele Apps müssen auf Änderungen der Scrollposition und des Elementlayouts reagieren und diese beobachten. Die Lazy-Komponenten unterstützen diesen Anwendungsfall, indem sie den LazyListState hochstufen:

@Composable
fun MessageList(messages: List<Message>) {
    // Remember our own LazyListState
    val listState = rememberLazyListState()

    // Provide it to LazyColumn
    LazyColumn(state = listState) {
        // ...
    }
}

In einfachen Anwendungsfällen benötigen Apps in der Regel nur Informationen zum ersten sichtbaren Element. Dafür stellt LazyListState die Properties firstVisibleItemIndex und firstVisibleItemScrollOffset bereit.

Sehen wir uns das Beispiel an, bei dem eine Schaltfläche ein- und ausgeblendet wird, je nachdem, ob der Nutzer am ersten Element vorbeigescrollt hat:

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

Das direkte Lesen des Status in der Komposition ist nützlich, wenn Sie andere UI-Kompositionen aktualisieren müssen. Es gibt jedoch auch Szenarien, in denen das Ereignis nicht in derselben Komposition verarbeitet werden muss. Ein gängiges Beispiel hierfür ist das Senden eines Analyseereignisses, sobald der Nutzer über einen bestimmten Punkt hinausgescrollt hat. Um dies effizient zu handhaben, können wir einen snapshotFlow() verwenden:

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

LazyListState stellt über das Attribut layoutInfo auch Informationen zu allen aktuell angezeigten Elementen und ihren Grenzen auf dem Bildschirm bereit. Weitere Informationen finden Sie in der Klasse LazyListLayoutInfo.

Scrollposition steuern

Es ist nicht nur wichtig, dass Apps auf die Scrollposition reagieren, sondern auch, dass sie die Scrollposition steuern können. LazyListState unterstützt dies über die Funktion scrollToItem(), die die Scrollposition „sofort“ ändert, und animateScrollToItem(), die mit einer Animation scrollt (auch als „optimiertes Scrollen“ bezeichnet):

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

Große Datasets (Paging)

Mit der Paging-Bibliothek können Apps große Listen von Elementen unterstützen und bei Bedarf kleine Teile der Liste laden und anzeigen. Paging 3.0 und höher bietet Compose-Unterstützung über die androidx.paging:paging-compose-Bibliothek.

Wenn Sie eine Liste mit paginierten Inhalten anzeigen möchten, können Sie die Erweiterungsfunktion collectAsLazyPagingItems() verwenden und dann den zurückgegebenen LazyPagingItems an items() in Ihrer LazyColumn übergeben. Ähnlich wie bei der Paging-Unterstützung in Ansichten können Sie Platzhalter anzeigen, während Daten geladen werden. Prüfen Sie dazu, ob item den Wert null hat:

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

Tipps zur Verwendung von Lazy-Layouts

Es gibt einige Tipps, die Sie beachten können, damit Ihre Lazy Layouts wie vorgesehen funktionieren.

Vermeiden Sie die Verwendung von Elementen mit einer Größe von 0 Pixeln.

Das kann beispielsweise passieren, wenn Sie erwarten, dass Sie einige Daten wie Bilder asynchron abrufen, um die Elemente Ihrer Liste zu einem späteren Zeitpunkt zu füllen. Dadurch würde das Lazy Layout alle Elemente in der ersten Messung zusammensetzen, da ihre Höhe 0 Pixel beträgt und alle in den Darstellungsbereich passen. Sobald die Elemente geladen und ihre Höhe erweitert wurde, werden alle anderen Elemente, die beim ersten Mal unnötigerweise zusammengesetzt wurden, von Lazy-Layouts verworfen, da sie nicht in den Darstellungsbereich passen. Um dies zu vermeiden, sollten Sie für Ihre Elemente eine Standardgröße festlegen, damit das Lazy Layout richtig berechnen kann, wie viele Elemente tatsächlich in den Viewport passen:

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

Wenn ihr die ungefähre Größe eurer Elemente nach dem asynchronen Laden der Daten kennt, solltet ihr dafür sorgen, dass die Größe der Elemente vor und nach dem Laden gleich bleibt, z. B. durch Hinzufügen von Platzhaltern. So wird die richtige Scrollposition beibehalten.

Vermeiden Sie es, Komponenten zu verschachteln, die in dieselbe Richtung scrollbar sind.

Dies gilt nur für Fälle, in denen verschachtelte untergeordnete Elemente ohne vordefinierte Größe in einem anderen übergeordneten Element mit Scrollfunktion in derselben Richtung verschachtelt sind. Beispiel: Versuch, ein untergeordnetes LazyColumn ohne feste Höhe in ein vertikal scrollbares Column-Übergeordnetes Element einzufügen.

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

Stattdessen kann dasselbe Ergebnis erzielt werden, indem Sie alle kombinierbaren Funktionen in ein übergeordnetes LazyColumn einfügen und dessen DSL verwenden, um verschiedene Arten von Inhalten zu übergeben. So können Sie einzelne Elemente sowie mehrere Listenelemente an einem Ort ausgeben.

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

Beachten Sie, dass Fälle, in denen Sie verschiedene Richtungslayouts verschachteln, z. B. ein scrollbares übergeordnetes Row und ein untergeordnetes LazyColumn, zulässig sind,

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

sowie Fälle, in denen Sie weiterhin dieselben Richtungslayouts verwenden, aber auch eine feste Größe für die verschachtelten untergeordneten Elemente festlegen.

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

Vermeiden Sie es, mehrere Elemente in einem Element zu platzieren

In diesem Beispiel gibt das zweite Element-Lambda zwei Elemente in einem Block aus:

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

Lazy Layouts verarbeiten dies wie erwartet: Sie ordnen Elemente nacheinander an, als wären sie unterschiedliche Elemente. Dabei gibt es jedoch einige Probleme.

Wenn mehrere Elemente als Teil eines Elements ausgegeben werden, werden sie als eine Einheit behandelt. Das bedeutet, dass sie nicht mehr einzeln zusammengesetzt werden können. Wenn ein Element auf dem Bildschirm sichtbar wird, müssen alle Elemente, die dem Artikel entsprechen, zusammengesetzt und gemessen werden. Bei übermäßigem Gebrauch kann dies die Leistung beeinträchtigen. Wenn Sie alle Elemente in ein Element einfügen, wird der Zweck der Verwendung von Lazy Layouts vollständig zunichte gemacht. Abgesehen von potenziellen Leistungsproblemen beeinträchtigt das Einfügen mehrerer Elemente in ein Element auch scrollToItem() und animateScrollToItem().

Es gibt jedoch gültige Anwendungsfälle, in denen mehrere Elemente in einem Element platziert werden, z. B. Trennlinien in einer Liste. Trennlinien sollten nicht die Scroll-Indizes ändern, da sie nicht als eigenständige Elemente betrachtet werden sollten. Außerdem wird die Leistung nicht beeinträchtigt, da die Trennlinien klein sind. Ein Trennelement muss wahrscheinlich sichtbar sein, wenn das Element davor sichtbar ist. Es kann also Teil des vorherigen Elements sein:

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

Benutzerdefinierte Anordnungen verwenden

In der Regel enthalten Lazy-Listen viele Elemente und belegen mehr Platz als der Scrollcontainer. Wenn Ihre Liste jedoch nur wenige Elemente enthält, kann Ihr Design spezifischere Anforderungen an die Positionierung dieser Elemente im Darstellungsbereich haben.

Dazu können Sie eine benutzerdefiniertes vertikales Arrangement verwenden und an LazyColumn übergeben. Im folgenden Beispiel muss das TopWithFooter-Objekt nur die arrange-Methode implementieren. Erstens werden Elemente nacheinander positioniert. Zweitens: Wenn die insgesamt verwendete Höhe geringer als die Höhe des Viewports ist, wird die Fußzeile unten positioniert:

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

Füge am besten contentType hinzu

Ab Compose 1.2 können Sie die Leistung Ihres Lazy Layouts maximieren, indem Sie Ihren Listen oder Grids contentType hinzufügen. So können Sie den Inhaltstyp für jedes Element des Layouts angeben, wenn Sie eine Liste oder ein Raster aus mehreren verschiedenen Elementtypen erstellen:

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

Wenn Sie contentType angeben, kann Compose Kompositionen nur zwischen Elementen desselben Typs wiederverwenden. Die Wiederverwendung ist effizienter, wenn Sie Elemente mit ähnlicher Struktur erstellen. Durch die Angabe der Inhaltstypen wird sichergestellt, dass Compose nicht versucht, ein Element vom Typ A auf ein völlig anderes Element vom Typ B zu setzen. So können Sie die Vorteile der Kompositionswiederverwendung und die Leistung Ihres Lazy Layouts maximieren.

Leistung messen

Die Leistung eines Lazy Layouts lässt sich nur zuverlässig messen, wenn es im Release-Modus und mit aktivierter R8-Optimierung ausgeführt wird. In Debug-Builds kann das Scrollen in Lazy Layouts langsamer erscheinen. Weitere Informationen finden Sie unter Compose-Leistung.

Zusätzliche Ressourcen