
一个被写在坏点子白板榜首的提案最终重写了整个 Android UI 工具包。这是 Jetpack Compose 的来处也是一个关于既然都要改不如全部重写的工程故事。今年是 Jetpack Compose 发布 1.0 的第五个年头。今天写 Android 界面的人大多已习惯用 Composable 函数描述界面、用 Modifier 串联行为、用状态驱动刷新。但这套范式并非一开始就存在。回溯它的历史会发现Compose 没有发布会式的宏大蓝图反而更像一连串工程师式的自嘲与意外一个最初只想逃离平台、安心修 bug的小项目是怎样一步步失控最后把整个 UI 工具包重写了一遍的。READING MAP · 全文导读这段历史可以拆成五章一条主线贯穿始终——从只想解耦修 bug到顺手重写了一切第一章 · 缘起——为什么解耦工具包当年是白板上的头号坏点子。第二章 · 汇合——R4A、波士顿峰会与 Flutter两条平行线如何走到一起。第三章 · 设计哲学——函数、Modifier 与洋葱模型一路做减法。第四章 · 落地 1.0——工具之争、互操作以及 Play 商店的火的考验。第五章 · 复盘——主题与样式的戒律、性能遗憾和那些惊艳到自己的瞬间。第一章 · 缘起一个修不动的 Button1.1 最初的痛点修一个 bug要等用户升级系统故事要从一个很朴素的烦恼讲起。在 toolkit 团队看来真正的起点是一个近乎窒息的事实发布一个 API 修复太难了。在 Button 类里改掉一个 bug用户并不能立刻拿到——他们得等到自己的设备收到下一次系统升级。而 Android 版本的分发节奏从来不由应用团队掌控。那几年团队越来越依赖 Support LibraryRecyclerView 这类能力已经可以脱离系统版本、单独迭代。一个自然的念头随之浮现控件能不能也这么做把整套 API 从系统平台里解耦出来跟着 Support Library 一起发布颇具讽刺意味的是Button 反而是最不具代表性的例子——它可能是所有控件里问题最少、代码也最短的一个本质上只是文本控件的一个子类背后却拖着一大堆它根本用不到的能力。真正让人绝望的是文本大量应用已经依赖了那些错误的行为以至于任何修正都可能引发兼容性灾难。相比 API 难以发布这种被自己历史彻底锁死的困境要严重得多。bug 修不动API 也改不动许多能力只能以脆弱的方式勉强维持ListView 就是典型。正是在这种走投无路的背景下后来被内部称作伟大的控件迁移The Great Widget Migration的计划悄悄萌芽。1.2 坏点子白板第一名这个想法的缩写是 GWM。若按内部的玩笑逻辑一路展开新项目的真名理应是 GWMI——当然没人真的打算这么叫。名字从来都是难事。GWM 之后项目还有过几个代号Crane 是其中之一起重机负责迁移吊装而鹤crane本身也是一种迁徙的鸟一语双关团队一度为它做了 Logo 和贴纸等到另一条团队合流项目又改名 Kittyhawk——莱特兄弟第一次成功起飞的地方也暗合当时正在搭建的 Jetpack。最终市场团队坚持使用了 Compose。这个名字在工程团队内部争议不小因为它本是底层运行时的名字——那套运行时其实不限于 UI任何大致呈树形结构的东西都可以用它把整个项目都叫 Compose等于偷走了运行时的名字。但换个角度既然团队当时还没准备好支持 UI 之外的场景不给运行时单独命名反而更诚实。很多年前有人在 Mountain View framework 团队对面竖起一块白板上面写着bad ideas坏点子。排在第一位的正是unbundle the toolkit解耦工具包。那么问题来了当年公认的坏点子后来为什么变成了好主意答案藏在硬件与工具的时代变迁里。在 Android 早期每一比特都要计较团队连一个 enum 都嫌浪费。如果连 enum 都用不起让每个 App 都打包一整套 View 工具包显然不可想象。彼时 R8 尚未出现代码压缩工具的效率远不如今天当年的 App 体积小得多设备 RAM 也少得可怜。在这样的约束下解耦几乎注定是个坏主意。哪怕到了今天成本依然是悬在头顶的剑。Compose 没有 View 那种内置在系统里的优势团队仍要花难以想象的精力去优化它的启动速度每个 App 也都得自己承担这份开销。从时间线看团队在 2017 年前后开始认真讨论解耦GWM 的说法也在那时出现而真正动手还要再往前倒三四年——他们实际上是在替换一个已有十年历史的工具包。第二章 · 汇合两条线与一句危险的话2.1 另一条独立的线R4A 与波士顿峰会Compose 其实是两条原本互不相干的线恰好在对的时间汇合的产物。差不多同时工程师 Jim 出于对 React 的热情独自做起了一个叫R4AReact for Android的研究项目。那是一个 Kotlin 编译器插件能把视图 XML 里那套标签语法直接搬进 Kotlin 代码再生成 Button、RecyclerView 这样的视图层级——类似当年 JavaScript 的 E4X全靠编译器插件施展魔法。令人惊讶的是他真的做出了一堆能跑的原型。相当长一段时间里两条线各自探索一边在姊妹团队里打磨 R4A一边在另一头琢磨控件迁移始终没有真正合并。外面看那正是声明式、响应式框架兴起的年代——React 在 Web 端如日中天React Native 刚刚发布选择多得反而没人知道该往哪走。面对这种观望管理层的态度很干脆先挑一条路走起来给自己一个地基哪怕之后推翻重来也行。转机发生在 2017 年波士顿的一场 toolkit 规划峰会上。它本来只是一次普通的年度规划结果成了 Compose 真正的起点。会上形成的共识是响应式模型已在 Web 领域被 React 验证而 Google 自己的 Flutter 也已探索了三年。Flutter 的具体技术选型并不适配——它基于另一套语言体系——但用 Kotlin 做一件类似的事成了破局的约束。团队甚至认真算过一笔账如果能把 Flutter 三年打磨 API 与使用体验的经验全部学过来Compose 是不是等于凭空省下三年、直接空降到三年之后也是在波士顿团队第一次把开发者关系DevRel拉了进来并下决心重投用户研究UXR。此后几年DevRel 每周基于最新代码开黑客松反过来深刻塑造了 Compose 的早期形态今天 Compose 之所以是 Compose离不开当年那笔大到离谱的研究投入。这种做法的妙处在于面对一张白纸人往往会卡住很久你真正需要的是一个可以围绕它构建的约束。哪怕起点不完美先有个东西再不断往上迭代。Flutter恰好充当了那个把团队从空白焦虑里拽出来的起点。2.2 工程界最危险的一句话项目最初的目标其实非常克制第一步只是想逃出平台。在另一个平行宇宙里他们本可以保留一模一样的 API——还是那个 Button还是那些傻乎乎的设计只是从此能在 Support Library 里持续修 bug。当时最核心的诉求甚至简单得可笑能不能让 Button 别再出问题但事情很快滑向了另一个方向。一个在工程史上反复出现、也格外致命的念头冒了出来既然都费这么大劲了是不是应该走远一点干脆重写成团队真正想要的那套 API这正是那句被称作工程界有史以来最危险的话——While were at it既然都做到这一步了……。前半句是务实后半句往往是失控的入口既然都到这一步了为什么不把所有东西都重做一遍呢于是一个只想解耦修 bug的项目就这样顺势长成了重新发明一切的 Compose。第三章 · 设计哲学一路做减法3.1 从 XML 到类再到函数通往 1.0 的路某种意义上是一条不断删东西的路。如果当初急于发布Compose 会比今天复杂得多。最早的代码库是那个 Flutter 的 Kotlin 移植版随后与 R4A 合并嵌入式的 XML团队内部叫 KTS标签一度出现在 Compose 的层级里。但 XML 语法有个绕不开的问题它依赖编译器对语言动手脚。团队就此与 JetBrains 谈了很久——对方无法阻止 Google 做编译器插件却担心这种特殊语法会侵蚀语言本身、伤害整个生态态度更接近你要非这么干就只能自求多福。反复权衡后团队放弃了这套特殊语法退回到类——像 Flutter 那样继承一个基类、重写方法。紧接着编译器团队又发现连类都可以不要用一个普通的顶层函数加一个注解就能做到。新的问题随之变成既然一个自由函数就够了还需要类吗这在当时极其反直觉。不少人强烈反对认为这会污染所有人的命名空间、凭空冒出一大堆顶层函数。那批人本质上还是正在学 Kotlin 的 Java 程序员对 lambda 也很陌生去掉类几乎是在和所有本能对着干。但一次次减法带来的轻盈是真实的不是自定义语法甚至不是自定义类就是函数——而开发者本来就懂函数。最终一个注解、一个函数就足以描述界面。这种相信直觉、逆着数据走的时刻不止一次。顾问委员会曾被问到是否想要一套用 Kotlin 写的全新响应式 UI 工具包答案基本是不想要View 虽有毛病但我们懂它别浪费时间前一年问要不要引入一门新语言得到的也是拒绝——而那门语言正是 Kotlin第二年却成了你们做过最棒的事。数据当然重要但难就难在你得知道什么时候该相信自己的判断。帮团队建立这种直觉的是持续不断的自己先用dogfooding他们在时机上也确有运气——那几年旧工具包足够稳定产品生态又恰好没有折叠屏、XR 这类新需求催逼团队才有余力悄悄完成这场研究。3.2 Modifier从游戏引擎偷来的灵感开发中途冒出来的另一个关键设计是 Modifier修饰符。它的思想源头很早。在 GWM 与 R4A 之前就有人设想把游戏与渲染引擎里的实体—组件系统entity-component system搬到 Android一个控件本身什么都不是只是个引用真正的能力由一个个挂上去的组件提供——这个负责渲染那个负责布局。这个想法一度停留在构想阶段但在与 Adam Powell 的反复讨论后由 Adam 据此做出了 Modifier。这是一次巨大的解锁。原本可能要单独做成控件的东西比如内边距 padding忽然变成了可以挂在组件上的插件。团队曾为边界到底在哪争论不休Surface 该是 Modifier 还是组件Button 呢如果把整套组件都写成 Modifier会不会退化成 Web 那种万物皆 div、再一层层往上堆的样子最终的落点折中而优雅Composable 是你需要认识的实体Modifier 更像附着其上的装饰。今天去看 Button 的源码它毫无魔法不过是 Surface 加一串 Modifier几乎就是个别名总共十来行。好处也随之而来开发者想改 Button 时不必从零开始只需照着源码把想要的部分重新拼起来——这远比让你自己用 Java 重写一个 TextView 友好得多。3.3 洋葱、悬崖与楼梯与 Modifier 相伴的是团队早期反复强调的一个理念做一颗洋葱。理想状态下Button 应该只有十行代码如果它不能满足你就往下剥一层。每一层都薄而透明你可以直接抄走上一层那十行改成自己想要的样子。旧 View 体系的问题则可以用另一组比喻概括它像是牵着你的手把你带到悬崖顶上然后一脚踹下去而真正该做的是修一条楼梯。早年的 ListActivity 就是那座悬崖——两行代码就能得到一个带列表的界面可一旦你想稍微定制就只能自生自灭。这种便利与失控之间的断崖正是 Compose 想消除的。团队并没有百分之百做到但至少开发者能一层层往下钻而不是被推下悬崖。第四章 · 落地 1.0工具、互操作与火的考验4.1 工具之争代码还是所见即所得很长一段时间里团队心里都压着一头大象如果放弃 XML 这种数据、全面转向代码可视化工具怎么办做工具的人通常都希望界面是一份数据。从 Swing 时代起手写代码、for 循环就意味着没法使用 UI 构建器后来业界为了可被工具处理发展出 GroupLayout 这类更规整的方案NetBeans 里甚至有过能拖拽、自动吸附、做基线对齐的可视化界面而那本质上是数据不是代码。Android 最初的 setContentView XML id 之所以好用也正是因为界面是一份可被工具读取的数据。因此退回纯代码让工具团队非常不安。他们设想过一种聪明的所见即所得把代码部分暂时丢掉先渲染遇到 for 循环不确定次数就先画三个选中时三个一起高亮因为它们对应同一处布局。这一切本想借助编译器实现但听上去太难——于是团队先做了 Preview预览。Preview 脱胎自当年为 XML 打造的可视化布局编辑器背后是一套极复杂的 layoutlib 技术把 UI toolkit 与系统的各种绑定剪断让它能在 IDE 里脱离设备运行。更早还有设计期元数据的探索在 XML 里使用tools命名空间告诉 AAPT 打包时直接丢弃这些属性、运行时零成本只在设计阶段用于补全渲染比如提示编辑器渲染这个列表项时先把它塞进一个列表里。这套思路最终演变成 Compose 的Preview注解用元数据描述该怎么渲染并让预览与代码实时共存。它还有一个隐性好处——因为界面始终是代码重构时数据永远不会和真实界面脱节而 Compose状态不属于控件的天性也引导人们写出更干净、更易预览的结构。对迭代速度的执着同样贯穿始终。UI 开发大量是挪动一两个像素、把颜色调一点点反馈回路必须足够短。这条路上踩过明显的坑2017 年的 Instant Run 想让任意 App、跨多个 API 版本都能热修补奇迹般地能跑却可能在 App 里留下开发者不知情的缓存与脏数据最终成了噩梦。后来的 Apply Changes 与 Live Edit 学聪明了——只支持 Compose因为团队完全清楚怎样戳一下App让它在状态变化后自己重绘。最早的 Live Edit 只处理 padding、颜色这类常量改动改完立刻生效体验已相当好。至于设计师用可视化工具产出界面、再直接变成应用这个理想多年来各家公司反复尝试包括把 Figma 与代码生成结合的方案但始终无人真正解决而设计师也从来不愿意使用 IDE 这类开发者工具。4.2 1.0 该有什么克制、互操作与火的考验到这时View 系统已积累近十五年想在 1.0 里复刻全部功能毫无可能。什么才算 1.0于是成了一场艰难取舍团队逐个领域定义最小可用集MVP。文本就是典型。彻底解耦文本技术栈几乎等于另一个十年工程而它深入渲染管线、对包体影响巨大因此直到今天 Compose 仍在沿用框架自带的文本栈。Material 这边同样在反复追问最小能实现到什么程度每周新做一个东西DevRel 拿去 hack 一天回来说这不行就打回重做。定义 1.0 的方式很务实先想清楚希望开发者能做出哪类 App再倒推出需要哪些 API。于是有了 JetNews、Jetcaster 这批Jets 样板应用。如果 DevRel 能用当时的 Compose 搭出四五个有代表性的应用社交、资讯、播客等就说明地基够了。1.0 的目标从不是什么都能做而是让你足以撑起这一类真实应用。另一个被反复验证的关键决策是互操作interop。团队在互操作层投入巨大让你可以只重写一个 Button、一张卡片就能与原有 Java/View 代码无缝共存。这借鉴了 Kotlin 当年单文件引入的成功经验极大降低了上手门槛。它同时也是双向的逃生舱1.0 尚未覆盖的能力可以随时退回 View——重写 WebView 这样的组件实在是个大工程它至今仍是那个退路。互操作被认为是 adoption 能真正跑起来的关键决定。而最能说明 1.0 含金量的是 Play 商店。这款同样有十余年历史、全球装机量最大的应用当时正酝酿一次大规模重构团队抓住机会让它在 1.0 之前就押注 Compose成了第一个 Compose 应用。回头看双方都很有勇气——这是不折不扣的火的考验trial by fire。过程极其痛苦而 Play 商店至今仍是最严厉的测试者一旦出现性能回归第一个尖叫的往往也是他们。围绕要不要叫 1.0工程团队内部吵得很凶大家普遍觉得我们还没做完。但 1.0 的意义本就不是完整替代——人们不会愿意把生产环境的 App 押在 0.x 版本上。正如 Android 1.0 在发布前最后几个月也在疯狂砍功能你可以永远打磨下去却总得在某个时刻把它发出来它代表愿景让人看清方向、愿意跟着上船。第五章 · 复盘戒律、遗憾与高光5.1 不重蹈主题与样式的覆辙在体验层面团队有一条明确的戒律不要再犯主题theming与样式styling的老错误。内部有一个朴素的判据如果每年 I/O 都得让 Alan 和 Chris Banes 上台专门把 styles 从头解释一遍那只能说明设计本身出了问题。于是团队做了个大胆决定——先不做主题系统以后随时可以加别一开始就背上这个包袱。效果立竿见影XML 里那种二十层的间接引用消失了每个样式从哪来都一目了然迭代也更快。耐人寻味的是多年之后 styles 又被重新引入——因为组件层仍留有一道定制悬崖新的 styles 正是为了填平这道断崖。这也是团队第一次真正改动 API 的使用方式既紧张又兴奋。5.2 遗憾绘图 API以及还不够的性能复盘遗憾主要集中在两件事上。其一是绘图 API。控件才是当时的重心Canvas 与 Paint 的能力没有被完整搬进 Compose以至于今天遇到某些场景仍要退回那些 Android 专属对象等 Compose Multiplatform 流行起来这种被迫绑定 Android 的写法就格外烦人。其二是性能。团队确实担心性能却担心得不够、低估了实际所需。ART 带来了好得多的垃圾回收器硬件也在快速进步这让团队在某些地方不够克制甚至有一些 API 注定每次布局都要付出一次固定的分配代价。不过需要说明的是早期团队是刻意把好用排在性能之前的——人体工学ergonomics被放在权衡的最前面甚至一度高估了运行时以后会自己解决。如果一开始就死磕性能会做出完全不同的取舍。而 1.0 之后团队恰恰是在不动 API 的前提下把大量性能问题在底层修掉并弃用了一批带 lambda 的昂贵 Modifier——这说明好用与谨慎本可以兼得。R8 在其中帮了大忙。即便如此这个权衡仍被认为是值得的。如果当年只做出一个性能超强的 Views 2.0那它就仅仅是 Views 2.0最后大家反过来遗憾的会是那套 API。5.3 高光时刻Slot API、函数即控件以及惊艳到自己回望这段历程有几个片段格外闪亮。首先是做 Material 时发明的Slot API槽位 API。当团队意识到只需让组件接收一个 lambda、由调用方把内容填进槽里许多难题瞬间瓦解——包括那个贯穿全程的 Button 问题。同样动人的是后来把语义、布局、Modifier 等大量核心部件整个重写一遍的自由。这恰恰反证了当初解耦的正确性——正因离开了平台团队才拥有了在系统里再也不可能拥有的腾挪空间。而函数即控件本身堪称最纯净、优雅的成果。它让状态必须被提升state hoisting变得不言而喻——状态直接出现在函数参数里你想绕都绕不开。那些感觉是对的、做起来却浑身不舒服的决定事后看往往都是好决定。更难得的是团队气质中务实的一面一个全新框架本可沉迷于重新发明一切他们却死磕与 View 的双向兼容认真思考怎样陪开发者平滑迁移而不只是自己写得开心。还有一类高光来自工具团队自己反过来用 Compose。大约一年前Android Studio 的新界面决定全部用 Compose 构建尤其是其中的 AI agent 界面。这一次做工具的人难得地亲身吃到了自己做的狗粮界面切换不再是生硬跳变而是动画式地优雅隐去UX 团队终于能把那些微妙的细节放进去。从做完东西盼着别人喜欢变成自己一边用一边感叹这技术真棒。跨平台这条暗线也早早埋下。尽管 Android 始终是第一优先代码库里从一开始就保留了大量面向未来的设施JetBrains 早在 1.0 之前就已启动 Compose Multiplatform后来它真正成了气候正得益于这份先见。写在最后工程团队曾激烈反对发布 1.0——还没做完还有那么多东西要修。但一件事你可以永远做下去却永远等不到完美0.x 版本再强也没人敢把生产环境的 App 押在上面。1.0 代表的从来不是全都有了而是方向足够清楚你可以上船了。事实是从 1.0 到真正准备好又走了整整五年——直到今年的 Google I/O团队才正式说出那句话现在把你的全部 UI 都写在 Compose 里吧。这段历史最打动人的地方在于它诚实地展示了一个伟大项目的真实起点不是某个高瞻远瞩的宏大蓝图而是一个朴素的痛点加上一点点工程师特有的、危险的顺手。While were at it 既可能是失控的开端也可能是伟大重构的第一行注脚。区别只在于——你是否清楚自己愿意为真正想要的 API付出多少又是否愿意在改写一切的冲动里保留一份对迁移者的务实与温柔。而 Compose 用这五年给出的答案是值得。