B心智模型
先建立声明式与 UI = f(state),再学积木和状态。看不懂这一章,后面会一直觉得「怪」。
🔗 引用:心智模型 / 声明式 UI
命令式 vs 声明式
概念
命令式:界面是一棵已经建好的 View 树。你改数据后,还要自己找到控件,再 setText / setEnabled / notify… 把变化「推」上去。
声明式:你只描述「当前状态下界面应该长什么样」。状态变了,框架重跑相关的 @Composable,把界面对齐到新描述。
和 XML 对照
| 维度 | XML 命令式 | Compose 声明式 |
|---|---|---|
| 你写什么 | 怎么改 View | 界面「是什么」 |
| 数据变了 | 自己 setXxx / notify | 改状态,等重组 |
| 找控件 | findViewById / Binding | 不需要,直接是参数/表达式 |
| 常见坑 | 改了数据忘刷新 | 在 Composable 里乱写副作用 |
怎么用(最小对照)
XML + 代码
// Activity 里
btn.setOnClickListener {
count++
tvCount.text = count.toString()
}
Compose
@Composable
fun CounterDemo() {
var count by remember { mutableStateOf(0) }
Column {
Text("$count")
Button(onClick = { count++ }) {
Text("+1")
}
}
}
📌 示例边界:上面 Kotlin 片段写在
@Composable fun CounterDemo() 体内。状态与组件不能写在函数外。易错点
- 还用「先拿到控件引用再改」的思维——Compose 里没有长期存活的 TextView 句柄给你改。
- 把声明式理解成「不用管状态」——恰恰相反:状态才是唯一真相,要认真管。
▶ 同一登录按钮:XML 命令刷新 vs Compose 声明
需求:账号、密码都填好后,登录按钮才可点。XML 要监听两个输入框,再手动刷 isEnabled;Compose 让按钮的 enabled 直接读状态表达式。
XML 命令式
etUser.addTextChangedListener { refresh() }
etPwd.addTextChangedListener { refresh() }
fun refresh() {
btnLogin.isEnabled =
etUser.text.isNotBlank() &&
etPwd.text.length >= 6
}
Compose 声明式
@Composable
fun LoginButtonDemo() {
var user by remember { mutableStateOf("") }
var pwd by remember { mutableStateOf("") }
val canLogin = user.isNotBlank() && pwd.length >= 6
Button(
onClick = { /* 登录 */ },
enabled = canLogin
) {
Text("登录")
}
}
要点:XML 要「记得刷新」;Compose 把「能不能点」写成状态的函数,状态一变按钮外观跟着变。
💡 复制 Compose 侧到 Studio,加 @Preview 改 user/pwd 初值即可看按钮亮灭。@Preview
UI = f(state)
概念
把界面看成函数:输入是状态,输出是当前 UI 描述。同一份状态,多次运行应得到同一份界面(纯函数直觉)。
核心公式:
把「同步数据和界面」从你手里移交给框架。重组、状态提升、单向数据流,都是这个公式的推论。
UI = f(state)把「同步数据和界面」从你手里移交给框架。重组、状态提升、单向数据流,都是这个公式的推论。
和 XML 对照
- XML:数据在一边,View 在另一边,靠你写胶水对齐。
- Compose:数据(状态)进函数,界面是输出;对齐由运行时完成。
怎么用
- 先问:这一屏「真相」是哪些字段?(如
user、pwd、loading) - 界面只读这些字段来画;事件里只改字段,不直接「命令某个控件」。
- 细节(
remember/ ViewModel)放到状态与数据流章。
易错点
- 在 Composable 里改状态又立刻依赖「改完后的副作用顺序」——容易和重组打架。
- 把业务状态散落在多个互不通信的局部变量里,界面会对不齐。
重组的直觉
概念
重组(Recomposition):状态变化时,Compose 只重跑读过该状态的 Composable,不是整页死刷,也不是「销毁再建整个 Activity」。
和 XML 对照
| XML 时代 | Compose |
|---|---|
invalidate() / 整段重绘 | 按状态依赖重跑函数 |
Adapter notifyItemChanged | Lazy 列表 + 状态变化驱动 item |
| 你决定「刷谁」 | 运行时根据「谁读了状态」决定 |
怎么用(先有直觉)
- 把会变的数放进
mutableStateOf(后面章节细讲),在Text("$count")里读它——读过的地方才会因它而重组。 - 没读到该状态的兄弟节点,理想情况下会被跳过。
🔗 引用:重组与性能
易错点
- 以为重组 = 整屏闪一下重画——多数时候只是相关函数再执行一遍。
- 在 Composable 里直接开网络、注册监听——重组可能反复执行;副作用放到后面的副作用边界章。
- 状态放得太高:改一个角标,半屏都跟着重跑(状态尽量靠近真正用它的地方)。
组合优于继承
概念
Compose 鼓励用小函数组合出界面:父组件调用子组件,用参数和内容 lambda(插槽)拼装。少去继承一套自定义 View / ViewGroup。
和 XML 对照
XML 常见路子
自定义 XxxView 继承 FrameLayout,重写测量/绘制,或再套一层布局 XML。复用靠继承树变深。
Compose 路子
写 @Composable fun ProfileHeader(...),在需要的地方调用;要变外观就加参数或再包一层 Composable,而不是开子类。
怎么用
@Composable
fun ProfileHeader(name: String, onEdit: () -> Unit) {
Row {
Text(name)
Button(onClick = onEdit) {
Text("编辑")
}
}
}
@Composable
fun ProfileScreen() {
Column {
ProfileHeader(name = "Kang", onEdit = { /* ... */ })
// 其它区块继续组合
}
}
易错点
- 把 Activity/Fragment 继承思维搬过来,做一个巨大的「上帝 Composable」——应拆成可组合的小块。
- 为了「扩展性」过早抽象;先复制再抽,比先造深继承更贴合 Compose。