第2章 核心三件套:状态、重组与副作用
说实话,很多人学Compose上来就怼布局,写一堆Column、Row,觉得挺简单。但真到做复杂应用时,状态一乱,UI就不受控了。我见过太多这样的项目——界面看着漂亮,一交互就崩。
要想用好Compose,你得先搞明白三个核心概念:状态驱动、重组、副作用。说白了,它们就是Compose世界的运行法则。搞不懂这几样,后面做离线架构肯定踩坑。
状态管理:别让UI牵着鼻子走
Compose是响应式的。状态变了,UI自动变。你不用手动调用setText()或notifyDataSetChanged()。我个人觉得,这是Compose最爽的地方——但也是最容易翻车的地方。
什么是状态? 说白了,就是随时间变化的数据。比如列表加载中、用户输入的文字、开关的选中状态。这些数据一旦变了,UI要跟着更新。
Compose管理状态的核心工具是mutableStateOf()。它包装一个值,当值变化时,Compose会自动重新组合那些读取了这个值的Composable函数。你想想看:
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("点击了 $count 次")
}
}
这里count变了,只有Text会重新显示。其他部分不受影响。嗯,这就是细粒度更新。
| 状态类型 | 定义位置 | 作用域 | 生命周期 | 示例 |
|---|---|---|---|---|
| 局部状态 | Composable内部 | 单个界面 | 随Composable消失 | remember { mutableStateOf(...) } |
| 提升状态 | 父Composable | 多个子界面共享 | 由父控制 | 参数传递 + (String, (String)->Unit) |
| ViewModel状态 | ViewModel | 整个界面/导航 | 比UI长(配置变化存活) | viewModel.uiState.collectAsState() |
remember就够了。一旦状态需要跨屏幕共享、或者需要生命周期的庇护(比如旋转屏幕不丢失),果断上ViewModel里的StateFlow。
我在项目中遇到过把网络请求结果直接扔给Composable做参数的坑,结果每次请求回来整个界面闪一下。后来我才明白,要把状态提升到ViewModel里统一管理。避免重复请求和状态混乱。
重组:Compose的“脏检查”机制
重组(Recomposition)这个词,你肯定听烂了。究竟它是什么?
重组不是全量重绘。 Compose只更新那些输入变了的部分。你想想看,如果每次状态变化都把UI所有部分重新画一遍,那性能岂不是崩塌?
Compose的聪明之处在于:它把UI组织成一颗函数树。状态变化后,只重新执行那些“读了这个状态”的Composable函数。其他节点保持不变。
举个我踩过的坑:
@Composable
fun MyScreen() {
var text by remember { mutableStateOf("") }
// 大列表,每次text变化都重走一遍?
LazyColumn {
items(100) { index ->
Text("Item $index") // 根本不用读 text
}
}
TextField(value = text, onValueChange = { text = it })
}
按说 LazyColumn 不读 text,所以它不该跟着 text 变化。对的,Compose会跳过它。我当时自己验证了一遍,才放下心来。
重组触发条件:
mutableStateOf的值变化StateFlow/LiveData发射新值(通过collectAsState()转换)- Composable 的输入参数变化
key()调用的值变化
LaunchedEffect没有指定key,导致每次重组都重新启动协程。千万别干这事。
副作用:和外部世界打交道
Composable函数应该是一个纯函数——只基于输入输出。但实际应用中,你得发起网络请求、订阅数据库、更新状态栏。这些“外部操作”就是副作用。
Compose提供了几个特殊的Effect来处理它们:
- LaunchedEffect:在进入组合时启动协程,退出时自动取消。适合一次性的网络请求。
- DisposableEffect:你需要在离开时清理资源?用它。比如注册广播接收器。
- SideEffect:每次重组都执行。适合记录日志、更新外部状态。
- rememberCoroutineScope:在Composable外部启动协程,比如按钮点击后触发。
@Composable
fun UserProfile(userId: String) {
var userData by remember { mutableStateOf<User?>(null) }
// LaunchedEffect 仅在 userId 变化时重新执行
LaunchedEffect(userId) {
userData = repository.fetchUser(userId)
}
// ...
}
我在项目中使用DisposableEffect来注册网络状态监听。离开页面时取消注册,这样不会内存泄漏。
DisposableEffect(Unit) {
val listener = NetworkCallback { isOnline ->
// 更新状态
}
connectivityManager.registerDefaultNetworkCallback(listener)
onDispose {
connectivityManager.unregisterNetworkCallback(listener)
}
}
最佳实践与避坑指南
launch或async。没有Effect包装,每次重组都会新发起请求。我曾经因为这事被测试妹子追着问:“为什么我点一次按钮,后台收到十个请求?”还有一个小技巧:用 remember 加 key 减少不必要的计算。比如:
val sortedList = remember(list, sortType) {
list.sortedWith(getComparator(sortType))
}
只有 list 或 sortType 变了才重新排序。我之前有个列表页,每次滑动都卡,查了才发现每次重组都重新计算复杂排序。用remember加key之后,流畅多了。
总之——状态是数据,重组是机制,副作用是桥梁。三样东西配合好了,离线架构才能稳稳落地。下一部分我们就要动手搭建真正的离线框架了,这些基础一定要啃透。