一、概念类似于 RecyclerView 是懒加载的滑动到哪里才会加载。而 Column、Row 用 Modifier 设置可滚动是相当于ScrollView会一次性全部加载。从 Navigation 返回后丢失状态跳到头部 将 LazyListState 提升到 Screen 级别不用放到 ViewModel 也能恢复滚动状态。2023-12-14。Composable fun Demo(dataList: ListString) { 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: ListString) { 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: FiniteAnimationSpecFloat? spring(stiffness Spring.StiffnessMediumLow),placementSpec: FiniteAnimationSpecIntOffset? spring(stiffness Spring.StiffnessMediumLow,visibilityThreshold IntOffset.VisibilityThreshold,),fadeOutSpec: FiniteAnimationSpecFloat? 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: MapChar, ListContact) { LazyColumn { grouped.forEach { (lastName, contactsForLastName) - stickyHeader { CharacterHeader(lastName) } items(contactsForLastName) { contact - ContactListItem(contact) } } } }2.6 分页加载配合 Paging 使用详见Compose - 使用 PagingPaging 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() } }三、列表状态 LazyListState3.1 滚动位置简单监听通常只需要监听第一个可见条目的信息LazyListState 提供了 firstVisibleItemIndex第一个可见条目的索引和 firstVisibleItemScrollOffset第一个可见条目偏移量属性。Composable fun MessageList(messages: ListMessage) { 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: ListMessage) { val listState rememberLazyListState() val coroutineScope rememberCoroutineScope() LazyColumn(state listState) { // ... } ScrollToTopButton( onClick { coroutineScope.launch { listState.animateScrollToItem(index 0) } } ) }3.4 预取 LazyLayoutCacheWindowv1.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 使用 contentTypev1.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: ListUser, onItemLick: (id: Long) - Unit ) { LazyColumn { items( items userList, key { it.id } ) { user - UserItem( user user, onClick { onItemLick(user.id) } ) } } }