應用程式基礎

將 Google Play 應用程式遷移至 Jetpack Compose

喇叭

  • Kirill Grouchnikov

    軟體工程師

  • Jon Boekenoogen

    軟體工程師

關於

講座簡介

今年,Android 正式改為優先使用 Compose 建構所有 UI,並將舊版 View 設為維護模式。Google Play 早在 2020 年就開始這項工作。在近二十年的工程歷程中,我們服務了數十億使用者,並透過經過驗證的最佳做法、與核心 Compose 團隊密切合作,以及持續演進的工具,成功遷移至 Jetpack Compose。

瞭解如何使用 Compose 翻新 Google Play,大幅提升速度和長期穩定性。本課程將逐步說明如何將具備代理功能的 Figma 設計工作流程與 Compose 實作連結、自動化品質控管以避免不必要的重新組合,以及運用動態色彩、豐富的字體排版和分層形狀,打造富有表現力的介面。您將學到經過驗證的策略,能更快分類錯誤,並有信心擴大 Compose 遷移規模。

摘要

  • 六年來,Google Play 將 97% 的使用者曝光次數轉移到僅限 Compose 的畫面。過去 14 個月內,主要網頁的卡頓率也改善了 30%。
  • 逐步採用可降低大型程式碼集的業務風險。團隊可以先在流量較低的畫面上熟悉這項功能,然後再直接在 Activity 根層級託管 ComposeView。
  • 動態主題設定完全在程式碼中運作,使用 MaterialTheme 疊加層和 Material Color Utilities 程式庫。這可避免 Activity 終止,但在建構時產生色彩配置,有助於限制執行階段延遲。
  • 單向資料流和宣告式 UI 可簡化架構,並移除多種競爭狀況。同時執行資料來源和 UI 轉換,可將 Google Play 的載入延遲時間縮短數十毫秒。
  • 自動 Lint 檢查會強制執行程式碼品質和設計系統規則。Play 會使用這些規則封鎖硬式編碼顏色,並標記潛在的效能問題,例如 BoxWithConstraints。

Gemini Notebook

主題

  • jetpack-compose
  • 遷移
  • performance
  • 主題設定
  • unidirectional-data-flow
  • lint-rules
  • developer-velocity

轉錄稿

簡介和 Google Play 商店採用情形

  • 大家好!

  • 我是 Jon。

  • 我是 Google Play 商店團隊的成員。

  • 這位是我的同事 Kirill。

  • 今天我們要談談如何將大量程式碼從 View 和 Fragment 系統移至 Compose。

  • 讓我們開始吧!

  • 那麼,採用過程如何?規模有多大?

  • 自 2007 年左右起,我們的應用程式程式碼集就自然而然地成長。

  • 我們有數百萬行正式環境程式碼、200 個畫面,以及 500 個不重複的 UI 元件,這些元件會用於許多不同的畫面。

  • 2020 年,我們發現開發人員速度、可靠性和效能開始無法達到預期目標。

  • 因此,我們決定採用現代化的 UI 技術堆疊。

  • 我們評估了許多不同的程式庫。

  • 當時,我們也和 AndroidX 的一些朋友討論 Compose,並決定採用 Compose,成為 Compose 的第一個主要合作夥伴。

  • 還順利嗎?

  • 六年後,Play 商店應用程式中 97% 的使用者曝光都發生在僅限 Compose 的畫面上。

  • 抱歉,自 2020 年起,所有手機和大型螢幕開發作業都已改為僅限 Compose。

  • 自 2025 年起,我們甚至將 XR、TV、Auto、Wear 等擴充板型規格也轉移至 Compose。

  • Compose 是我們能成功的重要推手,這也是我們持續採用更多 Compose 的原因。

  • 他們擁有非常強大、全面的測試套件,可協助我們每週兩次採用新版 Compose。

  • 我們導入了 Alpha 版和新 API,因此能利用新的錯誤修正和效能改善項目,並真正探索部分新 API,提供新的使用者體驗。

  • 特別是效能方面,Compose 確實為我們應用程式的所有主要畫面,持續提供流暢的效能。

  • 包括搜尋結果網頁、首頁和詳細資料頁面。

  • 效能、卡頓和載入延遲時間,對我們的業務和使用者體驗來說,都是非常重要的指標。

  • 因此,過去 14 個月內,我們看到 30% 的改善。

