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

我们在使用 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 ,所以前者性能更优。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fundroid

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值