
自 2021 年 7 月发布第一个稳定版本 Compose 1.0 到现在,Compose 已经走过了五个年头,最新版是 8 月份的 1.12 版本。
回顾 Compose,它改变的不只是 Android UI 的写法。很多过去需要反复处理的细节,终于不再需要开发者亲自维护了。
声明式 UI 并不是 Compose 的首创,但是 Compose 却确确实实地颠覆了 Android 的开发范式,同时也重塑了 Android 开发者的心智模型。
过去的 Android 开发,我们一直在思考:怎么做?如今已经变成了:是什么!
现在,我想停下来总结一下:我自己为什么会喜欢 Compose。先从眼下最能打动我的几点说起。
像写 for 循环一样写列表
如果我一提起给你一个列表让你显示,我相信很多开发者的第一直觉就是,它一定有个 for 循环!
但实际上,在过去的 View 体系中,完成一个列表需要写一个 RecyclerView,而写好一个 RecyclerView,往往至少要准备四份东西:XML item 布局、ViewHolder、Adapter,以及 DiffUtil.Callback。
你一个 for 循环都不会写!
这些代码大多是模板代码,但即便如此,你也经常会出错:
ViewHolder。忘记状态重置。如果你在某个条件分支中只编写了部分逻辑,那么在复用时就会显示错误的状态------你必须小心谨慎地保证处理了所有情况下的显示状态。DiffUtil.Callback。只要 item 比较逻辑有一点没写对,列表就可能闪动,或者复用后显示了错误的状态。
在 Compose 中,一个 LazyColumn 就能直接表达列表;配合稳定的 key,也不必再维护 Adapter 和 DiffUtil 那套样板代码:
kotlin
LazyColumn {
items(listOfItems, key = { it.id }) { item ->
ItemRow(item, onTap = { doSomething(item.id) })
}
}
我必须说的是,写列表,本应该如此!
key 负责标识每一项的身份。Compose 会在重组过程中用它追踪 item,因此列表重新排序时,状态也会继续留在正确的那一行上;如果还希望看到位移动画,则需要为 item 明确添加相应的动画 Modifier。
动画变简单了
以前让一个 View 淡入淡出,通常需要两套逻辑和一个回调。
你不能一边直接修改可见性,一边播放动画。因为一旦设置成 GONE,View 会立刻消失,动画也会在中间被截断。所以隐藏时要先做 alpha 动画,再在结束回调里设为 GONE;显示时又要记得先把 alpha 重置为 0,再设为可见,最后播放淡入动画。
kotlin
// hiding
view.animate().alpha(0f).setDuration(200)
.withEndAction(() -> view.setVisibility(View.GONE));
// showing
view.setAlpha(0f);
view.setVisibility(View.VISIBLE);
view.animate().alpha(1f).setDuration(200);
另外,要"做一段动画",你还得先在一堆动画体系里做选择题------AnimationUtils 加载的补间动画(XML 里的 alpha / translate)、ObjectAnimator / ValueAnimator 的属性动画、LayoutTransition 与 animateLayoutChanges、TransitionManager 场景过渡、加上 RecyclerView 单独一套 ItemAnimator。
它们的 API 风格各不相同,能力边界又互相重叠,很多时候你并不清楚眼前的场景该选哪一个。
现在,我们来看下 Compose。
AnimatedVisibility 只需要一个布尔值:
kotlin
AnimatedVisibility(
visible = isExpanded,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
DetailPanel()
}
退出动画还没有完成时,Compose 会继续把内容保留在组合树中;等动画结束后,再将它移除。
过去我们写在 withEndAction 里的那部分收尾逻辑,现在由 AnimatedVisibility 帮我们处理了。

