Jetpack Compose 易犯错误之:在 LazyColumn 中访问 LazyListState

news/2024/7/3 13:20:14

我们在使用 LazyColumn 或者 LazyRow 时,应该避免在 LazyListScope 中访问 LazyListState,这可能会造成隐藏的性能问题,看下面的代码:

@Composable
fun VerticalList(items: List<String>, onReachedBottom: () -> Unit) {
    val listState = rememberLazyListState()
    LazyColumn(state = listState) { // LazyListScope
        items(items) {
            Text(text = it)
        }
        if (listState.firstVisibleItemIndex + listState.layoutInfo
                .visibleItemsInfo.size == listState.layoutInfo.totalItemsCount) {
            onReachedBottom()
        }
    }
}

代码中我们希望,当列表滚动到底部时,回调 onReachedBottom处理一些业务。但这种写法会造成 content 的代码频繁重组,造成性能问题

原因分析

我们在 LazyColumn 的 content lambda 也就是 LazyListScope 通过访问了 listState.firstVisibleItemIndex 的访问判断当前列表滚动的位置

firstVisibleItemIndexLazyListState 中的定义如下:

@Stable
class LazyListState constructor(
    firstVisibleItemIndex: Int = 0,
    firstVisibleItemScrollOffset: Int = 0
) : ScrollableState {
   
    /**
     * The holder class for the current scroll position.
     */
    private val scrollPosition =
        LazyListScrollPosition(firstVisibleItemIndex, firstVisibleItemScrollOffset)
 
    /**
     * The index of the first item that is visible
     */
    val firstVisibleItemIndex: Int get() = scrollPosition.observableIndex
	
	//...
}

observableIndexscrollPosition 中的定义如下:

internal class LazyListScrollPosition(
    initialIndex: Int = 0,
    initialScrollOffset: Int = 0
) {
    var index = DataIndex(initialIndex)
        private set

    var scrollOffset = initialScrollOffset
        private set

    private val indexState = mutableStateOf(index.value)
    val observableIndex get() = indexState.value
	
	//...
}

可见,observableIndex 指向了 indexState 这个 State 的值,由于 content 是一个 Composable 的 lambda,所以在 content 中对 observableIndex 的访问时也就订阅了 indexState 的变化。

当我们将 LazyListState 传给 LazyColumn / LazyRow 后,随着列表的滚动,这个状态会实时更新,这就造成了 content 的无效重组。

Compose 中很多想 LazyListState 这样的对象,被称为 State Holder ,它们本身虽然不是 State 类型,但是它们内部会聚合一些 State,目的是将状态管理逻辑集中管理,所以对这些对象的访问很有可能就是对内部某个 State 的订阅。 因此对他们的使用要格外小心。

如何解决

@Composable
fun VerticalList(items: List<String>, onReachedBottom: () -> Unit) {
    val listState = rememberLazyListState()
    val isReachedBottom by remember {
        derivedStateOf {
            listState.firstVisibleItemIndex + listState.layoutInfo
                .visibleItemsInfo.size == listState.layoutInfo.totalItemsCount
        }
    }
    LaunchedEffect(Unit) {
        snapshotFlow { isReachedBottom }
            .collect {  isReached ->
                if (isReached) {
                    onReachedBottom()
                }
            }
    }

    LazyColumn(state = listState) {
        items(items) {
            Text(text = it)
        }
    }
}

修改的代码如上,我们将判断 list 滚动的逻辑抽象为一个 isReachedBottom 状态,然后通过 snapshotFlow 单独定义其变化,这样避免 LazyColumn 的 content 的重组。snapshotFlow {}可以订阅 State 的变化,并将其转换为 Flow 的数据流。

也许有人会问 derivedStateOf 的作用是什么?

/**
 * Creates a [State] object whose [State.value] is the result of [calculation]. The result of
 * calculation will be cached in such a way that calling [State.value] repeatedly will not cause
 * [calculation] to be executed multiple times, but reading [State.value] will cause all [State]
 * objects that got read during the [calculation] to be read in the current [Snapshot], meaning
 * that this will correctly subscribe to the derived state objects if the value is being read in
 * an observed context such as a [Composable] function.
 */
fun <T> derivedStateOf(calculation: () -> T): State<T> = DerivedSnapshotState(calculation)

