列表和网格

许多应用需要显示项目集合。本文档将解释如何在 Jetpack Compose 中高效地完成此操作。

如果您知道您的用例不需要任何滚动,您可能希望使用一个简单的 ColumnRow(取决于方向),并通过迭代列表的方式发出每个项目的内容,如下所示

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

我们可以通过使用 verticalScroll() 修饰符使 Column 可滚动。

Lazy 列表

如果您需要显示大量项目(或长度未知的列表),使用 Column 等布局可能会导致性能问题,因为所有项目无论是否可见都会被组合和布局。

Compose 提供了一组组件,这些组件只组合和布局在组件视口中可见的项目。这些组件包括 LazyColumnLazyRow

顾名思义,LazyColumnLazyRow 的区别在于它们布局项目和滚动的方向。LazyColumn 生成一个垂直滚动的列表,而 LazyRow 生成一个水平滚动的列表。

Lazy 组件与 Compose 中的大多数布局不同。它们不接受 @Composable 内容块参数,允许应用直接发出可组合项,而是提供 LazyListScope.() 块。此 LazyListScope 块提供了一个 DSL,允许应用描述项目内容。Lazy 组件然后负责根据布局和滚动位置的需要添加每个项目的内容。

LazyListScope DSL

LazyListScope 的 DSL 提供了许多函数来描述布局中的项目。最基本的是,item() 添加单个项目,而 items(Int) 添加多个项目

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

还有许多扩展函数允许您添加项目集合,例如 List。这些扩展允许我们轻松迁移上面的 Column 示例

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

还有 items() 扩展函数的一个变体叫做 itemsIndexed(),它提供索引。更多详细信息请参阅 LazyListScope 参考文档。

Lazy 网格

LazyVerticalGridLazyHorizontalGrid 可组合项支持以网格形式显示项目。Lazy 垂直网格将在垂直可滚动容器中显示其项目,跨多列排列,而 Lazy 水平网格将在水平轴上具有相同的行为。

网格具有与列表相同的强大 API 功能,并且它们也使用非常相似的 DSL - LazyGridScope.() 来描述内容。

Screenshot of a phone showing a grid of photos

LazyVerticalGrid 中的 columns 参数和 LazyHorizontalGrid 中的 rows 参数控制单元格如何形成列或行。以下示例使用 GridCells.Adaptive 在网格中显示项目,将每列设置为至少 128.dp

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

LazyVerticalGrid 允许您为项目指定宽度,然后网格将尽可能多地容纳列。在计算出列数后,任何剩余宽度都将平均分配给各列。这种自适应大小调整方式在跨不同屏幕尺寸显示项目集时特别有用。

如果您知道要使用的确切列数,则可以提供包含所需列数的 GridCells.Fixed 实例。

如果您的设计只要求某些项目具有非标准尺寸,您可以使用网格支持为项目提供自定义列跨度。使用 LazyGridScope DSLitemitems 方法的 span 参数指定列跨度。maxLineSpan 是跨度范围值之一,当您使用自适应尺寸调整时特别有用,因为列数不固定。此示例显示如何提供完整的行跨度

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

Lazy 交错网格

LazyVerticalStaggeredGridLazyHorizontalStaggeredGrid 是可组合项,允许您创建延迟加载的交错项目网格。Lazy 垂直交错网格在垂直可滚动容器中显示其项目,跨多列,并允许单个项目具有不同的高度。Lazy 水平网格在水平轴上具有相同的行为,项目具有不同的宽度。

以下代码段是使用 LazyVerticalStaggeredGrid 的基本示例,每项宽度为 200.dp

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

图 1. Lazy 交错垂直网格示例

要设置固定的列数,您可以使用 StaggeredGridCells.Fixed(columns) 而不是 StaggeredGridCells.Adaptive。这将可用宽度除以列数(或水平网格的行数),并使每个项目占用该宽度(或水平网格的高度)

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 of images in Compose
图 2. 带固定列的 Lazy 交错垂直网格示例

内容内边距

有时您需要在内容边缘添加内边距。Lazy 组件允许您将一些 PaddingValues 传递给 contentPadding 参数以支持此功能

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

在此示例中,我们在水平边缘(左和右)添加 16.dp 的内边距,然后向上和向下添加 8.dp 的内边距。

请注意,此填充应用于内容,而不是 LazyColumn 本身。在上面的示例中,第一个项目将为其顶部添加 8.dp 填充,最后一个项目将为其底部添加 8.dp,所有项目将在左侧和右侧具有 16.dp 填充。

作为另一个例子,您可以将 ScaffoldPaddingValues 传递给 LazyColumncontentPadding。请参阅边缘到边缘指南。

内容间距

要在项目之间添加间距,您可以使用 Arrangement.spacedBy()。以下示例在每个项目之间添加了 4.dp 的空间

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

对于 LazyRow 也是如此

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

然而,网格接受垂直和水平排列

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

项目键

默认情况下,每个项目的状态都以项目在列表或网格中的位置作为键。然而,如果数据集发生变化,这可能会导致问题,因为位置发生变化的项目实际上会丢失任何记住的状态。如果您想象一个 LazyColumn 中包含 LazyRow 的场景,如果行更改了项目位置,用户将失去该行中的滚动位置。

