列表

许多应用都需要显示列表项集合。本文档将介绍如何在 Jetpack Compose 中高效地执行此操作。

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

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

我们可以使用 verticalScroll() 修饰符使 Column 可滚动。如需了解详情,请参阅手势文档。

延迟可组合项

系统会对所有列表项进行组合和布局,无论它们是否可见,因此如果您需要显示大量列表项(或长度未知的列表),则使用 Column 等布局可能会导致性能问题。

Compose 提供了一组组件,这些组件只会对在组件视口中可见的列表项进行组合和布局。这些组件包括 LazyColumnLazyRow

顾名思义,LazyColumnLazyRow 之间的区别就在于它们的列表项布局和滚动方向不同。LazyColumn 生成的是垂直滚动列表,而 LazyRow 生成的是水平滚动列表。

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

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

@Composable
fun MessageList(messages: List<Message>) {
    LazyColumn {
        items(messages) { message ->
            MessageRow(message)
        }
    }
}

还有一个名为 itemsIndexed()items() 扩展函数的变体,用于提供索引。如需了解详情,请参阅 LazyListScope 参考文档。

内容内边距

有时,您需要围绕内容边缘添加内边距。借助延迟组件,您可以将一些 PaddingValues 传递给 contentPadding 参数以支持此功能:

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

在此示例中,我们将 16.dp 内边距添加到水平边缘(左侧和右侧),然后将 8.dp 添加到内容的顶部和底部。

请注意,此内边距会应用于内容,而不是 LazyColumn 本身。在上面的示例中,第一个项会在顶部添加 8.dp 内边距,最后一个项会在底部添加 8.dp,且所有项的左侧和右侧都会使用 16.dp 内边距。

内容间距

如需在列表项之间添加间距,可以使用 Arrangement.spacedBy()。以下示例在每个列表项之间添加了 4.dp 的间距:

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

LazyRow 也可进行类似的操作。

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

项动画

如果您使用过 RecyclerView 微件,便会知道它会自动为列表项更改添加动画效果。不过,延迟布局尚未提供该功能,这意味着列表项更改会导致系统瞬时“中断”。您可以跟进此错误,以跟踪此功能的任何更改。

粘性标题(实验性)

“粘性标题”模式在显示分组数据列表时非常有用。 下面显示的是“联系人列表”示例,其中的数据按照每个联系人的姓名首字母分组:

视频:在手机中上下滚动联系人列表

如需使用 LazyColumn 实现粘性标题,可以使用实验性 stickyHeader() 函数,该函数提供以下标题内容:

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

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

如需实现具有多个标题的列表(如上面的“联系人列表”示例),可以执行以下操作:

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

网格(实验性)

LazyVerticalGrid 可组合项为在网格中显示内容提供实验性支持。

屏幕截图:在手机中显示照片网格

cells 参数控制如何将单元格构建为列。以下示例在网格中显示列表项,并使用 GridCells.Adaptive 将每列设置为至少 128.dp 宽:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PhotoGrid(photos: List<Photo>) {
    LazyVerticalGrid(
        cells = GridCells.Adaptive(minSize = 128.dp)
    ) {
        items(photos) { photo ->
            PhotoItem(photo)
        }
    }
}

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

响应滚动位置

许多应用需要对滚动位置和列表项布局更改作出响应,并进行监听。延迟组件通过提升 LazyListState 来支持此用例:

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

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

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

如果我们使用根据用户是否滚动经过第一个列表项来显示和隐藏按钮的示例,那么代码如下:

@OptIn(ExperimentalAnimationApi::class) // AnimatedVisibility
@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()
        }
    }
}

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

val listState = rememberLazyListState()

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

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it == true }
        .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,在加载数据时显示占位符:

import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items

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

    LazyColumn {
        items(lazyPagingItems) { message ->
            if (message != null) {
                MessageRow(message)
            } else {
                MessagePlaceholder()
            }
        }
    }
}

项键

默认情况下,每个列表项的状态均与该项在列表中的位置相对应。但是,如果数据集发生变化,这可能会导致问题,因为位置发生变化的列表项实际上会丢失任何记忆状态。想象一下 LazyColumn 中的 LazyRow 场景,如果某个行更改了项位置,用户将丢失在该行内的滚动位置。

为避免出现此情况,您可以为每个列表项提供一个稳定的唯一键,为 key 参数提供一个块。提供稳定的键可使项状态在发生数据集更改后保持一致:

@Composable
fun MessageList(messages: List<Message>) {
    LazyColumn {
        items(
            items = messages,
            key = { message ->
                // Return a stable + unique key for the item
                message.id
            }
        ) { message ->
            MessageRow(message)
        }
    }
}