从注释可以清楚知道,derivedStateOf 将 calculation 的结果返回为一个 State,对这个 State 的访问相当于对 calculation 内部出现的 State 的访问,当 calculation 内部的 State 发生变化时,访问 DerivedState 的 Composable 会重组。为了避免 derivedStateOf 重复构建,需要使用 remember 进行缓存

从效果上来说

    val isReachedBottom by remember {
        derivedStateOf {
            listState.firstVisibleItemIndex + listState.layoutInfo
                .visibleItemsInfo.size == listState.layoutInfo.totalItemsCount
        }
    }

等价于

val isReachedBottom = remember(listState.firstVisibleItemIndex) {
	 listState.firstVisibleItemIndex + listState.layoutInfo
                .visibleItemsInfo.size == listState.layoutInfo.totalItemsCount
}

但是前者的重组范围只局限在对 isReachedBottom 访问的 Composable,而后者的重组范围发生在对 listState.firstVisibleItemIndex 访问的 Composable ,所以前者性能更优。


http://www.niftyadmin.cn/n/2071155.html

相关文章

Compose Multiplatform 正式版将于年内发布

近日&#xff0c;JetBrains 公司发布了 Compose Multiplatform 的 Beta 版本&#xff0c;这距离此前 Alpha 版本的发布才过去两个多月。 这个版本中包含了许多新的改进&#xff0c;在桌面端与Web端分别增加了新的 API &#xff0c;并对已有的部分 APIs 进行了稳定。Beta 版的发…

对标 VSCode?JetBrains 下一代编辑器 Fleet

11 月 29 日 JetBrains 官方发布了全新的轻量级编辑器 Fleet&#xff0c;并号称是基于20年IDE开发经验打造的“新一代 IDE”。 Fleet 的定位更加纯粹&#xff0c;聚焦编辑器功能而非替代现有的 IDE 产品。据推测 Fleet 的推出主要是 JetBrains 为了对抗微软的 VSCode &#xff…

10个问题带你看懂 Compose Multiplatform 1.0

近日 JetBrains 正式发布了 Compose Multiplatform 1.0 版&#xff0c;这标志其在生产环境中使用的时机已经成熟。相信有不少人对它还不太熟悉&#xff0c;本文通过下面 10 个热门问题带大家认识这一最新的跨平台技术。 FAQ&#xff1a; 与 Jetpack Compose 的关系? 是否会取代…

Jetpack MVVM 七宗罪之四: 使用 LiveData/StateFlow 发送 Events

久违的 “ Jetpack MVVM 七宗罪 ” 系列&#xff0c;今日再开。本系列主要盘点 MVVM 架构中各种常见错误写法&#xff0c;并针对性的给出最佳实践&#xff0c;帮助大家掌握 Jetpack 组件最正确的使用姿势。 Jetpack MVVM 七宗罪之一: 拿 Fragment 当 LifecycleOwnerJetpack MVV…

Jetpack MVVM 七宗罪之五: 在 Repository 中使用 LiveData

前言 现在的 Android 项目中几乎少不了对 LiveData 的使用。MVP 时代我们需要定义各种 IXXXView 实现与 Presenter 的通信&#xff0c;而现在已经很少见到类似的接口定义了&#xff0c;大家早已习惯了用响应式的思想设计表现层与逻辑层之间的通信&#xff0c;这少不了 LiveData…

使用整洁架构优化你的 Gradle Module

前言 现代的 Android 项目都是 Gradle 工程&#xff0c;所以大家都习惯于用 Gradle Module 来划分和组织代码&#xff0c;Module 的大量使用也带来一个问题&#xff0c;一个大项目往往几十上百的 Module&#xff0c;但是当数量众多的 Module 之间的依赖关系不合理时&#xff0c…

Jetpack Compose 无限加载列表(滚到底部自动加载更多)

Android 中使用 ListView 或者 RecycleView 经常有滚动到底部自动 LoadMore 的需求&#xff0c;那么在 Compose 中该如何实现呢&#xff1f; 两种方法可供选择&#xff1a; 基于 paging-compose自定义实现 方法一&#xff1a; paging-compose Jetpack 的 Paging 组件提供了…

Jetpack Compose 动画 API: AnimatedVisibility AnimatedContent

Jetpack Compose 的动画相关的 API 数量众多&#xff0c;分为低级别 API 和高级别 API&#xff0c;其中高级别 API 便于使用者针对具体场景开箱即用 &#xff0c;其中最常用的当属 AnimatedVisibility 和 AnimatedContent 这两个了。 1. AnimatedVisibility AnimatedVisibility…