许多应用都需要显示列表项集合。本文档将介绍如何在 Jetpack Compose 中高效地执行此操作。
如果您知道您的用例不需要任何滚动,您可能希望使用简单的 Column 或 Row(具体取决于方向),并通过迭代列表来发出每个列表项的内容,如下所示:
@Composable fun MessageList(messages: List<Message>) { Column { messages.forEach { message -> MessageRow(message) } } }
我们可以使用 verticalScroll() 修饰符使 Column 可滚动。
延迟列表
系统会对所有列表项进行组合和布局,无论它们是否可见,因此如果您需要显示大量列表项(或长度未知的列表),则使用 Column 等布局可能会导致性能问题。
Compose 提供了一组组件,这些组件只会对在组件视口中可见的列表项进行组合和布局。这些组件包括 LazyColumn 和 LazyRow。
从名称可以看出,LazyColumn 和 LazyRow 之间的区别就在于它们的列表项布局和滚动方向不同。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 */ LazyColumn { items(messages) { message -> MessageRow(message) } }
还有一个名为 itemsIndexed() 的 items() 扩展函数的变体,用于提供索引。如需了解详情,请参阅 LazyListScope 参考文档。
延迟网格
LazyVerticalGrid 和 LazyHorizontalGrid 可组合项为在网格中显示列表项提供支持。延迟垂直网格会在可垂直滚动容器中跨多个列显示其列表项,而延迟水平网格则会在水平轴上有相同的行为。
网格与列表具有同样强大的 API 功能,并且它们还使用非常相似的 DSL - LazyGridScope.() 来描述内容。
LazyVerticalGrid 中的 columns 参数和 LazyHorizontalGrid 中的 rows 参数用于控制单元格组成列或行的方式。以下示例在网格中显示列表项,并使用 GridCells.Adaptive 将每列设置为至少 128.dp 宽:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 128.dp) ) { items(photos) { photo -> PhotoItem(photo) } }
通过 LazyVerticalGrid,您可以指定列表项的宽度,然后网格将适应尽可能多的列。计算列数后,系统会将剩余的宽度平均分配给各列。这种自适应尺寸调整方式非常适合在不同尺寸的屏幕上显示多组列表项。
如果您知道要使用的确切列数,则可以改为提供包含所需列数的 GridCells.Fixed 实例。
如果您的设计只需要某些列表项具有非标准尺寸,您可以使用网格支持功能为列表项提供自定义列 span。使用 LazyGridScope DSL item 和 items 方法的 span 参数指定列 span。maxLineSpan 是 span 范围的值之一,在您使用自适应尺寸调整功能时特别有用,因为此情况下列数不固定。以下示例展示了如何提供完整的行 span:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 30.dp) ) { item(span = { // LazyGridItemSpanScope: // maxLineSpan GridItemSpan(maxLineSpan) }) { CategoryCard("Fruits") } // ... }
延迟加载交错网格
LazyVerticalStaggeredGrid 和 LazyHorizontalStaggeredGrid 是可组合项,可让您创建延迟加载的交错网格。延迟垂直交错网格会在可垂直滚动容器中跨多个列显示其列表项,并允许各个项具有不同的高度。延迟水平网格在水平轴上具有相同的行为,但项的宽度不同。
以下代码段是使用 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() )
如需设置固定列数,您可以使用 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() )
使用 LazyLayoutCacheWindow 提升滚动性能
LazyLayoutCacheWindow 是一个 API,旨在帮助提升列表、网格和交错网格中的滚动性能。在 CacheWindow 中,您可以定义一个虚拟空间,在滚动事件期间,可以在其中预提取项(前向窗口)或使项保持活跃状态(后向窗口)。这意味着,项组合和测量会预热内部缓存,当实际需要这些项(当它们变为可见)时,显示它们所需的时间比从头开始构建它们所需的时间要少得多。
所有延迟加载集合都使用 CacheWindow 作为默认预提取策略。默认窗口会将提前预提取距离限制在视口的 10% 到 50% 之间。它会将后方窗口设置为 0 像素,并停用非滚动缓存。
窗口配置
您可以在 Dp 中使用视口比例或固定距离来配置缓存窗口:
// Fraction-based cache window: prefetch 50% of viewport ahead, keep 20% behind LazyColumn( cacheWindow = LazyLayoutCacheWindow(aheadFraction = 0.5f, behindFraction = 0.2f) ) { items(itemsList) { item -> Text("Item $item") } } // Dp-based cache window: prefetch 150.dp ahead, keep 50.dp behind LazyVerticalGrid( columns = GridCells.Fixed(3), cacheWindow = LazyLayoutCacheWindow( ahead = 150.dp, behind = 50.dp, isNonScrollCachingEnabled = true ) ) { items(itemsList) { item -> Text("Item $item") } }
缓存窗口管理两个与滚动方向相关的不同区域:
- 提前窗口:框架会在即将显示的项进入可见视口之前对其进行预组合和预测量。此值默认为
0dp或0f。 - 窗口后:框架会保留最近滚动出屏幕的项,而不会将其丢弃。当用户反向滚动时,保留的项会立即显示,而无需重新组合或重新测量。默认为
0dp或0f。
此外,当您设置 isNonScrollCachingEnabled = true(默认值)时,布局会在用户未主动滚动屏幕的空闲时段填充预加载窗口。这些触发器包括初始布局传递、数据集更新和项重新排序。如果设置为 false,则仅在滚动事件期间进行缓存。
自定义 CacheWindow
如果固定距离或视口比例与您的布局模式不匹配,您可以直接实现 LazyLayoutCacheWindow 接口:
interface LazyLayoutCacheWindow { fun Density.calculateAheadWindow(viewport: Int): Int = 0 fun Density.calculateBehindWindow(viewport: Int): Int = 0 val isNonScrollCachingEnabled: Boolean get() = false }
实现计算方法,以返回给定视口大小的首选窗口大小(以像素为单位):
class ScaledCacheWindow( val someLayoutState: LayoutState ) : LazyLayoutCacheWindow { override fun Density.calculateAheadWindow(viewport: Int): Int { return someLayoutState.specialDynamicCalculation().coerceAtMost(viewport * 5) } override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is ScaledCacheWindow) return false return someLayoutState == other.someLayoutState } override fun hashCode(): Int { return someLayoutState.hashCode() } } @Composable fun ScaledCacheWindowSample() { val state = rememberLazyListState() val newCacheWindow = remember { ScaledCacheWindow(state) } LazyColumn( state = state, cacheWindow = newCacheWindow, ) { items(100) { index -> Text( text = "Item #$index", modifier = Modifier .fillMaxWidth() .padding(16.dp), ) } } }
迁移指南
初始预提取配置 API (PrefetchStrategy) 和 LazyLayoutCacheWindow 的第一个版本允许在状态中进行配置,但此功能现已废弃。本部分介绍了如何从以下已弃用的 API 和参数进行迁移:
rememberLazyListState和rememberLazyGridState中基于状态的cacheWindow参数。LazyListPrefetchStrategy和LazyGridPrefetchStrategy(含LazyLayoutCacheWindow)。
选项 1:移动 cacheWindow 参数
如需迁移到推荐的 API,请将 cacheWindow 实参从 rememberLazyListState 和 rememberLazyGridState 移至 LazyColumn、LazyRow、LazyVerticalGrid 和 LazyHorizontalGrid 上的 cacheWindow 实参。LazyVerticalStaggeredGrid 和 LazyHorizontalStaggeredGrid 已在布局可组合项中实现新范式。
// Before (Experimental in Compose 1.9 to 1.12, now Deprecated) val state = rememberLazyListState( cacheWindow = LazyLayoutCacheWindow(aheadFraction = 0.5f, behindFraction = 0.2f) ) LazyColumn(state = state) { items(itemsList) { item -> Text("Item $item") } }
// Now in Compose 1.13 (Stable) val state = rememberLazyListState() LazyColumn( state = state, cacheWindow = LazyLayoutCacheWindow(aheadFraction = 0.5f, behindFraction = 0.2f) ) { items(itemsList) { item -> Text("Item $item") } }
方法 2:替换 LazyListPrefetchStrategy 和 LazyGridPrefetchStrategy
将已废弃的 LazyListPrefetchStrategy 和 LazyGridPrefetchStrategy API 替换为 LazyLayoutCacheWindow。在内部,所有预提取行为现在都依赖于 cacheWindow。此变更会改变以下场景中的预提取行为:
- 小项:当内容项占用的视口空间小于 10% 时,Compose 会提前预提取多个内容项。这可以减少快速滑动期间的滚动卡顿。
- 大型项:当单个项占用的视口超过 50% 时,限制窗口无法覆盖整个即将显示的项。如果您有较高的内容项(例如媒体信息流或消息串),您可能会观察到预提取触发器延迟,因此应调整提前窗口。
- 如果您的自定义策略依赖于逐项调度,请将逻辑移植到
LazyLayoutCacheWindow.calculateAheadWindow。如需复制类似的预提取行为(即在滚动方向上提前一个项进行组合和测量),请根据可见项指标实现自定义窗口。请注意,虽然这与实际行为不完全相同,但这是使用基于窗口的预提取时最接近实际行为的方式:
val state = rememberLazyListState() val legacyCacheWindow = remember(state) { object : LazyLayoutCacheWindow { override fun Density.calculateAheadWindow(viewport: Int): Int { val visibleItems = state.layoutInfo.visibleItemsInfo if (visibleItems.isEmpty()) return 0 val averageItemHeight = visibleItems.sumOf { it.size } / visibleItems.size return averageItemHeight } } } LazyColumn( state = state, cacheWindow = legacyCacheWindow ) { items(itemsList) { item -> Text("Item $item") } }
内容内边距
有时,您需要围绕内容边缘添加内边距。借助延迟组件,您可以将一些 PaddingValues 传递给 contentPadding 参数以支持此功能:
LazyColumn( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), ) { // ... }
在此示例中,我们将 16.dp 内边距添加到水平边缘(左侧和右侧),然后将 8.dp 添加到内容的顶部和底部。
请注意,此内边距会应用于内容,而不是 LazyColumn 本身。在上面的示例中,第一个项将 8.dp 内边距添加到顶部,最后一个项将 8.dp 添加到底部,且所有项的左侧和右侧都使用 16.dp 内边距。
再举一个例子,您可以将 Scaffold 的 PaddingValues 传递到 LazyColumn 的 contentPadding 中。请参阅边到边指南。
内容间距
如需在列表项之间添加间距,可以使用 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 支持,这是 Android 的机制,旨在当重新创建 activity 时保持相应状态。Bundle 支持基元、枚举或 Parcelable 等类型。
LazyColumn { items(books, key = { // primitives, enums, Parcelable, etc. }) { // ... } }
Bundle 必须支持该键,以便在重新创建 activity 时,甚至在您滚动离开此项然后滚动回来时,此项可组合项中的 rememberSaveable 仍可以恢复。
LazyColumn { items(books, key = { it.id }) { val rememberedValue = rememberSaveable { Random.nextInt() } } }
项动画
如果您使用过 RecyclerView widget,便会知道它会自动为列表项更改添加动画效果。延迟布局提供了相同的功能,以用于重新排列列表项。此 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) ) ) { // ... } } }
您需要确保为您的项提供键,以便找到被移动的元素的新位置。
示例:为延迟列表中的项添加动画效果
借助 Compose,您可以为延迟列表中项的变化添加动画效果。以下代码段结合使用时,可在添加、移除和重新排序延迟列表项时实现动画效果。
以下代码段会在添加、移除或重新排序项时显示带有动画过渡效果的字符串列表:
@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), ) } } }
代码要点
ListAnimatedItems在LazyColumn中显示字符串列表,并在修改项时显示动画过渡。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会显示一个屏幕,其中包含用于添加、移除和排序内容的控件。onAddItem和onRemoveItem是传递给AddRemoveButtons的 lambda 表达式,用于在列表中添加和移除项。resetOrder、onSortAlphabetically和onSortByLength是传递给OrderButtons的 lambda 表达式,用于更改列表中项的顺序。
AddRemoveButtons会显示“添加”和“移除”按钮。它会启用/停用按钮并处理按钮点击。OrderButtons会显示用于对列表重新排序的按钮。它会接收用于重置顺序以及按长度或字母顺序对列表进行排序的 lambda 函数。ListAnimatedItems会调用ListAnimatedItems可组合项,并传递data列表以显示带动画效果的字符串列表。data在其他位置定义。
以下代码段创建了一个包含添加项和删除项按钮的界面:
@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会显示一行按钮,用于对列表执行添加和移除操作。canAddItem和canRemoveItem参数用于控制按钮的启用状态。如果canAddItem或canRemoveItem为 false,则停用相应按钮。onAddItem和onRemoveItem形参是用户点击相应按钮时执行的 lambda。
最后,此代码段会显示三个用于对列表进行排序的按钮(Reset、 Alphabetical 和 Length):
@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组件可让您从选项列表中选择单个选项。resetOrder、orderAlphabetically和orderByLength是在选择相应按钮时执行的 lambda 函数。selectedIndex状态变量会跟踪所选选项。
结果
此视频展示了在重新排列内容时上述代码段的结果:
固定标题
“粘性标题”模式在显示分组数据列表时非常有用。 下面显示的是“联系人列表”示例,其中的数据按照每个联系人的名字首字母分组:

如需使用 LazyColumn 实现粘性标题,可以使用 stickyHeader()
函数,该函数提供以下标题内容:
@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] } @Composable fun ContactsList(grouped: Map<Char, List<Contact>>) { LazyColumn { grouped.forEach { (initial, contactsForInitial) -> stickyHeader { CharacterHeader(initial) } items(contactsForInitial) { contact -> ContactListItem(contact) } } } }
响应滚动位置
许多应用需要对滚动位置和列表项布局更改作出响应,并进行监听。延迟组件通过提升 LazyListState 来支持此用例:
@Composable fun MessageList(messages: List<Message>) { // Remember our own LazyListState val listState = rememberLazyListState() // Provide it to LazyColumn LazyColumn(state = listState) { // ... } }
对于简单的用例,应用通常只需要了解第一个可见列表项的相关信息。为此,LazyListState 提供了 firstVisibleItemIndex 和 firstVisibleItemScrollOffset 属性。
如果我们使用根据用户是否滚动经过第一个列表项来显示和隐藏按钮的示例,那么代码如下:
@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 } .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() } } } }
关于使用延迟布局的提示
您不妨考虑下列提示,以确保您的延迟布局按预期运行。
避免使用大小为 0 像素的列表项
这可能会在某些情况下发生,例如,当您希望在后期阶段异步检索一些数据(例如图片)以填充列表项时。这会使延迟布局在首次衡量时组合其所有项,因为项的高度为 0 像素,所以这类项可完全适合视口大小。待这些项加载完毕且高度增加后,延迟布局随后会舍弃首次不必要组合起来的所有其他项,因为这些项实际上无法适合视口。为避免出现这种情况,您应该为您的项设置默认大小,这样可使延迟布局正确地计算有多少项实际上适合视口:
@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) } // ... }
延迟布局会按预期处理这种情况 - 逐个布置各元素,就像它们是不同的项一样。不过,这样做会出现一些问题。
当多个元素作为一个项的一部分发出时,系统会将其作为一个实体进行处理,这意味着这些元素无法再单独组合。如果一个元素在屏幕上可见,则与该项对应的所有元素都必须组合和衡量。如果过度使用,则可能会降低性能。将所有元素放入一个项属于极端情况,完全违背了使用延迟布局的目的。除了潜在的性能问题外,将更多元素放入一个项中还会干扰 scrollToItem() 和 animateScrollToItem()。
不过,也有一些将多个元素放入一个项的有效用例,例如在一个列表内添加多条分隔线。您不希望分隔线更改滚动索引,因为分割线不应被视为独立元素。此外,由于分隔线占空间很小,因此性能不会受到影响。分隔线可能需要在其之前的那个项可见时显示,这样分割线就可纳入前一个项中:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Divider() } item { Item(2) } // ... }
考虑使用自定义排列方式
通常,延迟列表包含许多项,并且这些项所占空间大于滚动容器的大小。不过,如果列表中填充的项很少,那么在设计中,您可以对这些项在视口中的位置做出更具体的要求。
为实现此目的,您可以使用自定义垂直 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 开始,为了最大限度地提高延迟布局的性能,建议将 contentType 添加到您的列表或网格中。当您的列表或网格由多种不同类型的项组成时,这样可让您为布局的每一项指定内容类型:
LazyColumn { items(elements, contentType = { it.type }) { // ... } }
当您提供 contentType 时,Compose 只能在相同类型的项之间重复使用组合。由于在组合具有相似结构的项时,重复使用效率更高,因此提供内容类型可确保 Compose 不会尝试在完全属于 B 类型的项之上组合 A 类型的项。这有助于最大限度地重复使用组合和提高延迟布局性能。
衡量性能
只有在发布模式下运行且启用了 R8 优化时,您才能可靠地衡量延迟布局的性能。在调试 build 中,延迟布局滚动可能会显得更慢。如需了解详情,请仔细阅读 Compose 性能。
其他资源
为你推荐
- 注意:当 JavaScript 处于关闭状态时,系统会显示链接文字
- 将
RecyclerView迁移到延迟列表 - 在 Compose 中保存界面状态
- Kotlin 对 Jetpack Compose 的支持