读取LazyListState导致LazyColumn项异常重组的原因咨询
请参考以下最小可复现代码:
@Composable fun Sandbox() { val state = rememberLazyListState() LazyColumn( state = state, modifier = Modifier.height(200.dp) ) { // 该行代码会引发偶发重组,注释后无重组问题 val test = DoSomething(state) items(count = 50, key = {index -> index}) { index -> Text(text = "Item: $index") } } } fun DoSomething(state: LazyListState) : Int? { val items = state.layoutInfo.visibleItemsInfo return null }
上下滚动列表数次后会触发重组(未操作的列表项也会发生重组,不符合预期),移除DoSomething(state)调用后问题消失。
补充说明:
- 未被操作的列表项也发生了重组,按Compose逻辑这些项应被跳过。
- 将
val test = DoSomething(state)移至LazyColumn外部后,一切正常(Sandbox重组,LazyColumn被跳过,列表项完全不受影响)。问题并非如何解决,而是为何在LazyColumn内部调用会出现不符合预期的情况。
1. LazyColumn的content lambda是特殊重组作用域
LazyColumn的content参数是LazyListScope.() -> Unit,这个作用域的代码逻辑和普通Composable函数体完全不同:框架会专门处理这个作用域,只要LazyListState的相关属性发生变化,整个content lambda就会被重新执行,而非普通Composable那样只重组依赖变化的局部代码。
你在这个lambda里调用DoSomething(state)时,读取了state.layoutInfo.visibleItemsInfo——这是一个依赖LazyListState的派生State属性,每次列表滚动时,visibleItemsInfo都会更新,直接触发content lambda的整体重新执行。
2. 列表项全量重组的原因
虽然你给items设置了key,但content lambda的整体重行会导致items()函数被再次调用。Compose会认为所有列表项的“创建逻辑”发生了变更,即便每个item的key和内容都没变化,也会触发它们的重组——因为item的lambda是在content lambda的上下文里执行的,content的重组会直接传递给所有列表项。
3. 移到外部正常的核心原因
当你把DoSomething(state)移到LazyColumn外面时,读取visibleItemsInfo只会触发Sandbox函数的重组。而LazyColumn本身的重组会被Compose跳过:因为LazyColumn的入参(state是同一个remember实例,modifier也没变化)都未发生改变,所以框架会跳过LazyColumn的重组,自然不会影响到内部的列表项。
总结
LazyColumn的content lambda是特殊作用域,它的重组逻辑和普通Composable不同——只要LazyListState的关联属性(比如visibleItemsInfo)变化,整个content lambda就会重跑,进而导致所有列表项触发重组。而在外部读取State只会触发父Composable的重组,由于LazyColumn的入参未变,所以不会被重组,列表项也就不受影响。
内容的提问来源于stack exchange,提问作者me.at.coding

