从Android 开发,踏入到Compose 之旅的时候,我相信有很多小伙伴和我一样,从命令式UI 转化到声明式UI,总是不太习惯。但像Flutter、Swift、ArkUI、Compose 这些声明式UI 逐渐走向了主流。今天我们就一起聊聊Compose 的声明式UI,走进声明式UI大门。
想了解更多Compose Multiplatform项目的小伙伴,可以看看之前的文章
- Compose Multiplatform 之旅 --- 启程
- Compose Multiplatform 之旅 --- 项目初探
- Compose Multiplatform 之旅 ---做一个自己的项目(别踩白块)
- Compose Multiplatform 之旅---看看大佬在做啥
- Compose Multiplatform 之旅---为什么可以跨平台
差异
命令式UI
- 开发者手动操作 UI 控件,每次状态改变都需要显式更新,强调"如何做"。
- UI是View树,每个节点都是View 和ViewGroup对象
- 依赖View系统模块化较差,需要主要更新UI,代码量相等较多
声明式UI
- 开发者只需声明 UI 与状态的关系,状态改变时系统会自动更新,强调"应该是什么"
- UI是Compose UI 树,Composable 函数生成的节点组成。这些节点描述了 UI 的结构、状态和行为。
- 高度模块化,函数易于重用,更少的代码
命令式 UI 就像是手动画画,你一笔一笔画,控制每个细节。 声明式 UI 就像描述一幅画的内容,然后系统自动画出来。
思维转变
- 忘掉 View 引用:在 Compose 中,不再需要手动获取和操作 View。
- 描述 UI 状态:将 UI 视为状态的结果,而不是逐步修改的对象。
- 状态管理驱动:UI 根据状态变化自动更新,你只需管理好状态。
- 关注数据流向:数据 -> 状态 -> UI。
核心思想
Compose 核心思想:UI 是状态的函数,即 UI = f(状态)。
函数式编程
Compose 之所以能够如此简便,得益于Kotlin的各种语法糖,像常见的回调函数,我们就可以省略无关的逻辑,直接通过大括号的形式进行回调。
kotlin
fun doSomething(onComplete: () -> Unit) {
println("Doing something...")
onComplete() // 调用回调
}
fun main() {
doSomething {
println("Task completed!")
}
}
有了这样方便的简写,就可以很直观明了的写出UI结构了,和原来的xml 的层级一样。
kotlin
@Composable
fun Greeting(msg: String){
Column {
Text("一级1")
Text("一级2")
Column {
Text("二级1")
Text("二级2")
}
}
}
@Composable
inline fun Column(
...
content: @Composable ColumnScope.() -> Unit
) {
...
}
之前第一次接触到这种形式的时候,还蛮好奇的。仔细研究,发现Column 的{} 里面其实就是一个@Composable ColumnScope.() -> Unit的函数参数,使用Kotlin的语法糖之后变得十分简洁。 框架会在初始,和合理的时机调用这个函数,获取到UI,去进行展示。
@Composable 注解
看到上面的代码,是不是很好奇@Composable注解到底有什么用。和常规JAVA的注解不一样,用于APT生成代码。这里是会在编译器编译时,对函数进行亿点点操作。
kotlin
//原始代码
@Composable
fun Greeting(msg: String){
Text("Hello $msg")
}
//编译器亿点点操作后
fun Greeting(msg: String,composer: Composer,key: Int){
composer.startRestartGroup(key)
if (composer.changed) {
Text("Hello, $msg", composer, 0)
}
composer.endRestartGroup()
}
看上面编译后的代码,可以看到Composable注解的函数会有一个composer: Composer的参数,以及一个key。
渲染时,系统会执行一个树的遍历,从顶层的 Composable 函数开始,逐步递归地构建出由以下类型组成的树:
- 组合树:表示 @Composable 函数的调用关系,描述 UI 的逻辑结构。
- 布局树:由 Modifier 和布局约束生成,描述实际的布局结构。
- UI 节点树:最终转换成显示在屏幕上的 View。
构建出来的树,不但能表示UI,还能感知状态的变化,当状态发生改变时,只通知和状态对应的树,从而避免无效的渲染提升性能。
不过这里生成的树的数据结构,不是简单的TreeNode,而是专门设计的一个SlotTable。内存占用小:SlotTable 是线性存储,避免了传统树形结构的指针开销;重组效率高:线性结构结合组层次信息,可以快速跳过未变的部分;动态性强:支持高效的增删操作,而传统树结构需要频繁调整子结点指针。这个数据结构比较复杂,感兴趣的伙伴可以去查询下相关的资料。
MutableState
上面说到的状态变化,通知UI是如何做到的呐,本质上就是一个观察者模式。
csharp
var msg by remember { mutableStateOf("hello") }
在创建compose 需要关注变化的变量,我们会用到mutableStateOf方法,他其实就是返回了一个MutableState 类型的对象。
MutableState 本质上是一个带有观察者功能的对象,可以简化理解为:
kotlin
class MutableState<T>(private var value: T) {
private val listeners = mutableListOf<() -> Unit>()
fun getValue(): T = value
fun setValue(newValue: T) {
if (value != newValue) {
value = newValue
// 通知所有监听者
listeners.forEach { it.invoke() }
}
}
fun addListener(listener: () -> Unit) {
listeners.add(listener)
}
}
读取值时:Compose 会自动注册一个监听器。 修改值时:setValue 会通知所有监听该状态的监听器,比如调用@Composable 相关的重组逻辑。
重组
本质上就是重新调用一下函数,绘制UI。就是在创建Comopose节点树的过程中,创建了MutableState观察者,当MutableState监听到值的变化,就会重新调用和这个状态相关的函数,重新绘制UI。
UI = State
+ Composable
通过Composable注解背后,做的亿点点操作,就能高效的实现根据状态返回UI。写代码时,只需牢牢的记住,UI 是状态的函数,根据状态,写好对应的UI就行。
结语
Compose 的这套机制很复杂,我这里也只是讲了一些皮毛,希望大家先有个基本的认识,一些细节后续再一起探讨。