要点
remember 负责保留值,mutableStateOf 负责被观察的读写。重组重复计算界面描述,不应该在计算过程中发请求或改业务数据。
目录
什么是 @Composable
@Composable 不是一个类,只是函数上的注解,但它改变了函数的能力:
@Composable
fun MessageCard(name: String) {
Text("Hello, $name")
}
三条心智规则:
- 函数即 UI: 调用
MessageCard("Ada")就是把它的内容”挂”到界面树上,可任意嵌套组合 - 幂等可重入: Compose 可能随时重新调用这个函数(重组),同一输入应产生同一界面——所以不要在函数体内写副作用(弹 toast、改数据库)
- 顺序执行: 函数体按顺序描述界面,类似”DSL 形式的布局文件”
💡 与 React 类比:
@Composable≈ 函数组件,Modifier≈ props 中的样式,重组 ≈ re-render。
第一个可交互组件:计数器
直接用普通变量”记”状态是无效的,看正确写法:
@Composable
fun Counter() {
// remember:让值跨重组存活;mutableStateOf:让值的变化可被 Compose 观察
var count by remember { mutableStateOf(0) }
Column {
Text("点击了 ${count} 次")
Button(onClick = { count++ }) {
Text("点我")
}
}
}
运行后每点一次按钮,文本自动更新。这背后发生了什么:
count++改变了MutableState的值- Compose 观察到”读取该状态的 Composable”需要更新
- 重新执行
Counter()函数体(重组),Text拿到新的count渲染 remember返回缓存中的同一状态对象,而不是重新初始化为 0
by 委托语法让我们直接读写 .value,比 count.value++ 简洁(详见泛型与委托属性)。
重组 Recomposition
定义: 状态变化后,Compose 重新执行受影响的 Composable 函数以更新界面树的过程。
关键特性:
| 特性 | 说明 |
|---|---|
| 智能范围 | 使读取状态的相关重组作用域失效;实际执行与跳过还取决于编译器、稳定性和读取位置 |
| 可跳过 | 符合跳过条件的 Composable 可能被跳过;比较与稳定性规则取决于编译配置,不能据此保证执行次数 |
| 可能频繁 | 动画、滚动、输入每帧都可能触发,因此 Composable 应轻量 |
| 顺序不定 | 不要依赖多个 Composable 的执行顺序,也不要依赖重组次数 |
@Composable
fun Profile(name: String, age: Int) {
Column {
Header(name) // 若 name 没变,Header 可被跳过
AgeBadge(age) // 若 age 变了,只有它重新执行
}
}
📖 状态 API(
State/derivedStateOf/snapshotFlow等)的完整字典见 Compose 状态 API 详解。
remember 与状态持久性
remember 的作用域是”该组件在界面树上的存在期”:
var query by remember { mutableStateOf("") } // 重组间存活
var query2 by rememberSaveable { mutableStateOf("") } // 还能存活于配置变更/进程恢复
remember: 重组不丢,但旋转屏幕、进程被杀后丢失rememberSaveable: 自动把值写入Bundle,能扛住配置变更(需可序列化类型)- ViewModel: 需要跨配置变更且包含业务逻辑的状态可放到 ViewModel;ViewModel 本身不保证进程重建后恢复(见协程与 Flow 基础)
一个常见递进:简单 UI 状态用 remember → 需要跨配置变更 → rememberSaveable → 涉及业务 → ViewModel + StateFlow。
状态提升
原则: 把状态从子组件”提升”到调用方,子组件变成无状态(stateless)组件。
// ❌ 有状态组件:内部锁死状态,难以复用与测试
@Composable
fun NameInputBad() {
var name by remember { mutableStateOf("") }
OutlinedTextField(value = name, onValueChange = { name = it })
}
// ✅ 无状态组件:状态由外部传入,事件向上回调
@Composable
fun NameInput(
name: String, // 状态下行(参数)
onNameChange: (String) -> Unit, // 事件上行(回调)
modifier: Modifier = Modifier
) {
OutlinedTextField(
value = name,
onValueChange = onNameChange,
modifier = modifier
)
}
// 调用方持有状态,可同时服务多个场景(预览、表单、撤销栈……)
@Composable
fun Form() {
var name by remember { mutableStateOf("") }
NameInput(name = name, onNameChange = { name = it })
}
这就是 单向数据流(UDF): 状态向下流动、事件向上传递。后续 ViewModel + StateFlow 的架构正是这个模式在页面级的放大。
常见误区
误区一:在组合期间写状态 → 无限重组死循环
@Composable
fun Broken() {
var count by remember { mutableStateOf(0) }
count++ // ❌ 组合中直接改状态:改完触发重组 → 再改 → 死循环崩溃
Text("$count")
}
修正:由事件、业务状态持有者或合适的副作用 API 发起变化,避免在描述 UI 时直接写入自己正在读取的状态。
误区二:普通变量当状态用
@Composable
fun AlsoBroken() {
var count = 0 // ❌ 每次重组都重新初始化为 0,界面永远不变
Button(onClick = { count++ }) { Text("$count") }
}
误区三:副作用直接写在函数体里
发送网络请求、写数据库等操作请放入 LaunchedEffect/ViewModel,明确工作生命周期:LaunchedEffect 在进入组合及 key 改变时启动,离开组合时取消;ViewModel 的工作遵循自身作用域。
更多疑难现象(重组死循环、状态丢失、协程泄漏)的排查手册见故障排除速查。
练习与实践
基础练习
- [ ] 复现计数器,并新增”重置”按钮把 count 归零
- [ ] 把计数器改造成无状态组件
Counter(count: Int, onIncrement: () -> Unit),在父组件中持有状态 - [ ] 用
OutlinedTextField+remember实现一个”输入即显示字数”的组件 - [ ] 把计数器的
remember换成rememberSaveable,旋转屏幕对比状态是否保留
进阶挑战
- [ ] 实现一个待办输入框:状态为
data class Draft(val title: String, val done: Boolean),验证输入后界面正确刷新 - [ ] 故意写出组合期间写状态的死循环代码,观察 Logcat 报错信息并记录修复方法
- [ ] 用两个并排的
NameInput共享同一个状态源,体会状态提升带来的复用性
相关文档
- 📖 State and Jetpack Compose – 官方状态文档
- Compose 布局
- Kotlin 语法要点
- Compose Navigation
- 第一个 Compose 应用