Compose 组件 - 滑动列表 LazyList(LazyColumn、LazyRow)
2026/9/5 15:11:49 网站建设 项目流程

一、概念

类似于 RecyclerView 是懒加载的,滑动到哪里才会加载。而 Column、Row 用 Modifier 设置可滚动是相当于ScrollView,会一次性全部加载。

  • 从 Navigation 返回后丢失状态(跳到头部) :将 LazyListState 提升到 Screen 级别,不用放到 ViewModel 也能恢复滚动状态。(2023-12-14)。
@Composable fun Demo(dataList: List<String>) { val state = rememberLazyListState() LazyColumn( modifier = Modifier.height(100.dp), state = state, //管理列表的状态,可通过这个操作列表滚动 contentPadding = PaddingValues(horizontal = 5.dp), //内容内边距 reverseLayout = false, //是否反转显示 verticalArrangement = Arrangement.Top, //排列方式,使用Arrangement.spacedBy(4.dp)就是设置间距 horizontalAlignment = Alignment.Start, //对齐方式 flingBehavior = ScrollableDefaults.flingBehavior(), userScrollEnabled = true, //是否允许滑动 overscrollEffect: OverscrollEffect? = rememberOverscrollEffect(), //滑动到边界时的拉伸和发光效果 ) { } }

二、使用

2.1 列表条目 item()、items()

LazyListScope DSL 提供多种函数来描述列表中的条目,item() 添加单个条目,items() 添加多个条目。

fun Demo(dataList: List<String>) { LazyColumn { //添加单个条目 item { Text(text = "单个条目") } //添加多个条目 items(5) {index -> Text(text = "条目索引:$index") } //根据数据源创建 items( items = dataList, //可传入、数组、集合、Paging的LazyPagingItems key = { element -> element.id //key设为element的唯一值 } ) { item -> //对条目布局使用该修饰符来对列表的更改添加动画效果 Row(Modifier.animateItemPlacement()) {} } //带索引 itemsIndexed(dataList) { index: Int, item: String -> Text(text = "$index:$item", modifier = Modifier.padding(1.dp).background(Color.Red)) } } }

2.2 内容内边距 contentPadding

如果对 LazyRow 设置 Modifier.padding() 滑动的时候左右两端会有割裂感(没有滑出屏幕边缘),使用 contentPadding 后初始状态下会显示边距,滑动后是滑出屏幕外。

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

2.3 内容间距 verticalArrangement

如果对 Item 设置padding,会导致相邻的两个 Item 之间是 2 个间距大小,首尾两个 Item 距离容器边缘是 1 个间距大小。

LazyColumn( //使用Arrangement.Top就是排列方式 verticalArrangement = Arrangement.spacedBy(4.dp) ) { }

2.4 条目动画

对条目布局使用修饰符来对列表的更改添加动画效果。

fun Modifier.animateItem(
fadeInSpec: FiniteAnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow),
placementSpec: FiniteAnimationSpec<IntOffset>? = spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntOffset.VisibilityThreshold,
),
fadeOutSpec: FiniteAnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow),
): Modifier = this

赋值 null 就不显示对应的动画。fadeInSpec添加动画,fadeOutSpec删除动画,placementSpec位置变化动画。需要对 item{} items{} 设置 key 来生效动画。

items(dataList, key = { it.id }) { Row(Modifier.animateItem()) { } }

2.5 黏性标题 stickyHeader()

不会跟随滚动,直到被下一个取代。背景默认透明滑动后会穿透显示。

2.5.1 单个粘性标题

@Composable fun Dode() { LazyColumn(Modifier.height(100.dp)) { stickyHeader { Text(text = "头部", modifier = Modifier.background(Color.Gray)) } items(10) { Text(text = "条目索引:$it") } } }

2.5.2 多个粘性标题

@Composable fun ContactsList(grouped: Map<Char, List<Contact>>) { LazyColumn { grouped.forEach { (lastName, contactsForLastName) -> stickyHeader { CharacterHeader(lastName) } items(contactsForLastName) { contact -> ContactListItem(contact) } } } }

2.6 分页加载(配合 Paging 使用)

详见:Compose - 使用 Paging

Paging 3.0 及更高版本通过androidx.paging:paging-compose库提供 Compose 支持。

@Composable fun Demo() { val pagingItems = viewModel.dataFlow.collectAsLazyPagingItems() LazyColumn( modifier = Modifier.fillMaxSize(), state = rememberLazyListState() ) { items( count = pagingItems.itemCount, key = pagingItems.itemKey { it.id } ) { index -> //... } } } ViewModel { val dataFlow by lazy { Pager(PagingConfig(10)) { NewestPagingSource(repository) }.flow.cachedIn(viewModelScope) } }

2.7 嵌套滚动

应避免内部嵌套一个没有固定大小同向可滚动子组合项,会抛出 IllegalStateException。可封装在同一个 LazyColumn 中,使用 LazyListScope DSL 传入不同类型的内容,得到相同的效果。

合理的嵌套滚动:

  • 内层和外层的列表滚动方向并不一致,这样它们之间是没有滑动事件冲突。
  • 内层和外层的列表滚动方向一致,但是内层列表在滚动方向上的尺寸是固定的。
  • 添加多个 items 函数来指定不同类型的数据源列表,还可以添加 item 函数来指定单个数据项,最终它们都会形成一个整体可滚动的列表。
// Throws IllegalStateException Column(modifier = Modifier.verticalScroll(state)) { LazyColumn {...} } //合法的同方向嵌套 Column(modifier = Modifier.verticalScroll(state)) { LazyColumn(modifier = Modifier.height(300.dp)) {...} } //合法的不同方向嵌套 Column(modifier = Modifier.verticalScroll(state)) { LazyRow {...} } //拼接 LazyColumn { item { Header() } items(data) { item -> Item(item) } item { Footer() } }

三、列表状态 LazyListState

3.1 滚动位置简单监听

通常只需要监听第一个可见条目的信息,LazyListState 提供了 firstVisibleItemIndex(第一个可见条目的索引)和 firstVisibleItemScrollOffset(第一个可见条目偏移量)属性。

@Composable fun MessageList(messages: List<Message>) { Box { val listState = rememberLazyListState() LazyColumn(state = listState) { //... } val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() } } }

3.2 滚动位置复杂监听

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

val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .map { index -> index > 0 } .distinctUntilChanged() .filter { it == true } .collect { MyAnalyticsService.sendScrolledPastFirstItemEvent() } }

3.3 控制滚动位置

LazyListState 通过 scrollToItem() 立即滚动和 animateScrollToItem() 使用动画滚动(平滑滚动)。

@Composable fun MessageList(messages: List<Message>) { val listState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() LazyColumn(state = listState) { // ... } ScrollToTopButton( onClick = { coroutineScope.launch { listState.animateScrollToItem(index = 0) } } ) }

3.4 预取 LazyLayoutCacheWindow

v1.9 可以定义 LazyLayoutCacheWindow 来预提取更多内容。默认情况下,系统仅会在滚动方向上(列表末尾)提前组合一个item,并在 item 滑出屏幕后立即舍弃。现在可以通过视口比例或 dp 尺寸,来自定义需要预提取的前置 item 数量和需要保留的后置 item 数量。注意这意味着 item 中的副作用会更早执行,勿用作可见性判断(使用Modifier的可见性修饰符)。

通过dp设置

fun LazyLayoutCacheWindow(

ahead: Dp = 0.dp, //列表尾部(滚动方向头部)

behind: Dp = 0.dp //列表头部(滚动方向尾部)

): LazyLayoutCacheWindow

通过比例设置fun LazyLayoutCacheWindow(
@FloatRange(from = 0.0) aheadFraction: Float = 0.0f, //列表尾部(滚动方向头部)
@FloatRange(from = 0.0) behindFraction: Float = 0.0f, //列表头部(滚动方向尾部)
): LazyLayoutCacheWindow
//方式一:通过dp设置。behind列表头部,ahead列表尾部 val dpCacheWindow = LazyLayoutCacheWindow(ahead = 150.dp, behind = 100.dp) //方式二:通过比例设置。 val fractionCacheWindow = LazyLayoutCacheWindow(aheadFraction = 1f, behindFraction = 0.5f) //传递给列表状态 val state = rememberLazyListState(cacheWindow = dpCacheWindow)

四、提升重组性能

4.1 使用 key

一般情况下函数的位置是不会发生变化的,Compose编译器能基于调用位置来区分不同的组合项,然后判断内容是否发生变化决定要不要重组。但对于列表来说,在尾部插入还好,如果是在头部或中间,后面的条目由于位置都发生了变化会全部重组,然而现实需求是只需要更新内容发生变化的条目,因此造成了无必要的刷新。

手动设置 key 为条目的唯一值(如id)后,使得编译器能够精准的定位每一个 item 函数, 对于内容是否变化能自动通过对象自身的 equals() 来感知,以便跳过重组。这样当数据源发生变化时列表可以高效重组,只需要处理那些发生过变化的条目对应的组合项即可,而不是全部更新。

items( items = dataList, key = { it.id } //it是dataBean ) { }

4.2 使用 contentType

v1.2版本开始,当列表由多种不同类型的条目组成时,可以为条目指定类型来确保 Compose 不会尝试在属于B类型的组合项上组合A类型的条目。

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

4.3 缓存 Item 中的对象

用 remember 缓存 item 内的 lambda、计算结果、图片请求等。避免在 item 中创建大对象,如每次 item 重组创建 Bitmap/Drawable,会导致内存抖动。

4.4 将 Item 中的回调提升到父容器中

事件提升。

@Composable fun Demo( userList: List<User>, onItemLick: (id: Long) -> Unit ) { LazyColumn { items( items = userList, key = { it.id } ) { user -> UserItem( user = user, onClick = { onItemLick(user.id) } ) } } }

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询