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

Posted fundroid_方卓

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 ,所以前者性能更优。

以上是关于Jetpack Compose 易犯错误之:在 LazyColumn 中访问 LazyListState的主要内容,如果未能解决你的问题,请参考以下文章

Jetpack Compose学习 之 HelloWorld

Jetpack Compose学习 之 HelloWorld

Jetpack Compose-没有方法签名:错误

Jetpack Compose - Canvas之BlendMode

Jetpack Compose 的 3 大错误(不惜一切代价避免)

Android安卓进阶技术之——Jetpack Compose