Jetpack Compose LazyColumn未随列表数据更新重组问题咨询
我正在使用Jetpack Compose开发应用,作为该UI框架的新手,我希望展示Category类的amountSpent属性。我在弹窗中修改该属性值后,发现LazyColumn未触发重组更新显示,但直接遍历列表的forEach中的Text却能正常更新。
代码中使用SnapshotStateList存储Category实例,修改categories[0].amountSpent后,LazyColumn仍显示旧值,而forEach遍历的Text显示新值。我通过移除再重新添加元素的方式临时解决了问题,但想确认这是Jetpack Compose的Bug还是我的实现问题?
相关代码
列表初始化代码
var categories = remember { mutableStateListOf( Category.FoodCategory(amountSpent=100), Category.ShoppingCategory(amountSpent=50), Category.TransportationCategory(amountSpent=80), Category.EducationCategory(amountSpent=90) ) } categories[0].amountSpent=400
UI展示代码
@Composable fun HomeUi(categories: SnapshotStateList<Category>) { LazyColumn() { items(categories){ Text(text = it.amountSpent.toString()) } } categories.forEach{ Text(text = it.amountSpent.toString()) } }
Category类定义
sealed class Category( val name: String, val totalBudget:Int, open var amountSpent: Int, @DrawableRes val icon : Int){ data class FoodCategory(override var amountSpent: Int=0): Category("Food",500,amountSpent,R.drawable.ic_food) class ShoppingCategory(override var amountSpent: Int=0): Category("Shopping",500,0,R.drawable.ic_shopping) class TransportationCategory(override var amountSpent: Int=0): Category("Transportation",500,amountSpent,R.drawable.ic_transportation) class EducationCategory(override var amountSpent: Int=0): Category("Education",500,amountSpent,R.drawable.ic_education) }
临时修复代码
var categoryChanged = categories.find { cat == it.name } categoryChanged?.amountSpent = categoryChanged?.amountSpent?.plus(400)!! var ind = categories.indexOf(categoryChanged) categories.remove(categoryChanged) categories.add(ind, categoryChanged!!)
这不是Jetpack Compose的Bug,是对SnapshotStateList的工作机制理解偏差导致的:
- SnapshotStateList仅追踪列表结构变化:只有当列表发生增删元素、替换元素这类操作时,它才会发出变化通知,触发依赖它的Composable重组。而列表中元素的内部属性修改,并不会被SnapshotStateList感知。
- LazyColumn的items逻辑:
items方法依赖SnapshotStateList的结构变化通知来更新对应Item。当元素内部属性修改时,SnapshotStateList没发出通知,所以LazyColumn不会重组更新。 - forEach遍历的Text能更新的原因:HomeUi Composable可能因其他操作(比如弹窗关闭)触发重组,此时forEach会重新遍历列表并读取元素的最新amountSpent值,所以能显示新内容。
另外,你的Category类中,除了FoodCategory是data class,其他都是普通class,且amountSpent是普通var属性,不是Compose的State类型,Compose无法追踪该属性的变化。
有两种常用的修复方式:
方式一:将amountSpent改为Compose State
把amountSpent定义为MutableState<Int>,让Compose能追踪属性变化:
sealed class Category( val name: String, val totalBudget: Int, open val amountSpent: MutableState<Int>, @DrawableRes val icon: Int ) { data class FoodCategory(override val amountSpent: MutableState<Int> = mutableStateOf(0)) : Category("Food", 500, amountSpent, R.drawable.ic_food) class ShoppingCategory(override val amountSpent: MutableState<Int> = mutableStateOf(0)) : Category("Shopping", 500, mutableStateOf(0), R.drawable.ic_shopping) class TransportationCategory(override val amountSpent: MutableState<Int> = mutableStateOf(0)) : Category("Transportation", 500, amountSpent, R.drawable.ic_transportation) class EducationCategory(override val amountSpent: MutableState<Int> = mutableStateOf(0)) : Category("Education", 500, amountSpent, R.drawable.ic_education) }
使用时读取State的value:
@Composable fun HomeUi(categories: SnapshotStateList<Category>) { LazyColumn() { items(categories) { Text(text = it.amountSpent.value.toString()) } } categories.forEach { Text(text = it.amountSpent.value.toString()) } }
修改值时直接更新State的value:
categories[0].amountSpent.value = 400
方式二:修改属性后替换列表中的元素
利用SnapshotStateList能感知元素替换的特性,把修改后的元素重新赋值到列表对应位置:
// 针对FoodCategory(data class可直接用copy) val updatedCategory = (categories[0] as Category.FoodCategory).copy(amountSpent = 400) categories[0] = updatedCategory
如果是普通class,需要手动创建新实例替换,比如ShoppingCategory:
val oldCategory = categories[1] as Category.ShoppingCategory val updatedCategory = Category.ShoppingCategory(amountSpent = 400) categories[1] = updatedCategory
你用的移除再添加元素的方法,本质上是触发了SnapshotStateList的结构变化(先删除再添加元素),此时SnapshotStateList会发出变化通知,LazyColumn收到后触发重组,从而显示新的amountSpent值。
内容的提问来源于stack exchange,提问作者Lokesh Chennamchetty

