我为什么喜欢 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 更容易构建,动画也更容易实现。

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

相关推荐
千里马学框架1 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work2 天前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
ai2work2 天前
ch21 签名、校验与发版
kotlin
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui