我们在使用 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
的访问判断当前列表滚动的位置
firstVisibleItemIndex
在 LazyListState
中的定义如下:
@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
//...
}
而 observableIndex
在 scrollPosition
中的定义如下:
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 ,所以前者性能更优。