遷移計畫

  • 但這一切究竟是怎麼開始的?

  • 這才是我們今天的主題。

  • 接下來,我們來談談遷移計畫。

  • 因此,Play 採取多步驟做法。

  • 我們不想孤注一擲,

  • 我們最後採取了幾個步驟,瞭解風險和對業務的影響,並在全力投入前建立信心。

  • 我將這些階段命名為:爬行、步行、慢跑和跑步。

  • 因此,我們在起步階段的目標是熟悉 Compose。

  • 我們一開始先從流量較低的網頁著手。

  • 例如設定頁面、第三方授權等。

  • 您的主要目標是熟悉 Compose 程式庫。

  • 因此,這可以習慣資料列、資料欄、文字、圖片等所有這類內容。

  • 接著,您需要開始導入所有應用程式基礎架構,並瞭解其在 Compose 中的運作方式。

  • 包括曝光記錄、點擊記錄、主題設定,以及資料載入。

  • 所以上述所有因素都有影響。

  • 我們發現,確保所有畫面看起來一致,是很有幫助的做法。我們建構了一些單元測試和功能測試,檢查 Compose 中的主題設定物件是否直接對應至 XML 樣式、字體排版、顏色等所有項目。

  • 因此,當我們熟悉並感到自在後,就決定開始步行。

  • 對我們來說,這是在流量非常高的畫面上,採用較小的元件,並評估這類遷移作業的情況。

  • 建議您採用 RecyclerView 中的項目,這樣就能瞭解 View 和 Compose 系統之間的相容性問題,並在過程中查看測量效能和卡頓情形。

  • 元件本身不一定要非常複雜,但您確實需要一些能練習版面配置、文字、圖片等內容的東西,以及一些您通常會看到的各種不同例項。

  • 在這個領域中,您應該注意並預期會發生的情況是,卡頓和延遲時間會稍微增加,但這是因為您現在執行兩個堆疊。

  • 您有一個完全在 Compose 中的轉譯堆疊,以及一個在 View 中的轉譯堆疊。

  • 因此會發生記憶體顛簸、載入額外類別等情況。

  • 但隨著您採用越來越多 Compose,這種情況就會消失。

  • 如果真的需要,可以稍微預熱該畫面,讓畫面符合需求,並在短期內降低成本。

  • 所以,在步行之後,我們轉為慢跑。

  • 這表示我們已經非常熟悉這項技術。

  • 不要只在高流量網頁上測試單一元件,而是測試整個高流量網頁。

  • 因此,我們仍保留導覽程式庫並使用片段,但每個畫面都完全以 Compose 實作。

  • 我們剛才讓 Fragment 傳回 ComposeView 本身。

  • 因此,您可以預期延遲和卡頓情形會有所改善。

  • 您不會遇到我剛才提到的記憶體顛簸和類別載入問題。

  • Compose 本身也會在 ComposeView 層級進行某些層級的快取。

  • 因此,如果 RecyclerView 有許多外觀一致且可能具有相同圖示的資訊卡,則所有圖示剖析和資源使用情形都會在 ComposeView 中的所有項目之間共用。

  • 所以之後我們就開始跑步。

  • 這時你就要全力以赴。

  • 當應用程式的大部分流量都來自 Compose 時,我們就會向上移動一個層級。

  • 因此,在畫面之上,我們有 Activity。

  • 因此,請將 ComposeView 移至 Activity 根層級。

  • 許多程式碼看起來會很相似。

  • 您仍會嘗試執行導覽指令,例如從一個畫面切換到另一個畫面。

  • 但您不是執行片段交易,而是呼叫可組合函式。

  • 無論選擇 Nav2 還是 Nav3 程式庫,都可以在其基礎上進行這項操作。

  • 我們並未遷移所有畫面。

  • 我說過,我們有很多這類工具。

  • 因此,我們建構了 Fragment 相容性,讓您可以使用以 Fragment 為基礎的畫面,並利用 Compose 的 AndroidFragment 包裝函式來執行這項操作。

  • 再次強調,卡頓和延遲對我們的業務至關重要,因此我們不斷挑戰極限,您也會看到這方面的改善,因為我們現在有跨螢幕快取,記憶體抖動問題也大幅減少。

  • 但也會獲得新功能。

  • 右側的 GIF 範例顯示,您可以在畫面之間進行共用元素轉場。

  • 因此,當您從一個畫面移至另一個畫面時,圖片會放大,提供連續性。

  • 在整個轉換過程中,您也可以充分運用 Compose 的動畫彈性。

  • 接下來,我將交給 Kirill 介紹更多功能。

