Listen und Raster

Viele Apps müssen Sammlungen von Elementen anzeigen. 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 ein einfaches 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 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 unabhängig davon, ob sie sichtbar sind, zusammengesetzt und angeordnet werden.

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 scrollende Liste und LazyRow eine horizontal scrollende Liste.

Die Lazy-Komponenten unterscheiden sich von den meisten Layouts in Compose. Anstelle eines @Composable-Parameters für Inhaltsblöcke, der es Apps ermöglicht, Composables direkt auszugeben, bieten die Lazy-Komponenten einen LazyListScope.()-Block. Dieser LazyListScope-Block bietet eine DSL, mit der Apps den Inhalt des Artikels beschreiben können. Die Lazy-Komponente ist dann dafür verantwortlich, die Inhalte der einzelnen Elemente nach Bedarf 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 wie List hinzufügen können. Mit diesen Erweiterungen können wir das obige Beispiel Column 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 Grids

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.

Grids 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

Mit dem Parameter columns in LazyVerticalGrid und dem Parameter rows in LazyHorizontalGrid wird gesteuert, wie Zellen in Spalten oder Zeilen angeordnet werden. Im folgenden Beispiel werden Elemente in einem Raster dargestellt. Dabei wird GridCells.Adaptive verwendet, um die Breite jeder Spalte auf 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 ein. Die verbleibende Breite wird gleichmäßig auf die Spalten verteilt, nachdem die Anzahl der Spalten berechnet wurde. Diese adaptive Art der Größenanpassung ist besonders nützlich, um Gruppen von Elementen auf verschiedenen Bildschirmgrößen darzustellen.

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 Ihr Design nur für bestimmte Elemente nicht standardmäßige Abmessungen erfordert, können Sie das Raster verwenden, um benutzerdefinierte Spannen für Elemente anzugeben. 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 verzögert geladenes, gestaffeltes Raster von Elementen erstellen können. In einem vertikalen, gestaffelten Lazy-Grid werden die Elemente in einem vertikal scrollbaren Container angezeigt, der sich über mehrere Spalten erstreckt und in dem die einzelnen Elemente unterschiedliche Höhen haben können. Lazy Horizontal Grids verhalten sich auf der horizontalen Achse bei Elementen mit unterschiedlichen Breiten gleich.

Das folgende Snippet ist ein einfaches Beispiel für die Verwendung von LazyVerticalStaggeredGrid mit einer 200.dp-Breite 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 Raster mit verzögerter Staffelung