当然,Compose 也有不少动画 API:animateFloatAsState 这类状态动画、Animatable 的手动驱动、AnimatedContent 的内容切换、animateContentSize 的尺寸自适应,还有 Crossfade、updateTransition、rememberInfiniteTransition......看起来同样是一堆选择。
但不一样的是,它们的接口名称本身就把职责说清楚了:AnimatedVisibility 管显隐、AnimatedContent 管内容切换、animateContentSize 管尺寸变化------看到名字,基本就知道该用它做什么、能用在哪里。而且它们把动画状态照顾得很好:中途被打断会平滑处理,动画结束后的收尾也不需要你操心。你不需要额外分配精力去"管理动画",只需要描述"这里要有个动画"。
布局朴素
听起来可能反直觉。Compose 当然有一定的学习曲线,但它的布局确实非常朴素。
现在大部分页面,本质上就是由 Row、Column 和 Box 一层层组合出来的。
不再需要维护一张由 ID 和约束关系组成的 ConstraintLayout 图,也不用猜测到底是哪一条约束在和另一条约束冲突。
而且,在 Compose 中嵌套布局的成本,也不像过去 View 体系里那么让人感到有负担。一次布局过程中,每个 child 通常只会被测量一次;如果需要额外的尺寸信息,则使用 intrinsic measurement 机制。因此,也不会出现过去嵌套 LinearLayout 并使用 weight 时常见的二次测量问题。
kotlin
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(painterResource(profileIcon), null)
Column(Modifier.weight(1f).padding(start = 12.dp)) {
Text(firstName, style = MaterialTheme.typography.titleMedium)
Text(lastName, style = MaterialTheme.typography.bodySmall)
}
Switch(checked = profile.isSignedIn, onCheckedChange = onToggle)
}
我只需要看这段代码,大致就能知道页面会长什么样。
这件事在 ConstraintLayout 或 LinearLayout 中当然也能做到,只是通常更繁琐,代码也更长。
不再只属于 Android
Compose Multiplatform 把同一套 Composable 带到了桌面端、iOS 和 Web。
共享的部分,本来就是我们最想复用的部分:布局、状态处理和设计系统。平台特有的实现,则继续放在 expect / actual 后面。
kotlin
@Composable
fun ProfileCard(profile: UserProfile) {
Card { /* identical on Android, desktop, iOS */ }
}
Compose Multiplatform 这些年进步很快,非 Android 平台上的性能也几乎每个版本都在持续改善。
通用的思维模型
Compose 并不是一种只能在 Android 上使用的 UI 写法。
它把界面描述为状态的函数,而不是不断去修改一棵 View 层级树。SwiftUI、Flutter 和 React 其实都采用了相近的思路。
Compose 在 2019 年 5 月的 Google I/O 上首次预览;约一个月后,SwiftUI 在 WWDC 上发布。它们之前还有 2013 年的 React(这里我不得不说那四个字,React 遥遥领先!),以及 2018 年的 Flutter。
这几套框架之间的对应关系已经足够接近,很多时候不需要额外的"翻译层"就能读懂:
kotlin
@Composable
fun Greeting(name: String) {
Column {
Text("Hello, $name")
Button(onClick = { greet() }) { Text("Tap me") }
}
}
struct Greeting: View {
let name: String
var body: some View {
VStack {
Text("Hello, \(name)")
Button("Tap me") { greet() }
}
}
}
Column 对应 VStack,Text 还是 Text,Button 也还是 Button。
当然,底层机制并不相同。
SwiftUI 的 body 返回 some View,编译器会静态地为这棵树推导类型;而 Composable 通常返回 Unit,其 UI 描述会由编译器插件转换为对 Compose runtime 的调用。
但现在我看到一段 SwiftUI 页面时,基本不需要先查文档就能理解它。这在 UIKit 时代几乎做不到。
UI 开发的能力,开始变得可以迁移了。
而这套思维模型的核心,就是状态驱动 UI。
写 XML 的时代,我们先搭好 UI 框架,再到另一个文件里写 UI 逻辑,两边靠 ID 和引用拼接,谁也离不开谁,却又谁都不完整。
现在,UI 逻辑和 UI 框架就在同一个地方:状态变化驱动 UI 更新,UI 通过事件改变状态。这可以形成清晰的单向数据流和分层------状态通常是界面的真相来源,界面只是它的呈现。一切因此更可靠,也更容易推断。
一点想法
Compose 并不完美。尤其是最近更新的 Styles API,也会让我在写代码时犹豫:究竟该用 Modifier,还是该用 Styles API 来定义 UI 的外观?
重组依旧会让人困惑。我也确实花过不少时间打开 Layout Inspector,排查为什么某个地方的重组次数比预期更多。
但我喜欢 Compose,不只是因为它能少写几行代码。列表项的身份、动画结束后的移除、状态变化后的界面更新,这些原本需要开发者反复记住、手动维护的细节,现在都交给了框架。我们终于能把注意力放回本来该关心的问题:用户此刻应该看到什么,又该如何与它交互。
所以,和 XML 相比,写 Compose 的过程是有趣的。复杂 UI 更容易构建,动画也更容易实现。
希望下一个五年,它还能继续把这些麻烦事变得更简单。