主題設定和效能

  • 謝謝 Jon。

  • 我想談談一個我非常重視的主題,那就是 Android 應用程式的主題設定和樣式設定,這是我一直以來都在做的事情。

  • 在 View 的世界中,有 Java 程式碼,稍後還有 Kotlin 程式碼,然後是 X-- XML 資源,例如顏色、字串、尺寸、版面配置、主題和樣式。

  • 運作方式是將所有 XML 資源編譯成 APK 內含的資源資料表,而且幾乎是靜態的。

  • 您可以在執行階段動態使用 API Resources.Theme.applyStyle,在 UI 階層頂端套用自訂樣式。

  • 主要限制是您必須在加載內容前完成這項操作,也就是說,如果您想在現有的 UI 上動態套用該樣式,就必須拆除並重新建立整個 Activity,這對使用者體驗來說並不好。

  • 此外,由於系統會套用這些預先編譯的 XML 區塊,因此根據實驗旗標或任何其他業務需求,將多個條件式動態疊加層相互結合,會變得有點困難。

  • 在 XML 世界中,很難建立適當的樣式階層心智模型。

  • 最後,Android Studio 或任何其他開發人員工具都沒有單一整合檢視畫面,可讓您在 Java、程式碼和 XML 之間架起橋樑。

  • 這有點難以兼顧,也難以建立適當的心智模型,以及偵錯為何未套用樣式、為何顏色錯誤等問題。

  • 在 Compose 的新世界中,特別是 Material Design 系統和 Compose Material 程式庫,以及其他設計系統,我們有 Material Design 系統,也有 MaterialTheme 可組合函式,您可以將其套用至螢幕上 UI 階層的最頂層,也可以將此 MaterialTheme 做為疊加層套用至 UI 的任何較小部分。

  • 右側畫面頂端顯示 Play Points 黃金級,這是一張小卡片,上面有進度指標和一些文字。

  • 我們使用 Material Color Utilities 程式庫產生的自訂色彩配置,套用這個 Material Design 主題疊加層,您可以在 GitHub 上使用。

  • 我們傳入類似這種棕黃色、金色的顏色做為種子,然後產生完整的 Material 顏色符記,主要顏色在主要顏色上,次要顏色在次要顏色上,然後我們在實作這張卡片時使用這些顏色。

  • Android 開發人員也能充分運用動態主題功能。

  • 所有作業都是透過程式碼完成,而且都是在 Kotlin 中完成,這表示您可以更輕鬆地建立適當的心智模型,瞭解所有作業的運作方式,也更容易偵錯。

  • 在內部,這些疊加層會附加為組合本機,套用至階層中您選擇的特定 UI 節點。

  • 效能考量:使用 Material Color Utilities 程式庫產生完整色彩配置並非免費。

  • 不過,這會產生一定的負擔。

  • 因此,如果您的產品需求較為複雜,需要多個這類元件 (例如便當盒等),且在執行階段產生多種色彩配置,就會開始增加延遲和卡頓。

  • 因此,您需要:a. 開始評估這對效能的影響;b. 將部分負擔從執行階段轉移至建構階段。

  • 您可以在建構時使用相同的 Material Color Utilities 程式庫,根據品牌或產品需求預先產生這些色彩配置,並將這項負擔轉移至建構時間。

  • 我提到用來追蹤疊加層在 UI 階層樹狀結構中確切位置的組合區域,這些區域的成本較低,但仍非免費。

  • 因此,視您使用的數量而定,數十個、數百個,就會開始增加。

  • 在某些情況下,您確實需要開始考量所有這些組合式本機的成本。

  • 這就是我提到的 Compose 真正的動態執行階段主題設定功能。

  • 這裡有四個象限,相同的視覺效果,相同的事件資訊卡。

  • 這張主頁橫幅橫跨整張資訊卡,左上角有標籤,右下角有按鈕,還有一些文字。

  • 在每個象限的右側,您可以看到顏色符記的標註,這些符記是每個元素的設計規格。

  • 這四個項目的資訊卡實作方式完全相同。

  • 唯一的差別在於,包裝資訊卡的 Material Design 主題設定疊加層,是根據圖片的主要顏色 (藍色、紫色、黃色和紅色),以動態建立的色彩配置生成或設定。

  • 其他部分則維持不變。

  • 您不必拆除並重新建立活動。

  • 您可以根據實驗旗標或任何其他業務需求,有條件地套用。

  • 如前所述,這是一種更動態、真正動態的方式,可將自訂色彩套用至應用程式。

  • 現在請 Jon 繼續介紹下一個部分。

