我为什么喜欢 Compose ?

自 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 布局、ViewHolderAdapter,以及 DiffUtil.Callback

你一个 for 循环都不会写!

这些代码大多是模板代码,但即便如此,你也经常会出错:

  1. ViewHolder。忘记状态重置。如果你在某个条件分支中只编写了部分逻辑,那么在复用时就会显示错误的状态------你必须小心谨慎地保证处理了所有情况下的显示状态。
  2. DiffUtil.Callback。只要 item 比较逻辑有一点没写对,列表就可能闪动,或者复用后显示了错误的状态。

在 Compose 中,一个 LazyColumn 就能直接表达列表;配合稳定的 key,也不必再维护 AdapterDiffUtil 那套样板代码:

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 的属性动画、LayoutTransitionanimateLayoutChangesTransitionManager 场景过渡、加上 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 的尺寸自适应,还有 CrossfadeupdateTransitionrememberInfiniteTransition......看起来同样是一堆选择。

但不一样的是,它们的接口名称本身就把职责说清楚了:AnimatedVisibility 管显隐、AnimatedContent 管内容切换、animateContentSize 管尺寸变化------看到名字,基本就知道该用它做什么、能用在哪里。而且它们把动画状态照顾得很好:中途被打断会平滑处理,动画结束后的收尾也不需要你操心。你不需要额外分配精力去"管理动画",只需要描述"这里要有个动画"。

布局朴素

听起来可能反直觉。Compose 当然有一定的学习曲线,但它的布局确实非常朴素。

现在大部分页面,本质上就是由 RowColumnBox 一层层组合出来的。

不再需要维护一张由 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)
}

我只需要看这段代码,大致就能知道页面会长什么样。

这件事在 ConstraintLayoutLinearLayout 中当然也能做到,只是通常更繁琐,代码也更长。

不再只属于 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 对应 VStackText 还是 TextButton 也还是 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 更容易构建,动画也更容易实现。

希望下一个五年,它还能继续把这些麻烦事变得更简单。

相关推荐
JMchen12317 分钟前
【Android 性能优化实战 60 讲】03 Perfetto 现代分析利器:SQL 查询 Trace 数据,微秒级定位函数 CPU 耗时
android·sql·性能优化·实战·源码分析·perfetto·启动优化
开开心心就好36 分钟前
手机精确倒计时工具悬浮窗口秒数实时显示
android·前端·javascript·jupyter·智能手机·pdf·html
墨狂之逸才38 分钟前
一次 Android 工位机黑屏卡死排查:根因不是 App,而是 scrcpy 与 Rockchip 编码器的 DMA-BUF 泄漏
android·android studio
Privasa-隐私实验室1 小时前
跨端技术选型|Flutter搭建隐私加密App,安卓与iOS双端差异适配实战
android·笔记·安全·flutter·ios·隐私安全·aes-256
limuyang28 小时前
PDF Viewer KMP(基于 chrome 的 PDFium 内核)
android·kotlin
心平气和量大福大12 小时前
android-控件-搜索框SearchView
android·gitee
2601_9621771312 小时前
【MySQL篇】聚合查询,联合查询
android·java·mysql
壮哥_icon12 小时前
【Android】批量 VectorDrawable 动态分别修改 fillColor 和 strokeColor 的踩坑与终极解决方案
android