Jetpack Compose中无需使用MutableState触发Composable重组的简便方法咨询
大家好,我最近经常遇到开发者问这个Jetpack Compose的问题:怎么在不把数据类每个属性都套MutableState的前提下,触发Composable重组更新UI。我结合自己的实践和常见的踩坑经历,给大家梳理下完整的解决思路:
问题场景
先看一个典型的场景:我定义了一个包含多个普通Int属性的类,在Compose的Column里展示这些属性,再加个按钮修改其中一个属性:
class MyDataClass( var a: Int = 1, var b: Int = 1, var c: Int = 1, var d: Int = 1, var e: Int = 1, var f: Int = 1, var g: Int = 1, ) @Composable fun DemoScreen() { val foo = MyDataClass() Column(Modifier.padding(16.dp)) { Text(foo.a.toString(), fontSize = 18.sp) Text(foo.b.toString(), fontSize = 18.sp) Text(foo.c.toString(), fontSize = 18.sp) Text(foo.d.toString(), fontSize = 18.sp) Text(foo.e.toString(), fontSize = 18.sp) Text(foo.f.toString(), fontSize = 18.sp) Text(foo.g.toString(), fontSize = 18.sp) Button( onClick = { foo.a = 69 }, modifier = Modifier.topPadding(16.dp) ) { Text("Change some value") } } }
点击按钮后你会发现:foo.a的值确实改成69了,但UI上的Text完全没更新——因为Compose的重组触发逻辑是感知状态的变化,普通类的属性修改,Compose根本检测不到。
常见尝试的误区
误区1:把整个数据类包在mutableStateOf里
很多人第一反应会把整个数据类实例放进mutableStateOf:
val foo = mutableStateOf(MyDataClass())
但这完全没用!因为Compose只会检测foo这个引用本身的变化——比如你执行foo.value = MyDataClass()时,Compose才会感知到;但修改foo.value.a这种内部属性时,Compose完全没察觉,UI自然不会更新。
误区2:给每个属性套MutableState
这是官方文档里的“标准”做法,把每个属性都改成MutableState<Int>类型:
class MyDataClass( var a: MutableState<Int> = mutableStateOf(1), var b: MutableState<Int> = mutableStateOf(1), var c: MutableState<Int> = mutableStateOf(1), var d: MutableState<Int> = mutableStateOf(1), var e: MutableState<Int> = mutableStateOf(1), var f: MutableState<Int> = mutableStateOf(1), var g: MutableState<Int> = mutableStateOf(1), )
这样修改foo.a.value = 69时,Compose就能感知到变化触发重组了。但问题是——如果数据类有几十个属性,每个都套MutableState太繁琐,代码也变得臃肿,而且我们本来就想让属性保持普通的Int类型。
更简洁的解决方案:data class + mutableStateOf + copy
这是我个人最推荐的方案,既简洁又符合Compose的状态设计原则:
步骤1:把类改成data class(用val定义不可变属性)
data class MyDataClass( val a: Int = 1, val b: Int = 1, val c: Int = 1, val d: Int = 1, val e: Int = 1, val f: Int = 1, val g: Int = 1, )
用val让属性不可变,强制我们通过生成新实例的方式修改状态,这也符合Compose的状态不可变设计原则,能避免很多可变状态带来的潜在问题。
步骤2:在Composable中使用mutableStateOf包裹,修改时用copy生成新实例
@Composable fun DemoScreen() { // 用remember保存状态,避免重组时重复创建 val foo = remember { mutableStateOf(MyDataClass()) } Column(Modifier.padding(16.dp)) { Text(foo.value.a.toString(), fontSize = 18.sp) Text(foo.value.b.toString(), fontSize = 18.sp) Text(foo.value.c.toString(), fontSize = 18.sp) Text(foo.value.d.toString(), fontSize = 18.sp) Text(foo.value.e.toString(), fontSize = 18.sp) Text(foo.value.f.toString(), fontSize = 18.sp) Text(foo.value.g.toString(), fontSize = 18.sp) Button( onClick = { // 生成新实例,只修改需要变更的属性,其他属性保持不变 foo.value = foo.value.copy(a = 69) }, modifier = Modifier.topPadding(16.dp) ) { Text("Change some value") } } }
这样修改后,点击按钮时foo.value被赋值为新的MyDataClass实例,Compose能立即感知到状态变化,触发所有依赖foo.value的Composable重组,UI也就跟着更新了。
这个方案的优势很明显:
- 数据类的属性都是普通Int类型,代码简洁符合常规编程习惯
- 遵循Compose状态设计原则,避免可变状态的隐患
- 不需要给每个属性套
MutableState,维护成本极低
另一种可选方案:手动触发重组
如果你不想修改原有的类结构,还有个更直接的小技巧——手动获取RecomposeScope,修改属性后手动触发重组:
@Composable fun DemoScreen() { val foo = MyDataClass() // 获取当前Composable的重组作用域 val recomposeScope = currentRecomposeScope Column(Modifier.padding(16.dp)) { // 展示属性... Button( onClick = { foo.a = 69 // 手动触发当前Composable的重组 recomposeScope.recompose() } ) { Text("Change some value") } } }
这个方案适合简单的小场景,但要注意:如果有多个地方修改属性,每个地方都要调用recompose(),而且手动触发可能会导致不必要的UI刷新,只推荐在小范围场景使用。
内容来源于stack exchange