提升可靠性和速度

  • 是啊

  • 這就是 Kirill 提到的靈活性。

  • 我們來談談可靠性和速度。

  • 因此,所有開發人員和企業都希望快速建構高品質程式碼。

  • 接下來,我們來看看 Compose 如何協助 Play 達成這個目標。

  • 首先,我知道這張圖表很大,但簡化架構對所有程式碼作者和維護人員 (無論是人類還是代理) 都有很大的好處。

  • 我不會深入探討這個問題,但 Compose 嘗試提出的其中一個心智模型是單向資料流,這對我們的團隊產生了很大的影響。

  • 如果不熟悉運作方式,基本概念是您有一個資料來源。

  • 資料來源會提供資料。

  • 這類資料可能會轉換為特定 UI,並以 UI 模型的形式輸出。

  • Compose 只要呼叫該狀態的值屬性,就能輕鬆觀察到這項資訊。

  • 這就像是圖表頂端的箭頭。

  • 事件則會反向流動。

  • 因此,如果使用者點選按鈕,系統會返回 UI 轉換器,更新資料來源並發出新值,然後執行轉換,形成這個迴圈。

  • 因此,所有內容都會透過這個相對簡單的迴圈執行。

  • 我們發現,這項做法有助於縮短適應期。

  • 您不必再擔心這些呼叫是在主執行緒上發生,還是從其他執行緒上發生。

  • 它遵循一個一般迴圈。

  • Compose 讓資料觀測變得非常簡單,且資料在各架構層之間具有強大的同步一致保證。

  • 這個心智模型減少了團隊中許多關於可靠資料來源的討論,並完全消除了幾種競爭狀況。

  • 另一個巨大優勢是,與我們之前的模型檢視控制器架構相比,平行執行資料來源和 UI 轉換變得非常簡單。

  • 因此,這確實改善了商店內的載入延遲時間,大約縮短了 50 毫秒。

  • 這讓我們印象深刻。

  • 除了單向資料流,Compose 的另一大優勢是宣告式 UI。

  • Kirill 稍早有提到這點。

  • 因此,Play 開發人員也對這項功能讚譽有加。

  • UI 控制流程現在位於單一檔案中。

  • 右側範例相當簡單明瞭。

    @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)
        }
      }
    }
  • 我們會根據實驗旗標,在兩種按鈕實作方式之間切換。

  • 您不必閱讀多個檢視畫面檔案、片段檔案和 XML 檔案,只要從上到下閱讀即可,這樣會簡單得多。

  • 巢狀可組合項的成本也遠低於巢狀檢視區塊階層,因此我們能自然地安排程式碼,提高可讀性,而不必為了效能而費盡心思。

  • 我們發現的其他好處是,生命週期回呼變得更簡單,需要擔心的數量也減少了。

  • 因此,您不必使用檢視畫面生命週期、片段生命週期、活動生命週期,而是使用可處置效果、副作用,並根據您放置的位置,以非常一致的方式運作。

  • 這對我們來說是一大優勢。