为了解决这个问题,您可以为每个项目提供一个稳定且唯一的键,向 key 参数提供一个块。提供稳定键可确保项目状态在数据集更改时保持一致

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

通过提供键,您可以帮助 Compose 正确处理重新排序。例如,如果您的项目包含记住的状态,设置键将允许 Compose 在项目位置更改时将此状态与项目一起移动。

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

但是,对可以用作项目键的类型有一个限制。键的类型必须由 Bundle 支持,Bundle 是 Android 在 Activity 重新创建时保持状态的机制。Bundle 支持基本类型、枚举或 Parcelables 等类型。

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

键必须受 Bundle 支持,以便当 Activity 重新创建时,甚至当您从该项目滚动离开并重新滚动回来时,项目可组合项内的 rememberSaveable 都可以恢复。

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

项目动画

如果您使用过 RecyclerView 微件,您会知道它会自动为项目更改添加动画。Lazy 布局为项目重新排序提供了相同的功能。API 很简单 - 您只需要将 animateItem 修饰符设置到项目内容

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

如果需要,您甚至可以提供自定义动画规范

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

请务必为您的项目提供键,以便可以找到移动元素的新位置。

示例:在 Lazy 列表中为项目添加动画

通过 Compose,您可以为 Lazy 列表中项目的更改添加动画。当一起使用时,以下代码片段实现了添加、删除和重新排序 Lazy 列表项目时的动画。

此代码段显示了一个字符串列表,当项目被添加、删除或重新排序时,会带有动画过渡

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

代码要点

  • ListAnimatedItemsLazyColumn 中显示一个字符串列表,并在项目修改时带有动画过渡。
  • items 函数为列表中的每个项目分配一个唯一的键。Compose 使用这些键来跟踪项目并识别其位置的变化。
  • ListItem 定义了每个列表项目的布局。它接受一个 headlineContent 参数,该参数定义了项目的主要内容。
  • animateItem 修饰符将默认动画应用于项目的添加、删除和移动。

以下代码片段呈现了一个屏幕,其中包含添加和删除项目的控件,以及对预定义列表进行排序的控件

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

代码要点

  • ListAnimatedItemsExample 显示了一个屏幕,其中包含添加、删除和排序项目的控件。
    • onAddItemonRemoveItem 是传递给 AddRemoveButtons 的 lambda 表达式,用于从列表中添加和删除项目。
    • resetOrderonSortAlphabeticallyonSortByLength 是传递给 OrderButtons 的 lambda 表达式,用于更改列表中项目的顺序。
  • AddRemoveButtons 显示“添加”和“移除”按钮。它启用/禁用按钮并处理按钮点击。
  • OrderButtons 显示用于重新排序列表的按钮。它接收用于重置顺序和按长度或字母顺序排序列表的 lambda 函数。
  • ListAnimatedItems 调用 ListAnimatedItems 可组合项,传入 data 列表以显示字符串的动画列表。data 在其他地方定义。

此代码段创建了一个包含 添加项目删除项目 按钮的 UI

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

代码要点

  • AddRemoveButtons 显示一排按钮,用于对列表执行添加和删除操作。
  • canAddItemcanRemoveItem 参数控制按钮的启用状态。如果 canAddItemcanRemoveItem 为 false,则相应的按钮将被禁用。
  • onAddItemonRemoveItem 参数是当用户点击相应按钮时执行的 lambda。

最后,此代码段显示了三个用于排序列表的按钮(重置、按字母顺序按长度

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

代码要点

  • OrderButtons 显示一个 SingleChoiceSegmentedButtonRow,允许用户选择列表的排序方法或重置列表顺序。 SegmentedButton 组件允许您从选项列表中选择单个选项。
  • resetOrderorderAlphabeticallyorderByLength 是当选中相应按钮时执行的 lambda 函数。
  • selectedIndex 状态变量跟踪选定的选项。

结果

此视频显示了前面代码片段在项目重新排序时的结果

图 1. 当项目被添加、删除或排序时,会显示动画过渡的列表。

粘性标题(实验性)

“粘性标题”模式在显示分组数据列表时很有用。下面是按联系人姓名首字母分组的“联系人列表”示例

Video of a phone scrolling up and down through a contacts list

要使用 LazyColumn 实现粘性标题,您可以使用实验性的 stickyHeader() 函数,提供标题内容

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

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

要实现带有多个标题的列表,例如上面的“联系人列表”示例,您可以这样做

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

响应滚动位置

许多应用需要响应和监听滚动位置和项目布局变化。Lazy 组件通过提升 LazyListState 来支持此用例

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

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

对于简单的用例,应用通常只需要了解有关第一个可见项目的信息。为此,LazyListState 提供了 firstVisibleItemIndexfirstVisibleItemScrollOffset 属性。

如果我们以显示和隐藏按钮为例,根据用户是否已滚动过第一个项目来决定

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

直接在组合中读取状态在需要更新其他 UI 可组合项时很有用,但在某些情况下,事件不需要在同一组合中处理。一个常见的例子是当用户滚动过某个点后发送分析事件。为了高效地处理此问题,我们可以使用 snapshotFlow()

val listState = rememberLazyListState()

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

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

LazyListState 还通过 layoutInfo 属性提供有关当前显示的所有项目及其屏幕边界的信息。有关更多信息,请参阅 LazyListLayoutInfo 类。

控制滚动位置

除了响应滚动位置外,应用能够控制滚动位置也很有用。LazyListState 通过 scrollToItem() 函数支持此功能,该函数“立即”调整滚动位置,以及 animateScrollToItem(),该函数使用动画(也称为平滑滚动)进行滚动

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

大数据集(分页)

Paging 库使应用能够支持大量项目列表,并根据需要加载和显示列表的小块。Paging 3.0 及更高版本通过 androidx.paging:paging-compose 库提供 Compose 支持。

要显示分页内容列表,我们可以使用 collectAsLazyPagingItems() 扩展函数,然后将返回的 LazyPagingItems 传递给 LazyColumn 中的 items()。与视图中的 Paging 支持类似,您可以通过检查 item 是否为 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()
            }
        }
    }
}

