软件工程师
今年,Android 正式全面转向 Compose 优先,将其作为构建所有界面的首选,而旧版 View 体系则进入维护模式。对于 Google Play 而言,这段旅程始于 2020 年。在近二十年的工程历程中,我们服务了数十亿用户,通过行之有效的最佳实践、与 Compose 核心团队的紧密协作以及持续的工具演进,成功迁移到了 Jetpack Compose。
了解如何通过 Compose 对 Google Play 进行现代化改造,从而在速度和长期稳定性方面实现可衡量的提升。本讲座将介绍如何将智能体 Figma 设计工作流与 Compose 实现相结合,通过自动化质量控制来避免不必要的重组,并利用动态颜色、丰富的排版和分层形状打造富有表现力的界面。您将掌握经过验证的策略,从而能够更快地对 bug 进行分类,并自信地扩大 Compose 迁移规模。
ComposeView。MaterialTheme 叠加层和 Material Color Utilities 库。这样可以避免 Activity 销毁,不过在构建时生成配色方案有助于限制运行时延迟。BoxWithConstraints。转写文稿
欢迎大家!
我叫 Jon。
我是 Google Play 商店团队的成员。
这是我的同事 Kirill。
今天,我们将与大家分享如何将大量代码从 View 和 Fragment 系统迁移到 Compose。
让我们开始吧。
那么,采用情况如何?规模有多大?
自 2007 年左右以来,我们的应用代码库一直在自然增长。
我们有数百万行生产代码、200 个屏幕和 500 个独特的界面组件,这些组件在许多不同的屏幕中使用。
2020 年,我们发现开发速度、可靠性和性能开始无法达到我们想要的目标。
因此,我们决定采用现代界面技术栈。
我们评估了许多不同的库。
当时,AndroidX 团队的一些朋友也向我们介绍了 Compose,我们决定采用 Compose,并成为 Compose 的首个主要合作伙伴。
结果怎么样?
六年后的今天,Play 商店应用中 97% 的用户展示都发生在仅使用 Compose 的界面上。
抱歉,自 2020 年起,所有手机和大屏设备开发都已改用 Compose。
自 2025 年起,我们甚至将 XR、TV、Auto、Wear 等扩展设备类型也迁移到了 Compose。
Compose 是我们取得成功的重要推手,这也是我们不断扩大其应用范围的原因。
他们拥有非常强大、全面的测试套件,这帮助我们每周两次采用新版 Compose。
通过引入 Alpha 版 build 和新 API,我们可以利用新的问题修复和性能改进,并真正探索其中一些新 API,从而提供新的用户体验。
特别是在性能方面,Compose 确实为我们应用的所有主要屏幕提供了持续的卡顿性能。
这是搜索结果页、首页、详情页。
这些指标(性能、卡顿和加载延迟)对我们的业务和用户体验都非常重要。
因此,在过去 14 个月中,我们看到 30% 的提升。
那么我们是如何走到今天的呢?
这正是我们今天要讨论的主题。
接下来,我们来谈谈迁移计划。
因此,Google Play 采取了多步骤方法。
我们不想孤注一掷,把所有鸡蛋都放在一个篮子里。
我们最终采取了一些措施来了解风险及其对业务的影响,并在全面投入之前真正建立信心。
因此,我将这些阶段命名为:爬行、行走、慢跑和跑步。
因此,对我们来说,摸索阶段实际上就是熟悉 Compose 的过程。
我们从一个流量较低的网页开始。
这可能是设置页面、第三方许可等。
您的主要目标是熟悉 Compose 库。
因此,这可能需要您熟悉行、列、文本、图片等各种元素。
然后,您需要开始引入所有应用基础架构,并了解其在 Compose 中的运作方式。
因此,这可能涉及展示日志记录、点击日志记录、主题设置,同时我们还在改进数据加载。
所以,这些因素都有影响。
我们发现,有一点特别有用,那就是确保所有屏幕看起来都一致。我们构建了一些单元测试和功能测试,以检查 Compose 中的主题对象是否直接映射到 XML 样式、排版、颜色等所有这些内容。
因此,当我们对这个功能有了一定的熟悉度并感到舒适后,我们决定开始尝试步行。
对我们来说,这意味着在流量非常高的屏幕上选择一个较小的组件,并评估这种迁移的效果。
我们建议您选择 RecyclerView 中的某个项,这样您就可以了解 View 系统和 Compose 系统之间的任何兼容性问题,并在此过程中了解测量性能和卡顿情况。
组件本身不一定要非常复杂,但您确实需要一些能够练习布局、文本、图片等常见元素的东西,以及一些您通常会看到的各种不同实例。
在此区域中,您应该注意并可能预期到卡顿和延迟会有所增加,虽然幅度很小,但这是因为您现在运行了两个堆栈。
您有一个完全在 Compose 中的渲染堆栈,还有一个在 View 中的渲染堆栈。
因此,会出现一些内存抖动、一些额外的类加载等情况。
但随着您越来越多地采用 Compose,这种情况将会消失。
如果您在此期间确实需要它,可以对该屏幕进行一些轻微的预热,以帮助其保持一致,并在短期内降低该成本。
因此,在步行之后,我们开始慢跑。
所以,这是在我们已经非常非常熟练之后。
不要只对高流量页面上的单个组件进行测试,而是要对整个高流量页面进行测试。
因此,我们保留了导航库并使用 Fragment,但对于每个单独的屏幕,我们完全使用 Compose 实现了。
我们只是让 Fragment 返回 ComposeView 本身。
因此,您可以期待延迟时间和卡顿问题得到改善。
您不会遇到我之前提到的内存抖动和类加载问题。
Compose 本身也会在 ComposeView 级别进行一些缓存。
因此,如果您的 RecyclerView 中有许多外观一致且可能具有相同图标的卡片,那么所有这些图标解析和资源使用都会在 ComposeView 中的所有内容之间共享。
之后,我们开始跑步。
因此,在这个阶段,您需要全力以赴。
当应用的大部分流量都来自 Compose 时,我们便更上一层楼。
因此,在屏幕之上,我们有 Activity。
因此,请将 ComposeView 移至 Activity 根。
而且您的许多代码看起来都相对相似。
您仍在尝试执行导航指令,例如从一个屏幕切换到另一个屏幕。
但您无需执行 Fragment 事务,而是调用可组合函数。
无论您选择 Nav2 库还是 Nav3 库,都可以在其基础上实现此目的。
对我们来说,我们并没有迁移所有界面。
我确实提到过,我们有很多这样的功能。
因此,我们构建了 Fragment 兼容性,让您能够拥有基于 Fragment 的界面,并利用 Compose 的 AndroidFragment 封装容器来实现这一点。
卡顿和延迟对我们的业务至关重要,因此我们一直在不断突破极限,您也会看到这些问题在不断改善,因为我们现在实现了跨屏幕缓存,并且大大减少了内存抖动问题。
但您也会获得新功能。
右侧的 GIF 示例展示了如何在屏幕之间实现共享元素过渡。
因此,当您从一个屏幕切换到另一个屏幕时,我们会放大图片,从而提供连续性。
在整个过渡过程中,您还可以充分利用 Compose 的所有动画灵活性。
接下来,我将把时间交给 Kirill,由他来介绍更多功能。
谢谢 Jon。
我想谈谈一个我非常感兴趣的话题,那就是 Android 应用的主题和样式,这也是我一直在做的事情。
在 View 世界中,有 Java 代码,后来又有了 Kotlin 代码,还有 X-- XML 资源,例如颜色、字符串、尺寸、布局、主题和样式。
其工作方式是,所有这些 XML 资源都会编译到 APK 中包含的资源表中,并且基本上是静态的。
有一个 API Resources.Theme.applyStyle,您可以在运行时动态使用它,在界面层次结构之上应用自定义样式。
主要限制是,您必须在膨胀内容之前执行此操作,这意味着,如果您想在现有界面上动态应用该样式,则必须销毁并重新创建整个 Activity,这对用户体验不利。
此外,由于它应用的是这些预编译的 XML 块,因此很难根据实验标志或任何其他业务需求,将多个条件动态叠加层组合在一起。
在 XML 世界中,很难构建一个合适的样式层次结构心智模型。
最后,Android Studio 或任何其他开发者工具中都没有一个统一的视图,可让您在 Java 世界、代码世界和 XML 世界之间架起桥梁。
这有点难以兼顾,很难建立正确的心理模型,也很难调试为什么我的样式没有应用、为什么我得到了错误的颜色等问题。
在 Compose 的新世界中,特别是对于 Material Design 系统和 Compose Material 库,以及其他设计体系,我们为 Material Design 系统提供了一个 MaterialTheme 可组合项,您可以将其应用于屏幕上界面层次结构的顶层,也可以将其作为叠加层应用于界面的任何较小部分。
如果您查看右侧的屏幕,可以看到顶部有一个 Play Points 黄金级会员卡片,这是一个带有进度指示器和一些文本的小卡片。
我们使用 Material Color Utilities 库生成的自定义配色方案来应用此 Material 主题叠加层,该库可在 GitHub 上供您使用。
我们传入一种类似棕黄色的金色作为种子,它会生成全光谱的 Material 颜色令牌,包括主要颜色、主要颜色上的颜色、次要颜色、次要颜色上的颜色,然后我们在实现此卡片时使用这些颜色。
这让 Android 开发者能够充分利用动态主题功能。
所有这些都是在代码中完成的,都是在 Kotlin 中完成的,这意味着您可以更轻松地构建关于其运作方式的正确心智模型,并且更轻松地进行调试。
在内部,这些叠加层会作为 CompositionLocal 附加到您选择应用它们的层次结构中的特定界面节点。
性能注意事项:使用 Material Color Utilities 库生成完整的配色方案并非免费。
它确实会带来一定的开销。
因此,如果您的界面足够复杂,需要多个这样的组件,比如某种便当盒布局,或者根据产品需求,您在运行时生成了多个配色方案,那么这些开销就会开始累积,导致延迟和卡顿。
因此,首先,您需要开始衡量它对性能的影响;其次,您可以将部分开销从运行时转移到构建时。
您可以在构建时使用相同的 Material Color Utilities 库,根据品牌需求或产品需求预先生成这些配色方案,并将此开销转移到构建时。
我之前提到过,有些 CompositionLocal 会跟踪叠加层在界面层次结构树中的确切位置,这些 CompositionLocal 的开销要低得多,但仍然不是免费的。
因此,根据您使用的数量(数十个、数百个),这些费用会开始累积。
在某个时间点,您确实需要开始考虑所有这些组合式 CompositionLocal 的成本。
这就是我之前提到的 Compose 真正的动态运行时主题设置功能的一个示例。
这里有四个象限,相同的视觉效果,相同的事件卡片。
我们有这张主打图片,它占据了整个卡片,然后左上角有一个标签,右下角有一个按钮,还有一些文字。
在每个象限的右侧,您可以看到颜色令牌的标注,这些令牌用作每个元素的设计规范。
这四种卡片的实现方式完全相同。
唯一的区别在于,封装卡片的 Material 主题设置叠加层是使用根据图片主色(蓝色、紫色、黄色和红色)动态创建的配色方案生成或配置的。
其他所有内容均保持不变。
您无需销毁并重新创建 activity。
您可以根据实验标志或任何其他业务需求,有条件地应用该功能。
正如我之前所说,这是一种更加动态的方式,为应用应用颜色自定义,让应用世界变得更加生动。
现在,Jon 将继续讲解下一部分。
没错
这就是 Kirill 提到的更多灵活性。
我们来谈谈可靠性和速度。
因此,快速构建高质量代码是所有开发者和企业都在追求的目标。
接下来,我们来看看 Compose 如何帮助 Google Play 实现这一目标。
首先,我知道这个图表很大,但架构的简单性对所有代码作者和维护者(无论是人类还是智能体)都有巨大的好处。
我不会对此进行深入探讨,但 Compose 试图提出的一个心智模型是围绕单向数据流,这对我们的团队产生了很大的影响。
对于不熟悉其工作原理的用户,基本原理是您有一个数据源。
数据源提供数据。
这些数据可能会转换为特定的界面,我们将其输出为界面模型。
然后,Compose 只需调用该状态的值属性,即可轻松观察到该状态。
这就像是此图表顶部的箭头。
然后,事件会反向流动。
因此,如果用户点击某个按钮,该操作会返回到界面转换器,更新数据源,发出新值,运行转换,从而形成一个循环。
因此,所有内容都会通过这个相对简单的循环运行。
我们发现,这种做法的真正好处在于有助于缩短磨合期。
您无需再担心这些调用是在主线程上还是在其他线程上发生。
它遵循一个通用循环。
Compose 让数据观察变得非常简单,并且数据在各个架构层之间具有强一致性保证。
该心智模型减少了我们团队内部关于可靠来源的许多讨论,并彻底消除了几种类型的竞态条件。
对我们来说,另一个巨大的好处是,与我们之前采用的模型-视图-控制器架构相比,并行执行数据源和界面转换变得非常简单。
因此,这确实缩短了我们在 Play 商店中的加载延迟时间,缩短了大约 50 毫秒。
这让我们印象深刻。
除了单向数据流,Compose 的另一个重要优势在于声明式界面。
Kirill 已经稍微提到了这一点。
因此,Google Play 开发者也对这项功能赞不绝口。
界面控制流现在位于单个文件中。
右侧有一个非常简单的示例。
@Composable
fun PurchaseRow(onClick: () -> Unit, modifier: Modifier) {
Row(modifier) {
Text("20% off sale")
if (LocalExpts.current.useButtonV2) {
ButtonV2("Buy", onClick = onClick)
} else {
ButtonV1("Buy", onClick = onClick)
}
}
}我们将根据实验标志在两种按钮实现之间切换。
您无需阅读多个视图文件、fragment 文件和 XML 文件,只需从上到下阅读即可,这让开发工作变得简单得多。
嵌套可组合项的成本也远低于嵌套视图层次结构,这使我们能够自然地排列代码,使其更具可读性,而无需为了性能而精打细算。
我们还发现,生命周期回调变得简单得多,需要担心的回调数量也减少了。
因此,您无需处理视图生命周期、fragment 生命周期和 activity 生命周期,而是会获得 DisposableEffect 和 副作用,并且无论您将它们放在哪里,它们都会以非常一致的方式运行。
因此,这对我们来说是一个巨大的优势。
因此,在我看来,这两项架构变革确实为 LLM 奠定了基础,使其能够生成相当高质量的代码。
我相信,你们中的许多人可能已经尝试过或使用过自己喜欢的 LLM。
您可以截取一张带有颜色和排版注释的屏幕截图,然后将其输入,您会发现它能生成行、列、文本和按钮,而且效果还不错。
不过,它也有其局限性。
而 Play 真正看到的是,它不了解您的代码库,对吧?
它不知道您已经构建了哪些应该重复使用的组件,也不知道设计令牌名称。
它只会为您提供硬编码的颜色和字符串。
哪些内容应该来自设备,哪些内容应该来自服务器,或者如何确保在使用 LazyList 时性能良好,而不仅仅是创建一个巨大的 Column?
因此,为了解决其中一些问题,我们为这些屏幕截图添加了更多元数据。
因此,界面名称、边界框(以便我们知道组件的起始和结束位置)、设计令牌(用于颜色和排版的语义名称)、技能,以及一些提示(用于说明这些组件将放置在何处,以便考虑性能因素)。
然后,只需提供一个非常高级的函数签名,以便告知它哪些内容是动态的,哪些内容是静态的。
因此,您最终会得到类似之前的内容,然后移至之后。
我没有在这里画出边界框,因为这会给图表增加很多干扰。
但您可以看到,这些硬编码的颜色和排版名称已被替换为 title large 和 on surface。
我们清楚地标出了卡片标题和图标标记等组件名称。
这样,您就可以在代码库中找到这些内容。
对我们来说,很多这些组件也来自通用设计库,Kirill 稍后会对此进行介绍。
因此,您无需始终为所有这些产品添加自定义标签。
您将其与方法签名(如底部的 OfferCard)配对,然后它就会知道标题、endDate、图片都是动态的。
我们不应硬编码这些内容。
除了这些语义名称之外,我们现在还可以切换到 Kirill 提到的所有这些不同的颜色应用。
这样一来,我们就能生成更多代码。
接下来,我将把时间交给 Kirill,由他来讲解我们如何维护质量。
是的。
谢谢 Jon。
接下来,我们继续探讨 LLM 和智能体辅助开发,首先从用户体验设计方面入手。
无论您选择哪种设计工具(可能是 Figma,也可能是其他工具),您都需要有一个共享模板,供所有设计师使用,其中包含您所选设计体系中的颜色令牌、排版令牌和形状令牌。
也许是 Material。
也许是您的自定义设计体系。
具体而言,对于颜色令牌,您希望这些模板内置支持在浅色和深色模式之间切换,以及在几年前 Android 级别引入的正常、中等和高对比度模式之间切换。
这样,设计师就可以在这些不同的组合之间快速切换,了解其设计规范对此有何反应。
然后,在设计稿定稿并移交给实现端时,您希望在导出流水线中具备某种能力,能够跟踪每种颜色、每个排版元素、每个形状是否都使用了匹配的令牌,而不是硬编码的 RGB 值、硬编码的字体系列或字体大小。
最后,我们发现,在 Google Play 这样的大规模应用中,我们有数百个组件、集群、卡片和容器,它们在各种屏幕上使用,非常实用。
我们发现,在一个地方(我们称之为组件目录)跟踪所有这些信息,然后根据该组件目录训练自定义智能体,是非常有用的。
当您开始设计新界面、新用户体验历程或新产品领域时,这个智能体就派上用场了。您可以将初始设计探索(例如幻灯片或 Figma 规范)输入到智能体中,它会快速识别哪些组件已经存在,从而避免重复设计和规范,也无需从头开始重新实现。
这大大降低了设计成本和实施成本。
在实现方面,我们非常依赖核心和自定义 Lint 规则。
我想特别指出两个核心 Lint 规则:FrequentlyChangingValueDetector,它有助于识别不必要的重组来源,这些来源可能会导致延迟和卡顿;另一个是 ScaffoldPaddingDetector,用于标记我们使用 Material Compose Scaffold 可组合项但忽略传入内边距的情况。
它不是延迟和卡顿的来源,但在某些情况下,它可能会导致非常难以调试的问题,例如在某些情况下(如边到边、折叠、展开)出现布局错位。
最后,BoxWithConstraints 存在一些性能方面的考量。
可能需要两帧才能正确布局所有内容。
我们不会直接阻止它,但会将其标记为 Lint 警告,以便正在处理该特定屏幕的开发者知道他们正在做出使用 BoxWithConstraints 的特定选择,或者他们可以选择为该特定布局使用其他内容。
对于主题自动化,同样有颜色令牌、排版令牌和形状令牌。
我们有一条 Lint 规则,用于禁止使用硬编码颜色和硬编码排版。
我们有一条 Lint 规则,用于标记 Material 2 组件的使用情况。
我们只想使用最新、最棒的 Material 3,包括去年和今年推出的全新富有表现力的组件。
我们希望,我们有一个自定义 Lint 规则,可以防止人们从旧版 XML 文件中读取颜色。
我们希望在新界面中优先使用 Compose,并且仅使用 Compose。
此外,Text 可组合项(尤其是在 Material Design 设计体系中)具有样式和颜色的后备。
它们会从本地内容颜色和本地排版中读取这些值,但我们不想依赖于此。
我们希望每个文本都刻意使用设计规范中的特定样式、排版样式和特定颜色。
这就是我们的一条 Lint 规则。
最后一个是,与 ScaffoldPaddingDetector 类似,我们有一个自定义 Lint 规则,该规则强制要求我们拥有的每个可组合函数都使用其输入修饰符。
同样,这也是为了防止新用途被错误放置、不尊重传入背景或出现其他问题。