2、Compose核心概念:声明式UI思想、Composable函数、重组(Recomposition)、状态与事件、单向数据流
好,咱们正式开始啃 Compose 的核心概念。
说实话,我刚从传统 View 体系切换到 Compose 时,最大的感受就是——脑子得换个操作系统。以前写 XML 布局,再在 Activity 里 findViewById,然后手动更新 UI。这套流程我用了快十年,肌肉记忆都形成了。但 Compose 不一样,它逼着你用全新的方式思考 UI 的构建。
这一节,我会把五个最核心的概念串起来讲。你理解了它们,就相当于拿到了 Compose 世界的钥匙。
2.1 声明式UI思想:你告诉它“是什么”,而不是“怎么做”
传统 View 系统是命令式的。你得像下棋一样,一步步告诉系统:
- “找到这个 TextView”
- “把它的文字改成‘你好’”
- “再把这个按钮变灰”
代码里全是 findViewById、setText、setEnabled 这种操作。UI 状态一多,代码就乱成一锅粥。
Compose 用的是声明式思想。你只需要描述 UI 在当前状态下应该长什么样。状态变了,UI 自动跟着变。
举个例子。以前写一个点赞按钮:
// 命令式
if (isLiked) {
likeButton.setImageResource(R.drawable.ic_liked)
likeButton.setColorFilter(Color.RED)
} else {
likeButton.setImageResource(R.drawable.ic_unliked)
likeButton.setColorFilter(Color.GRAY)
}
在 Compose 里,你直接写:
// 声明式
@Composable
fun LikeButton(isLiked: Boolean) {
Image(
painter = if (isLiked) painterResource(R.drawable.ic_liked)
else painterResource(R.drawable.ic_unliked),
contentDescription = null,
colorFilter = if (isLiked) ColorFilter.tint(Color.Red)
else ColorFilter.tint(Color.Gray)
)
}
看到了吗?没有 if-else 的赋值操作。UI 完全由数据驱动。我个人觉得,这种思维转变是学习 Compose 最大的门槛。一旦跨过去,后面就顺了。
2.2 Composable函数:UI 的基本积木块
在 Compose 里,一切 UI 都是函数。这个函数用 @Composable 注解标记,我们叫它可组合函数。
它有几个特点:
- 无返回值:它不返回 View 对象,而是直接发射 UI 节点到组合树里
- 可调用其他 Composable 函数:就像搭积木,小的拼成大的
- 执行顺序不保证:别依赖代码行号来推断 UI 顺序
一个最简单的例子:
@Composable
fun Greeting(name: String) {
Text(text = "你好,$name!")
}
@Composable
fun MyScreen() {
Column {
Greeting("小明")
Greeting("小红")
}
}
嗯,这里要注意:Composable 函数不能直接写在普通函数里。它只能在另一个 Composable 函数的作用域内被调用。这是 Compose 编译器插件的硬性要求。
Greeting.kt 里放 Greeting() 函数。这样找代码时不用动脑子。
2.3 重组(Recomposition):UI 自动更新的秘密
重组是 Compose 最神奇的地方。当状态发生变化时,Compose 不会重新执行整个 UI 树,而是只重新执行那些依赖该状态的 Composable 函数。
我举个例子你就明白了:
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Column {
Text("当前计数:$count")
Button(onClick = { count++ }) {
Text("点击加一")
}
}
}
当你点击按钮时,count 变了。Compose 会检测到 Text("当前计数:$count") 依赖了 count,于是只重组这个 Text。Button 和 Column 都不会重新执行。
为什么会这样?因为 Compose 在编译期就分析出了每个 Composable 函数读取了哪些状态。这叫跳过(Skipping)优化。
2.4 状态与事件:UI 的输入和输出
在 Compose 的世界里,状态是向下传递的,事件是向上传递的。这是单向数据流的核心。
状态就是 UI 的数据。比如:
- 输入框里的文字
- 开关是否打开
- 列表的数据源
事件就是用户的操作。比如:
- 点击按钮
- 滑动列表
- 输入文字
看一个完整的例子:
@Composable
fun LoginScreen() {
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
// 状态向下传递
TextField(
value = username,
onValueChange = { username = it }, // 事件向上传递
label = { Text("用户名") }
)
TextField(
value = password,
onValueChange = { password = it },
label = { Text("密码") }
)
Button(onClick = { /* 登录逻辑 */ }) {
Text("登录")
}
}
}
你想想看,username 这个状态在顶层定义,然后传给 TextField。TextField 不直接修改状态,而是通过 onValueChange 回调把新值传回来。这就是状态提升。
2.5 单向数据流:让数据变化可预测
单向数据流说白了就是:数据只能从父流向子,不能反过来。
如果子组件想改变数据,它得通过回调通知父组件,由父组件来修改。这样数据的变化路径是清晰的,调试起来特别方便。
我画了一张图,帮你理解这个流程:
你看,箭头方向很清晰。父组件持有状态,传给子组件展示。子组件不能直接改状态,只能通过回调说“用户点了按钮”。父组件收到回调后,自己修改状态,然后触发重组。
这样做的好处是什么?
- 可预测:数据变化路径唯一,不会出现“这个值到底是谁改的”这种问题
- 可测试:给相同的状态,UI 输出是确定的
- 可调试:出问题时,顺着数据流往上查就行
2.6 把这些概念串起来
咱们用一张表格总结一下这五个概念的关系:
| 概念 | 一句话理解 | 我的经验 |
|---|---|---|
| 声明式UI | 描述“是什么”,不是“怎么做” | 思维转换是最大门槛 |
| Composable函数 | UI 的基本积木块 | 文件名和函数名保持一致 |
| 重组 | 状态变了,只更新依赖它的部分 | 别在 Composable 里写副作用 |
| 状态与事件 | 状态向下,事件向上 | 状态提升让代码更干净 |
| 单向数据流 | 数据只能从父流向子 | 调试时你会感谢这个设计 |
说白了,Compose 的核心就是用函数描述 UI,用状态驱动更新,用事件处理交互。这三个点抓住了,后面学什么组件、什么布局都不怕。
嗯,这一节的内容就到这。你先把这几个概念消化一下,尤其是“状态提升”和“重组”这两个,后面几乎每节课都会用到。
公众号:蓝海资料掘金营,微信deep3321