Wenn Sie eine feste Anzahl von Spalten festlegen möchten, können Sie anstelle von StaggeredGridCells.Adaptive auch StaggeredGridCells.Fixed(columns) verwenden. Dabei wird die verfügbare Breite durch die Anzahl der Spalten (oder Zeilen für ein horizontales Raster) geteilt und jedes Element nimmt diese Breite (oder Höhe für ein horizontales 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 Grid von Bildern in Compose
Abbildung 2. Beispiel für ein vertikales Raster mit verzögerter Staffelung und festen Spalten

Innenabstand von Inhalten

Manchmal müssen Sie um die Ränder der Inhalte herum 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 Innenabstand von 16.dp und dem oberen und unteren Rand des Inhalts einen Innenabstand von 8.dp hinzu.

Diese Auffüllung 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 in die contentPadding von LazyColumn übergeben. Weitere Informationen finden Sie im Leitfaden für die Darstellung von Inhalten von Rand zu Rand.

Abstand zwischen Inhalten

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

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

Ähnlich für LazyRow:

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

Bei Rastern sind jedoch sowohl vertikale als auch horizontale Anordnungen möglich:

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

Schlüssel des Feedelements

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

Um dem entgegenzuwirken, können Sie für jedes Element einen stabilen und eindeutigen Schlüssel angeben, der einen Block für den key-Parameter bereitstellt. Wenn Sie einen stabilen Schlüssel angeben, bleibt der Artikelstatus bei Änderungen am Datensatz konsistent:

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 Umordnungen 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 hinsichtlich der Typen, die Sie als Elementschlüssel verwenden können. Der Typ des Schlüssels muss von Bundle unterstützt werden. Das ist der Android-Mechanismus zum Beibehalten der Status, wenn die Aktivität neu erstellt wird. Bundle unterstützt Typen wie Primitives, Enums oder Parcelables.

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

Der Schlüssel muss von Bundle unterstützt werden, damit der rememberSaveable im zusammensetzbaren 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 Änderungen an Elementen automatisch animiert. Lazy Layouts bieten dieselbe Funktionalität für das Neuanordnen von Elementen. Die API ist einfach – Sie müssen nur den Modifikator animateItem für den Artikelinhalt 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, wenn Sie Folgendes benötigen:

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

Sie müssen Schlüssel für Ihre Elemente angeben, damit die neue Position für das verschobene Element gefunden werden kann.

Beispiel: Elemente in Lazy Lists animieren

Mit Compose können Sie Änderungen an Elementen in Lazy-Listen 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.
  • Mit der Funktion items wird jedem Element in der Liste ein eindeutiger Schlüssel zugewiesen. 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 Artikels 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 – Auf dem Bildschirm werden Steuerelemente zum Hinzufügen, Entfernen und Sortieren von Elementen angezeigt.
    • 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.
  • In AddRemoveButtons werden die Schaltflächen „Hinzufügen“ und „Entfernen“ angezeigt. Damit werden die Schaltflächen aktiviert bzw. deaktiviert und Schaltflächenklicks verarbeitet.
  • OrderButtons zeigt die Schaltflächen zum Neuordnen 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 zusammensetzbare 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

  • In AddRemoveButtons wird eine Zeile mit Schaltflächen zum Hinzufügen und Entfernen von Elementen in der Liste angezeigt.
  • Mit den Parametern canAddItem und canRemoveItem wird der aktivierte Status der Schaltflächen gesteuert. 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 die entsprechende Schaltfläche klickt.

Schließlich werden in diesem Snippet drei Schaltflächen 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

  • In OrderButtons wird ein SingleChoiceSegmentedButtonRow angezeigt, damit Nutzer eine Sortiermethode für die Liste auswählen oder die Listenreihenfolge zurücksetzen können. Mit der Komponente A SegmentedButton 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 die entsprechende Schaltfläche ausgewählt wird.
  • Die Statusvariable selectedIndex verfolgt die ausgewählte Option.

Ergebnis

In diesem Video sehen Sie das Ergebnis der vorherigen Snippets, wenn Elemente neu angeordnet werden:

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

Fixierte Header (experimentell)

Das Muster „Fixierter Header“ 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 in einer Kontaktliste nach oben und unten gescrollt wird

Um mit LazyColumn einen fixierten Header zu erstellen, können Sie die experimentelle Funktion stickyHeader() verwenden und den Headerinhalt angeben:

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

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

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

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

Auf die Scrollposition reagieren

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

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

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

Bei einfachen Anwendungsfällen müssen Apps in der Regel nur Informationen zum ersten sichtbaren Element kennen. Dazu stellt LazyListState die Properties firstVisibleItemIndex und firstVisibleItemScrollOffset bereit.

Wenn wir das Beispiel verwenden, in 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-Composables aktualisieren müssen. Es gibt aber auch Szenarien, in denen das Ereignis nicht in derselben Komposition verarbeitet werden muss. Ein häufiges Beispiel hierfür ist das Senden eines Analytics-Ereignisses, sobald der Nutzer einen bestimmten Punkt auf der Seite erreicht hat. Um dies effizient zu handhaben, können wir ein 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 die Eigenschaft layoutInfo auch Informationen zu allen aktuell angezeigten Elementen und deren 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 können, 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 „weiches 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 Library können Apps große Listen von Elementen unterstützen, indem sie 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.

Um eine Liste mit paginiertem Inhalt anzuzeigen, können wir die Erweiterungsfunktion collectAsLazyPagingItems() verwenden und dann das zurückgegebene LazyPagingItems an items() in unserem LazyColumn übergeben. Ähnlich wie bei der Unterstützung der Paginierung in Ansichten können Sie Platzhalter anzeigen, während Daten geladen werden. Prüfen Sie dazu, ob item gleich null ist:

@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.

Verwenden Sie keine Elemente 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 Sie die ungefähre Größe Ihrer Elemente kennen, nachdem die Daten asynchron geladen wurden, sollten Sie dafür sorgen, dass die Größe der Elemente vor und nach dem Laden gleich bleibt. Fügen Sie dazu beispielsweise einige Platzhalter hinzu. So wird die richtige Scrollposition beibehalten.

Verschachtelung von Komponenten vermeiden, die in dieselbe Richtung gescrollt werden können

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

Mehrere Elemente in einem Element vermeiden

In diesem Beispiel gibt die Lambda-Funktion des zweiten Elements zwei Elemente in einem Block aus:

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

Bei Lazy-Layouts wird dies wie erwartet gehandhabt. Die Elemente werden nacheinander angeordnet, als wären es 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 Element 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 für das Einfügen mehrerer Elemente in ein Element, z. B. Trennzeichen in einer Liste. Trennzeichen sollen die Scrollindexe nicht ändern, da sie nicht als unabhängige Elemente betrachtet werden sollten. Da Trennzeichen klein sind, wird die Leistung nicht beeinträchtigt. Eine Trennlinie muss wahrscheinlich sichtbar sein, wenn das Element davor sichtbar ist. Sie kann also Teil des vorherigen Elements sein:

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

Benutzerdefinierte Arrangements verwenden

Lazy Lists haben in der Regel viele Elemente und nehmen mehr Platz ein als der Scrolling-Container. Wenn Ihre Liste nur wenige Elemente enthält, kann es sein, dass Ihr Design spezifischere Anforderungen an die Positionierung dieser Elemente im Viewport hat.

Dazu können Sie eine benutzerdefinierte Branche Arrangement verwenden und sie 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ügen Sie 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