使用 Lazy 布局的技巧

您可以考虑以下一些技巧,以确保您的 Lazy 布局按预期工作。

避免使用 0 像素大小的项目

这可能发生在以下场景中:例如,您期望异步检索一些数据(如图像)以便稍后填充列表项目。这将导致 Lazy 布局在第一次测量时组合所有项目,因为它们的高度为 0 像素,并且可以全部适应视口。一旦项目加载并其高度扩展,Lazy 布局将丢弃所有在第一次时不必要地组合的其他项目,因为它们实际上无法适应视口。为避免这种情况,您应该为项目设置默认大小,以便 Lazy 布局可以正确计算有多少项目实际上可以适应视口

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

当您知道数据异步加载后项目的近似大小时,一个好的做法是确保项目的大小在加载前后保持不变,例如,通过添加一些占位符。这将有助于保持正确的滚动位置。

避免在同一方向上嵌套可滚动组件

这仅适用于在另一个同一方向可滚动父级中嵌套没有预定义大小的可滚动子级的情况。例如,尝试在垂直可滚动的 Column 父级中嵌套没有固定高度的子 LazyColumn

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

相反,可以通过将所有可组合项包装在一个父 LazyColumn 中并使用其 DSL 传入不同类型的内容来实现相同的效果。这使得可以在一个地方发出单个项目以及多个列表项目

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

请记住,嵌套不同方向布局(例如,可滚动的父 Row 和子 LazyColumn)的情况是允许的

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

以及您仍然使用相同方向布局但为嵌套子级设置固定大小的情况

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

请注意将多个元素放入一个项目

在此示例中,第二个项目 lambda 在一个块中发出 2 个项目

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

Lazy 布局将按预期处理此问题 - 它们将一个接一个地布局元素,就好像它们是不同的项目一样。然而,这样做存在几个问题。

当多个元素作为单个项目的一部分发出时,它们被视为一个实体,这意味着它们不能再单独组合。如果一个元素在屏幕上可见,那么对应于该项目的所有元素都必须被组合和测量。如果过度使用,这会损害性能。在将所有元素放在一个项目中的极端情况下,它完全违背了使用 Lazy 布局的目的。除了潜在的性能问题之外,在一个项目中放置更多元素还会干扰 scrollToItem()animateScrollToItem()

然而,将多个元素放在一个项目中有合理的用例,例如在列表中放置分隔符。您不希望分隔符更改滚动索引,因为它们不应被视为独立元素。此外,由于分隔符很小,性能不会受到影响。分隔符很可能需要在其之前的项目可见时可见,因此它们可以作为上一个项目的一部分

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

考虑使用自定义排列

通常,Lazy 列表有许多项目,它们占用的空间超过了滚动容器的大小。然而,当您的列表只包含少量项目时,您的设计可能对这些项目在视口中的位置有更具体的要求。

要实现此目的,您可以使用自定义垂直 Arrangement 并将其传递给 LazyColumn。在以下示例中,TopWithFooter 对象只需实现 arrange 方法。首先,它会一个接一个地定位项目。其次,如果总使用高度低于视口高度,它会将页脚定位在底部

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

考虑添加 contentType

从 Compose 1.2 开始,为了最大化您的 Lazy 布局的性能,请考虑为您的列表或网格添加 contentType。这允许您在组合由多种不同类型的项目组成的列表或网格时,为布局的每个项目指定内容类型

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

当您提供 contentType 时,Compose 能够仅在相同类型的项目之间重用组合。由于在组合具有相似结构的项目时重用效率更高,因此提供内容类型可确保 Compose 不会尝试将类型 A 的项目组合在完全不同类型 B 的项目之上。这有助于最大限度地发挥组合重用的优势和 Lazy 布局的性能。

测量性能

您只能在发布模式下并启用 R8 优化时可靠地测量 Lazy 布局的性能。在调试构建上,Lazy 布局滚动可能会显得较慢。有关此内容的更多信息,请阅读Compose 性能

其他资源