概述
在Jetpack Compose的世界里,状态(State)作为驱动UI更新的核心,其管理机制的掌握程度,直接关系到能否构建出响应迅速、稳定且易于维护的界面。深入理解并熟练运用Compose的状态管理,是开发者提升技能、打造优质应用的必经之路。
1.Compose 状态是什么?
在Android开发场景中,状态指代那些随时间推移发生变化,并对UI展示效果产生影响的数据。就像输入框里不断变化的文本内容、按钮被点击的累计次数,以及加载数据后得到的结果等,都属于状态的范畴。
传统的View系统通过findViewById获取控件,再手动更新视图,操作较为繁琐。而Compose则截然不同,它以数据驱动UI,数据一旦发生改变,UI便会自动重新绘制,也就是进行重组。这使得管理和保存这些动态变化的数据,成为Compose状态管理的重中之重。那我们如何管理和创建状态?
2.Compose框架如何管理和创建状态?(mutableStateOf和remember)
2.1 mutableStateOf
在Compose中,mutableStateOf是管理可变状态的得力工具。它创建的状态对象能在UI中被观察到,一旦状态有变动,UI就会自动更新。比如下面这段代码,用mutableStateOf来记录按钮的点击次数:
@Composable
fun Counter() {
// 使用mutableStateOf创建可变的状态,通俗讲:就是将数据保存到状态中,我们一般直接把状态当成了数据。
var count = mutableStateOf(0)
Column {
Text(text = "点击次数: ${count.value}")
Button(onClick = { count.value++ }) {Text("点击我")}
}
}
在这个示例里,mutableStateOf(0)创建了一个可观察的状态对象,count变量存储着该状态的值。每当按钮被点击,count.value++更新值,进而触发UI更新。通俗讲,Compose其实是将数据保存到状态中,当然我们一般把状态(State)当成数据。
然而,这段代码存在一个潜在问题:每次UI更新(即重组)时,Counter()函数都会重新执行,这就导致count每次都会被重置为0。所以每次点击按钮,count看起来都没有变化。如何解决这个问题?
2.2 remember
为了避免状态在重组过程中丢失,Compose提供了remember函数。remember能够在同一次重组中保存状态,确保状态数据在重组时不会被重置。结合remember和mutableStateOf,就能解决上述问题:
@Composable
fun Counter() {
// 使用mutableStateOf创建可变的状态,通俗讲:就是将数据保存到状态中,我们一般直接把状态当成了数据。
var count by remember { mutableStateOf(0) }
Column {
Text(text = "点击次数: $count")
Button(onClick = { count++ }) {Text("点击我") }
}
}
这里,remember { mutableStateOf(0) }保证了count在同一次重组中维持状态。当按钮点击时,count会正常增加,UI也会随之实时更新。
remember和mutableStateOf的底层原理
mutableStateOf本质是一个State<T>对象,内部运用了观察者模式。一旦状态改变,Compose会通知相关的Composable函数重新执行,从而更新UI。
remember则是基于缓存机制实现的,它能在当前组合范围(Composition)内缓存数据,有效防止UI重组时状态丢失。
上面的例子可以翻译成下面这样子。currentComposer就是当前Activity的Compose
//缓存到当前Activity的Composer中即:currentCompose
var count by currentComposer.cache(false){mutableStateOf(0) }
下篇文章会详细讲currentCompose 和currentRecompsoe
3. Compose重组机制(Recomposition)
3.1 重组是如何工作的?
重组是Compose的核心特性,当状态发生变化时,Compose会重新执行受影响的Composable函数,并重新绘制UI,以此实现UI对数据变化的动态响应。
当修改State对象的值(比如通过mutableStateOf修改),Compose会检测到变化,并标记需要更新的Composable。随着这些Composable重新执行,UI会依据新数据重新呈现。
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Log.d("Compose", "Counter重组")
Column {
Text("点击次数: $count")
Button(onClick = { count++ }) {
Text("点击我")
}
}
}
在这个例子中,每次按钮点击使count更新,Compose就会触发重组。通过Log输出可以看到,每次点击按钮,Counter Composable都会重新执行,并在日志中输出"Counter重组"。
与传统Android开发中手动调用invalidate()或setText()方法触发UI更新不同,Compose的UI更新完全由数据驱动,状态变化时UI自动更新。
3.2 重组的精细化控制
Compose高效的重组机制是其一大优势,即使状态变化,也不会导致整个Composable函数UI重新绘制,而是仅更新最小范围的UI。
-
局部更新:Compose只会重组受状态变化影响的部分Composables。比如按钮点击次数变化时,只有显示次数的
Text组件会更新,而不会重新创建整个Counter组件。 - 避免不必要的重组:Compose通过智能比较,精准判断哪些Composables需要更新,有效避免了重复计算和UI渲染,极大地优化了性能。
比如上述Conuter的例子,状态变化依然会导致整个Compose函数UI重新绘制的,因为重组作用域是根据距离状态变量最近的read代码处的非inline函数和非inline lambda块 。inline 函数或lambda比如 Column和Row,由于Column是inline函数,往上就是更新整个Compose函数了,如果我们加一层wrapper,那么重组作用域就是wrapper函数内。
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Log.d("Compose", "Counter重组")
Column {
TextWrapper{
Text("点击次数: $count")
}
Button(onClick = { count++ }) {
Text("点击我")
}
}
}
fun TextWrapper(content:@Composable ()->Unit){
content()
}
3.3 重组的执行过程
-
触发重组:当
mutableStateOf的值改变,Compose会标记(Recorder)对应的Composable需要重新执行。 - 计算新的UI:Compose重新执行该Composable,生成新的UI树(UI结构)。
- 更新UI:Compose将新UI树和当前UI树对比,仅更新有变化的部分,高效呈现更新后的界面。
3.4 为什么要关注重组?
深入理解Compose的重组机制对开发者意义重大:
- 避免性能问题:确保不会出现不必要的UI更新,优化应用性能,比如:TextWrapper尽量缩小重组作用域。
- 提高响应性:保证UI始终与状态同步,为用户带来流畅的体验。
4. remember vs rememberSaveable区别
remember只能在内存中保存状态,适用于短生命周期的数据。而rememberSaveable支持持久化,即使进程被杀死或发生配置更改(如旋转屏幕),也能恢复状态。
4.1 rememberSaveable与remember的对比
remember和rememberSaveable都用于在Compose中保存和恢复状态,但在处理配置变化和进程销毁的方式上有所不同。
-
remember:仅在组件重组时保留状态,遇到配置变化(如屏幕旋转)或进程销毁,状态会丢失。
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Column {
Text("点击次数: $count")
Button(onClick = { count++ }) {
Text("点击我")
}
}
}
-
rememberSaveable:类似remember,但它会将状态保存在Bundle中,在配置变化时恢复状态,适用于需要持久保存状态的场景,如表单输入。
@Composable
fun Counter() {
var count by rememberSaveable { mutableStateOf(0) }
Column {
Text("点击次数: $count")
Button(onClick = { count++ }) {
Text("点击我")
}
}
}
两者的关键区别就在于,rememberSaveable可以在配置变化时恢复状态,而remember仅在组件重组时保存状态。
rememberSaveable的原理
rememberSaveable借助Bundle来保存状态,这使得状态在配置变化时能够恢复。比如屏幕旋转或进程销毁后重新启动,状态会自动恢复。
4.1 与 remember 的区别
为了方便大家快速查阅,下面以表格形式梳理了rememberSaveable与 remember 的区别:
| 特性 | remember | rememberSaveable |
|---|---|---|
| 生命周期 | 仅在组合期间保持状态 | 在配置更改后也能恢复状态 |
| 状态保存 | 不支持 | 支持通过 Bundle 保存和恢复状态 |
| 适用场景 | 短暂状态(如动画、临时变量) | 长期状态(如表单数据、分页位置) |
5. 状态提升(State Hoisting)
状态提升是把状态从子组件提取到父组件,实现UI与状态管理的解耦。这样做能提升组件的复用性、可测试性,还能让多个组件共享相同状态。
5.1 状态提升的实际应用
以计数器功能为例,为确保状态在重组时不丢失,将状态提升到父组件管理:
@Composable
fun ParentComponent() {
var count by remember { mutableStateOf(0) }
Counter(count, onIncrement = { count++ })
}
@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
Column {
Text("点击次数: $count")
Button(onClick = onIncrement) {Text("点击我")}
}
}
在这个示例中:
-
ParentComponent组件负责管理count状态,并通过count和onIncrement回调传递给Counter组件。 -
Counter组件只负责展示文本框和响应用户输入,实际状态由父组件控制。
这种方式大大提升了Counter组件的复用性,无论有多少个Counter组件,都能通过父组件共享和管理同一个计数器状态。
状态提升的优势明显:
-
复用性:
Counter组件变得独立且无状态,可在多个地方复用。 - 解耦性:UI展示和状态管理分离,增强了可维护性和可测试性。
5.2 什么时候不需要状态提升?
并非所有场景都适合状态提升。在一些简单的、状态仅在组件内部有效的情况下,直接在组件内部管理状态会更简洁。比如显示计时器的组件,其状态只在组件内部使用,无需与外部共享,就没必要进行状态提升:
@Composable
fun Timer() {
var time by remember { mutableStateOf(0) }
LaunchedEffect(true) {
while (true) {
delay(1000)
time++
}
}
Text("计时器: $time")
}
这里,Timer组件内部管理time状态,无需与父组件交互,在内部管理状态就足够了。
6. Compose与ViewModel结合
通常会借助ViewModel来持有和管理状态,确保数据在组件生命周期内得以保存。将Compose和ViewModel结合,能实现更灵活、稳定的状态管理。
6.1 ViewModel + StateFlow
ViewModel用于管理和存储与UI相关的数据,StateFlow和LiveData是Compose中常用的可观察数据类型。通过collectAsState(针对Flow)或observeAsState(针对LiveData),Compose能自动观察数据变更并更新UI。
- 示例:使用
StateFlow
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count
fun increment() {
_count.value++
}
}
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
// collectAsState会自动观察StateFlow数据,并更新UI
val count by viewModel.count.collectAsState()
Column {
Text("点击次数: $count")
Button(onClick = { viewModel.increment() }) {
Text("点击我")
}
}
}
此例中,StateFlow用于管理计数器状态,collectAsState自动监听StateFlow变化并更新UI。
- ViewModel + LiveData
class CounterViewModel : ViewModel() {
private val _count = MutableLiveData(0)
val count: LiveData<Int> = _count
fun increment() {
_count.value = (_count.value ?: 0) + 1
}
}
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
// observeAsState会自动观察LiveData数据,并更新UI
val count by viewModel.count.observeAsState(0)
Column {
Text("点击次数: $count")
Button(onClick = { viewModel.increment() }) {
Text("点击我")
}
}
}
这里,LiveData管理计数器状态,observeAsState自动监听LiveData变化并更新UI。
collectAsState(适用于StateFlow)和observeAsState(适用于LiveData)都能自动监听数据变化,并及时将变化反映到UI上。StateFlow和LiveData都是响应式的,数据变化时会自动通知Compose触发UI更新。
7. 总结
状态是Compose的核心要素,直接驱动UI更新。在实际开发中,用mutableStateOf创建可变状态,结合remember保留状态,防止重组时数据丢失。rememberSaveable适用于需要持久化的状态场景。状态提升模式能解耦UI与数据,提升组件复用性和可测试性。与ViewModel配合使用,则能在复杂应用中保障数据的长期存活和稳定性。
深入理解Compose状态管理机制,有助于开发者更高效、优雅地构建响应式UI,提升应用性能和用户体验,在Android开发的道路上迈出坚实的步伐。
其实我们开发Compose 遵循以下4要点:
- 获取数据并处理成想要的数据;
- 把数据保存到状态(State);
- 讲状态(State)关联到Compose UI。
- 专注更新状态(State)