应用基础知识

将 Google Play 应用 20 年的代码迁移到 Jetpack Compose

扬声器

  • Kirill Grouchnikov

    软件工程师

  • Jon Boekenoogen

    软件工程师

关于

演讲简介

今年,Android 正式全面转向 Compose 优先,将其作为构建所有界面的首选,而旧版 View 体系则进入维护模式。对于 Google Play 而言,这段旅程始于 2020 年。在近二十年的工程历程中,我们服务了数十亿用户,通过行之有效的最佳实践、与 Compose 核心团队的紧密协作以及持续的工具演进,成功迁移到了 Jetpack Compose。

了解如何通过 Compose 对 Google Play 进行现代化改造,从而在速度和长期稳定性方面实现可衡量的提升。本讲座将介绍如何将智能体 Figma 设计工作流与 Compose 实现相结合,通过自动化质量控制来避免不必要的重组,并利用动态颜色、丰富的排版和分层形状打造富有表现力的界面。您将掌握经过验证的策略,从而能够更快地对 bug 进行分类,并自信地扩大 Compose 迁移规模。

摘要

  • Google Play 在六年内将 97% 的用户展示次数转移到了纯 Compose 界面。在过去 14 个月中,主要网页的卡顿率也降低了 30%。
  • 逐步采用可降低大型代码库的业务风险。团队可以先在低流量屏幕上熟悉 Compose,然后逐步过渡到直接在 Activity 根级托管 ComposeView。
  • 动态主题完全在代码中运行,使用 MaterialTheme 叠加层和 Material Color Utilities 库。这样可以避免 Activity 销毁,不过在构建时生成配色方案有助于限制运行时延迟。
  • 单向数据流和声明式界面可简化架构并消除多种类型的竞态条件。通过并行运行数据源和界面转换,Google Play 的加载延迟时间缩短了数十毫秒。
  • 自动化 Lint 检查可强制执行代码质量和设计体系规则。Play 会使用这些规则来阻止硬编码颜色,并标记潜在的性能陷阱,例如 BoxWithConstraints。

Gemini Notebook

主题

  • jetpack-compose
  • 迁移
  • 性能
  • 主题关联
  • unidirectional-data-flow
  • lint-rules
  • developer-velocity

转写文稿

简介和 Google Play 商店采用情况

  • 欢迎大家!

  • 我叫 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 奠定了基础,使其能够生成相当高质量的代码。

  • 我相信,你们中的许多人可能已经尝试过或使用过自己喜欢的 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 规则,该规则强制要求我们拥有的每个可组合函数都使用其输入修饰符。

  • 同样,这也是为了防止新用途被错误放置、不尊重传入背景或出现其他问题。

总结

  • 以上就是我们希望您从本次演讲中了解到的三个要点。

  • 首先,我们认为,在 2026 年底,Compose 已成为一款成熟的界面工具包,并已在大规模应用中得到验证。

  • Android 本身也优先采用 Compose。

  • 在今年早些时候的 Google I/O 大会上,我们宣布所有界面库和所有智能体工作流都将采用 Compose 优先。

  • 正如 Jon 在演讲中提到的,循序渐进,我们建议您逐步迁移,尤其是大型代码库,以加快开发者流程和智能体流程的速度。

  • 最后,感谢大家今天抽出宝贵时间参加本次活动。稍后,我和 Jon 将在场外回答大家的任何问题。

  • 再次感谢您抽出宝贵时间。

代码

代码段

“提高可靠性和速度”中 16:09 处的 Kotlin 代码段
@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)
    }
  }
}