為 LLM 奠定基礎

  • 因此,我認為這兩項架構變更為 LLM 奠定了基礎,使其能生成相當高品質的程式碼。

  • 相信許多人可能都試過,或用過自己最喜歡的大型語言模型。

  • 您可以擷取螢幕截圖,並加上顏色和字體排版註解,然後將其輸入,看看它是否能生成資料列、資料欄、文字和按鈕,而且效果相當不錯。

  • 不過,這項功能仍有其限制。

  • Play 其實不瞭解您的程式碼集,對吧?

  • 它不知道您已建構哪些元件,也不知道設計權杖名稱。

  • 只會提供硬式編碼的顏色和字串。

  • 哪些內容應來自裝置、哪些內容應來自伺服器,或如何確保使用 LazyList 時效能良好,而不只是建立大型資料欄?

  • 因此,為瞭解決部分問題,我們在這些螢幕截圖中加上更多中繼資料。

  • 包括 UI 名稱、定界框 (用於判斷元件的起點和終點)、設計符記 (用於顏色和字體排版的語意名稱)、技能,以及一些提示 (用於說明元件的放置位置,以便考量效能)。

  • 以及非常高階的函式簽章,以便區分動態和靜態內容。

  • 因此,您最終會得到類似我們之前的結果,然後移至之後。

  • 這裡沒有定界框,因為這會為圖表增加許多雜訊。

  • 但您可以看到這些硬式編碼的顏色和字體排版名稱,已替換為 title large 和 on surface。

  • 我們清楚標示了資訊卡標題和圖示徽章等元件名稱。

  • 這樣一來,您就能在程式碼集中找到這些內容。

  • 對我們來說,許多這類元件也來自常見的設計程式庫,Kirill 稍後會說明。

  • 因此,您不需要一直為所有產品加上自訂標籤。

  • 接著,您將其與方法簽章配對,如我們在底部 OfferCard 所示,然後它就會知道標題、endDate、圖片都是動態的。

  • 不應硬式編碼。

  • 有了這些語意名稱,我們現在可以切換到 Kirill 提到的所有不同顏色應用程式。

  • 因此,我們生成了更多程式碼。

  • 接下來,我會將時間交還給 Kirill,由他說明我們如何維護品質。

品質自動化

  • 好

  • 謝謝 Jon。

  • 接著,我們繼續探討 LLM 和 Agent Assist 輔助開發,首先從使用者體驗設計開始。

  • 無論您選擇哪種設計工具 (例如 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。

  • 此外,文字可組合項 (特別是 Material Design 設計系統中的文字可組合項) 具有樣式和顏色的備用設定。

  • 系統會從本機內容顏色和本機字體排版讀取,但我們不想依賴這項功能。

  • 我們希望每段文字都能刻意使用設計規格中的特定樣式、字體樣式和特定顏色。

  • 這就是我們提供的 Lint 規則。

  • 最後,與 ScaffoldPaddingDetector 類似,我們有一項自訂 Lint 規則,強制要求每個可組合函式都使用其輸入修飾符。

  • 同樣地,這也是為了避免新用途不當放置,或不尊重傳入的背景等。

結論

  • 以上是我們希望您從這份簡報中瞭解的三個重點。

  • 首先,我們認為 Compose 在 2026 年底已是成熟的 UI 工具包,並已通過大規模驗證。

  • Android 本身也以 Compose 為優先。

  • 在今年稍早的 Google I/O 大會上,我們宣布所有 UI 程式庫和代理流程都將以 Compose 為優先。

  • 正如 Jon 在簡報中提到的,我們建議您逐步遷移,尤其是大型程式碼集,以加快開發人員流程和代理流程的速度。

  • 最後,感謝各位今天撥冗參與。幾分鐘後,我和 Jon 會在外面回答各位的問題。

  • 再次感謝您撥冗參與。

程式碼

程式碼片段

「Scaling reliability and velocity」影片 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)
    }
  }
}