说实话,这两年每次看到群里聊“XML 还是 Compose”,我都觉得大家低估了一件事:Android 原生的 Compose 已经不是“试试看”的玩具了,而是真正长成了 Android UI 的主干。标题里提到的 Compose A2UI,我一看到就很有感触,这不就是我们一直等的“原生声明式 UI 完全体”吗?很多人还抱着 XML 写界面,理由无非是“项目老、怕重构、文档熟”。但如果你还在从头新建 XML 界面,或者面试时只讲 findViewById,那我劝你认真看下去。
Compose 能做什么?一句话说清楚:用 Kotlin 代码直接描述界面长什么样、状态怎么变,不用再写 layout.xml,不用再找 id,不用再手动处理刷新。它解决的最大痛点是“UI 和状态脱节”,比如进度条的变化、列表的局部刷新、深色模式的适配,Compose 能用一套代码自动搞定。适合谁?想提升开发效率的 Android 工程师、准备迁移老项目的团队、以及所有不想被 XML 绑死的新手。这期内容我就结合自己从 XML 迁到 Compose 的实操经历,把思路、细节、坑都摊开讲。
1. XML 的辉煌时刻与现在的尴尬
1.1 XML 曾经是 Android 开发者的“标准答案”
早期的 Android 开发,写界面基本就是 XML。activity_main.xml 里摆一个 LinearLayout,放几个 Button,然后 setContentView,findViewById 三连。看起来挺清晰,因为 XML 把布局结构直接暴露出来了,IDE 还能实时预览,那时候真的觉得这就是“正统”。
但问题也随着项目膨胀逐渐暴露。随便一个稍微复杂的页面,XML 就能写几百行,嵌套层级深了,渲染性能和可读性一起崩。我印象最深的是有一次维护一个老项目,一个设置页面的 XML 嵌套了七层 RelativeLayout,改一个边距要找半天。而且 XML 本身不是图灵完备的语言,它只能描述静态结构,状态一多,就得在 Activity 里写大量逻辑去控制 View 的可见性、文字内容、背景颜色。页面一复杂,Activity 就变成“神类”,一边找 View,一边设监听,一边改状态,代码全混在一起,维护成本爆炸。
还有布局预览。Android Studio 的 XML 预览确实能看,但实际跑在真机上,不同分辨率、不同字体缩放、不同 Android 版本,经常出现“预览没问题,上机就错位”。XML 的约束布局倒是能解决一部分问题,可约束规则本身又是一套需要熟练度的体系,新同学上手门槛并不低。
1.2 XML 真正让人头疼的是状态同步
我举个最常见的例子:一个下载按钮,按时显示进度条,下载中显示进度百分比,下载完显示“打开”。用 XML 的话,你得在 layout 里放好 Button、ProgressBar、TextView,然后在代码里根据状态去切换 visibility、更新进度值、改文案。如果界面里有好几个这样的状态,你得写一堆 if/else,还要小心别漏了某个状态没还原。
这种“状态驱动界面”的需求,XML 天生就不擅长。XML 定义的是静态结构,状态变化要靠外部代码反复“命令”View 去改。而 Compose 的思路反过来——界面是状态的函数,状态变了,界面自动重组出新样子。你只需要定义好状态和 UI 的关系,剩下的交给框架。这个思维转换,才是 Compose 对 XML 的根本性替代。
2. Compose A2UI 到底是什么
2.1 别被名字唬住,它本质是 Android 原生 UI 的下一代形态
“A2UI”这个叫法,我在好几个技术群里看到过,其实就是 Android 原生 Compose UI 的通俗缩写。大家愿意叫它 A2UI,是因为 Compose 在 Android 上已经足够“原生”——它不是跑在 WebView 里的 H5 壳,也不是像 React Native 那样还要经过 JavaScript 桥接层,而是直接用 Kotlin 编译到 Android 框架里,底层还是和 View 系统一样绘制到 Canvas 上,但上层完全换成了声明式 API。
很多人不知道的是,Compose 其实是 Google 在 2019 年就开始推的现代 UI 工具包,到了 2021 年正式发布 1.0,到现在已经迭代非常成熟。它不再使用 XML 描述布局,而是让你用可组合函数(@Composable)来构建 UI。比如一个带进度条的按钮,代码大概长这样:
@Composable fun DownloadButton(state: DownloadState) { when (state) { is DownloadState.Idle -> Button(onClick = { /* 开始下载 */ }) { Text("下载") } is DownloadState.Downloading -> { LinearProgressIndicator(progress = state.progress) Text("${(state.progress * 100).toInt()}%") } is DownloadState.Done -> Button(onClick = { /* 打开文件 */ }) { Text("打开") } } }看到区别了吗?XML 时代你要在代码和 XML 之间来回切,Compose 里界面和逻辑就在同一个函数里,状态一变,界面自己就对了。这就是声明式 UI 的威力。
2.2 Compose 的“原生”体现在哪
第一,它直接支持 Android 系统特性。深色模式、字体缩放、无障碍、屏幕旋转,Compose 都能感知,并且通过系统 API 自动适配。比如深色模式,你只要用系统提供的 Material 颜色,深浅色就自动切换。XML 时代你得自己搞 values-night 资源目录,搞一堆定制主题,麻烦得很。
第二,它和现有 Android 生态兼容。你可以在 XML 布局里嵌入一段 Compose UI,也可以在 Compose 里放一个传统 View。Google 提供了 AndroidView 和 ComposeView 这两个桥接组件,让你不用一次性推翻老架构。很多老项目迁移动不动就“推翻重来”,其实是不懂这个渐进式迁移的思路。
第三,Compose 官方支持多端复用。不仅仅是手机,Android TV、Wear OS、平板、折叠屏,都用同一套 Compose 代码。如果你在做 Android TV 应用,用 Compose 写焦点控制、写卡片列表,比 XML 配合 RecyclerView 那一套轻松太多。折叠屏和自适应布局更是 Compose 的主场,官方还专门出了 adaptive 库,专门处理窗口尺寸变化。
3. XML 与 Compose 实操对比:别光听概念,动手写写看
3.1 同一个页面的两种写法对比
为了让大家更直观地感受差异,我用一个最简单的用户信息卡片来对比。先看 XML 写法,大概是这样的:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:padding="16dp" android:layout_width="match_parent" android:layout_height="wrap_content"> <ImageView android:id="@+id/avatar" android:layout_width="48dp" android:layout_height="48dp" android:src="@drawable/default_avatar" /> <TextView android:id="@+id/name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="16sp" android:textStyle="bold" /> <TextView android:id="@+id/status" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="12sp" /> </LinearLayout>然后你在 Activity 里这样写:
val avatar = findViewById<ImageView>(R.id.avatar) val name = findViewById<TextView>(R.id.name) val status = findViewById<TextView>(R.id.status) name.text = user.name status.text = user.status if (user.isVip) { status.setTextColor(getColor(R.color.gold)) } else { status.setTextColor(getColor(R.color.gray)) }这只是最简单的例子,已经需要两处地方同步维护。如果界面里再多几个字段,再增加点击事件,代码量会很难看。
再看 Compose 写法:
@Composable fun UserCard(user: User) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(16.dp)) { Image( painter = rememberAsyncImagePainter(user.avatarUrl), contentDescription = "用户头像", modifier = Modifier.size(48.dp).clip(CircleShape) ) Spacer(modifier = Modifier.width(12.dp)) Column { Text(text = user.name, fontSize = 16.sp, fontWeight = FontWeight.Bold) Text( text = if (user.isVip) "VIP 用户" else "普通用户", fontSize = 12.sp, color = if (user.isVip) Color(0xFFFFD700) else Color.Gray ) } } }没有任何 findViewById,没有 id,没有 setText。传入一个 user 对象,UI 就渲染成什么样。user 改了,只要触发重组,UI 自动更新。这就是我前面说的“状态驱动界面”。
3.2 自定义组件在 Compose 里的表达方式
XML 里自定义组件一般要写自定义 View 类,重写 onDraw、onMeasure 这些方法,门槛比较高。Compose 里自定义组件其实就是写一个 @Composable 函数,你可以随意组合现有的可组合项,也能用 Canvas 直接画。比如做一个带阴影的圆角卡片,Compose 里用 Modifier 就能解决:
@Composable fun ShadowCard(content: @Composable () -> Unit) { Card( modifier = Modifier.fillMaxWidth().shadow(8.dp, RoundedCornerShape(12.dp)), shape = RoundedCornerShape(12.dp) ) { content() } }在 XML 里,要达到同样的阴影效果,你得用 CardView,或者自己画 shape,渲染效果有时候还挑硬件。Compose 里 Modifier 就是一套链式调用的样式系统,阴影、圆角、边框、点击水波纹全都能直接加,而且能灵活地组合、复用,写起来比 XML 舒服太多。我做了这么多年 Android,以前最怕写自定义 View,现在用 Compose 画复杂动效,思路清晰得多。
3.3 列表和 RecyclerView 的替代
XML 时代列表几乎是 RecyclerView 的天下,写 Adapter、写 ViewHolder、处理复用,代码分散在至少三个文件里。现在 Compose 里一个 LazyColumn 就搞定,比如显示一个用户列表:
@Composable fun UserList(users: List<User>) { LazyColumn { items(users) { user -> UserCard(user = user) } } }简洁到让人无语。而且因为 Compose 的重组机制,列表项只会在数据变化时更新,性能上并没有比 RecyclerView 差多少,甚至因为省掉了 View 复用那一套模板代码,开发效率要明显更高。Android TV 上常见的焦点浏览列表,Compose 里也有现成的 LazyRow、LazyVerticalGrid 这些可组合项,配上面板焦点控制,比原来 TV 端的 RecyclerView 加各种焦点监听好写多了。
4. 老项目迁移到 Compose 的可行路径
4.1 别想着一次性重写,尽量渐进式迁移
很多老项目不敢动 Compose,就是觉得“迁移成本太高”。其实 Google 从一开始就考虑到这个问题了。你完全可以在一个已有的 XML 项目里,添加 Compose 依赖,然后在某个页面里用 ComposeView 把新的 Compose UI 嵌进去。或者反过来,在 Compose 里用 AndroidView 包一个老的自定义 View。我就见过一个团队先拿一个新页面试水,跑通之后再逐步替换老页面,整个过渡非常平滑。
具体怎么做?第一步,在模块的 build.gradle 里开启 Compose 编译选项,添加依赖:
android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.4" } }依赖至少要包含:
implementation platform('androidx.compose:compose-bom:2024.05.00') implementation 'androidx.activity:activity-compose:1.9.0' implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.material3:material3'第二步,把 ComposeView 加到 XML 布局里:
<androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" android:layout_width="match_parent" android:layout_height="wrap_content" />第三步,在代码里 setContent:
findViewById<ComposeView>(R.id.compose_view).setContent { UserCard(user = User("小明", vip = true)) }就这样,你已经在老项目里用上 Compose 了。之后想再深入,就把整页的 XML 逻辑去掉,改成 Compose 函数,替换掉原来的 setContentView。我不建议一开始就搞“架构大重构”,先让团队熟悉 Compose 的开发模式,再逐步扩大边界,成功率最高。
4.2 新旧代码互操作时的几个原则
迁移过程中,我总结出几条实操原则。第一,不要混用状态管理方式。如果你已经用了 ViewModel + LiveData,那 Compose 里依然可以观察 LiveData,但更好的方案是转成 StateFlow,配合 collectAsState() 使用,这样和 Compose 的重组机制结合得更自然。第二,老的 Adapter 和 ViewHolder 可以保留,在 Compose 里用 AndroidView 临时包住,但凡是新写的列表,一律用 LazyColumn,别再写 RecyclerView。第三,主题资源尽量统一到 Compose 的 MaterialTheme 里,颜色、字体、形状都在 Kotlin 里定义,xml 里的资源文件慢慢淘汰。
需要特别注意,如果你的项目里有大量自定义 View,它们的 onDraw 事件和触摸事件在 Compose 里会被包装成独立的 AndroidView, 这种包装会有一定的性能开销。如果自定义 View 本身很复杂,频繁刷新会导致性能下降。好在这类场景不是主流,大家在个人项目或中小型项目里完全可以直接用 Compose 重写,遇到复杂自定义绘制,用 Compose 的 Canvas 重写效率反而更高。
4.3 用 Compose 写 Android TV 和自适应布局的经验
我在做 Android TV 版本时试过 Compose,感受很深。TV 开发最麻烦的是焦点控制。XML 时代要处理 dpad 按键、焦点搜索、边界回退,逻辑很绕。Compose 对焦点控制的抽象做的很到位,直接用 Modifier.focusable() 和 Modifier.onFocusChanged() 就能控制,配合 LazyRow 做横向的卡片列表,表格布局做宫格,代码清晰多了。
折叠屏和平板适配同样是 Compose 的拿手好戏。官方 adaptive 库提供 WindowSizeClass,可以判断当前窗口是 Compact(手机竖屏)、Medium(折叠屏展开)还是 Expanded(平板/大屏),然后动态调整布局。比如同一个页面,在手机上显示单列列表,在平板上显示双列网格,代码里只要判断一下窗口大小类,复用同一套 Composable 就行。这比 XML 里搞多套 layout 目录要灵活太多了。以前做平板适配,要准备 layout、layout-sw600dp 等多套文件,现在一套 Compose 搞定。
5. 从 XML 转向 Compose 的常见问题和避坑技巧
5.1 性能焦虑:重组次数太多怎么办
很多人刚开始用 Compose 最担心的就是“是不是性能不如 View”。我先说结论:绝大多数业务页面的性能,Compose 完全够用,甚至因为避免了过度绘制,表现比 XML 更好。但 Compose 确实有它自己的性能陷阱,最常见的就是“无谓重组”。比如你在 Composable 里写了一个 lambda,每次重组都创建新对象,导致下游也跟着重组。解决办法很简单,用 remember 和 derivedStateOf 来缓存耗时计算,用 lambdas 包住回调时尽量把回调参数抽出来,避免在组合函数内部直接创建非稳定的对象。
我自己就遇到过一个列表滚动卡顿的案例,排查半天发现是 LazyColumn 的 item 里没有加 key。加上 key 之后,列表项的重组范围大幅缩小,立刻流畅了。所以遇到性能问题先别甩锅给 Compose,先检查重组日志,再用 Layout Inspector 看重组范围,绝大多数性能问题都能定位。
5.2 学习曲线:从“找 View”到“写函数”的心态转换
Compose 的学习曲线不是难度高,而是思维模式要转变。XML 时代是“我要改这个 View”,Compose 是“我这个状态应该呈现什么样”。我见过不少老手一开始很抗拒,因为写代码的方式确实完全变了。但好处是,一旦你适应了,你会发现逻辑更连贯,不会再出现“改了 XML 忘了改 Activity”这种低级失误。
新手常犯的错包括:在 @Composable 里做耗时操作,比如网络请求、文件 IO;没有使用 remember 导致状态丢失;过度使用可变的 MutableState 而不是 StateFlow。我建议刚开始学 Compose 时,先别碰复杂状态管理,就用简单的 remember + mutableStateOf 写小东西,比如一个进度条按钮。等熟练了,再上 ViewModel 和 Flow。
5.3 工具链和资料:把这些配置好能少走一半弯路
Android Studio 一定要升级到最新稳定版,Compose 预览、布局检查器这些工具依赖 IDE 的版本。很多人用旧版 AS 写 Compose,预览一直报错,其实是 IDE 太旧。还有一件事,Studio 设置里可以开启“Android Studio 设置中文”的插件,虽然界面汉化不是重点,但能降低新人上手的门槛,团队里可以考虑统一配置。不过我更推荐直接看官方文档和官方示例,中文社区的资料很多,但质量参差不齐,以官方为准不会跑偏。
再补充一个代码提示技巧:Compose 的 Modifier 是有顺序的,绘制顺序和触摸事件处理顺序都跟 Modifier 的调用顺序有关。比如 background 和 clickable 的顺序会影响点击水波纹的显示区域。我建议把 size、padding、layout 相关的 Modifier 放在前面,把视觉相关的 background、border 放在中间,把交互相关的 clickable、focusable 放在最后,这样不容易出问题。
5.4 常见问题速查表
| 症状 | 可能原因 | 解决思路 |
|---|---|---|
| 进度条不更新 | 状态不是 Compose 可观察对象 | 用 mutableStateOf 或 StateFlow,而不是普通变量 |
| 列表滚动卡顿 | 缺少 key 或 item 内部有大计算量 | 给 items 加 key,使用 derivedStateOf 缓存 |
| 点击事件偶尔不生效 | Modifier 顺序导致触摸区域被遮挡 | 把 clickable 放在 Modifier 链的最后 |
| 深色模式不适配 | 用了写死的 Color | 改用 MaterialTheme.colorScheme 的颜色 |
| Android TV 上焦点乱跳 | 没有正确的 focusable 模式 | 使用 Modifier.focusable(FocusRequester) 精确控制 |
| 页面重组后输入框状态丢失 | 没有用 rememberSaveable 保存状态 | 输入框的值用 rememberSaveable 包裹 |
这些坑我基本都踩过一遍。尤其是进度条不更新那个,我刚开始写 Compose 时一直以为 API 写错了,后来才意识到普通变量根本不会触发重组,必须用 State。理解了“状态驱动”这四个字,至少能避开一半的坑。
6. 我现在的工作方式和对新手的一点建议
6.1 我实际项目中怎么安排 Compose 和 XML
现在我自己做新项目,界面层已经 100% 用 Compose。老项目里,我会按模块去迁移,先把一些低频但仍要维护的老页面留着不动,优先把商城首页、个人中心、登录注册这些核心流程换到 Compose。迁移的时候,除了界面代码,我还顺手把自定义 View 的重绘逻辑一起重写,因为 Compose Canvas 做自定义绘制,代码量有时候只有原来的三分之一。
还有一个重点,多模块项目里,Compose 也会影响模块的依赖方式。以前 UI 模块依赖 AndroidX 库,现在要依赖 Compose BOM,模块间共享 UI 组件变得更容易,你可以把公共的 Composable 函数抽成一个 ui-components 模块,所有页面直接引用。
6.2 给新手的入门路径
如果你是个编制外的新人,或者刚从培训班出来,我特别不建议你再花大量时间啃 XML 的细节。XML 的知识不是没用,但你得知道它正在被替代。我的建议是:把 Compose 当成 Android UI 的“当前默认”,直接学 Kotlin + Compose + Material3。学的时候按这个顺序走:可组合函数怎么用 -> 状态和重组怎么理解 -> Modifier 链式调用怎么组合 -> 列表和动画怎么实现 -> 和 ViewModel 怎么配合。每一步都用一个小例子练一遍。
至于那些老项目、旧代码,你只要会读、会改就行,不用太深入。我自己面试新人时,已经不怎么看 XML 经验了,主要看 Compose 状态管理、自定义绘制、性能优化这些能力。可以说,Compose 已经是这个行业的技术底牌之一,早晚都得会。
6.3 我个人最后想说的
踩过这么多次坑,我最深的一个体会是:工具本身永远在变,关键是背后的思维方式。XML 解决的是“静态布局描述”的问题,Compose 解决的是“动态界面与状态同步”的问题。技术选型不是追新,而是看它有没有真正提升你的开发质量和效率。Compose 已经足够成熟,Android 官方也在持续投入,从系统应用到第三方 App,自下而上的用户早就多了。你不需要一步跨完所有项目,但至少可以从一个页面、一个小按钮开始,试试看。写了一个 Compose 的进度条按钮,把这个逻辑理清了,你会发现,原来的那些 XML 模板代码,真的可以慢慢说再见了。