Jetpack Compose(1) —— Android 全新的 UI 框架

寫在前面

Jetpack Compose 已經不是什麼新技術了,Google 早在 2019 年就推出 Jetpack Compose 的首個 alpha 版本,時至今日,相當大比例的國內 Android 開發者還沒有學習使用過。本篇文章主要介紹 Jetpack Compose 的一些發展背景,接下來會有一個系列的文章,來逐步講述 Jetpack Compose 的一些知識點。

一、Jetpack Compose 是什麼

1.1 全新的 Android UI 開發框架

Jetpack Compose 是用於構建原生 Android 界面的新工具包,是一種全新的聲明式 UI 框架,它使用更少的代碼、更直觀的 Kotlin API 以及擁有更強大的開發工具,可以幫助開發者加快 Android 開發界面的開發。你可以理解爲是傳統的 View 視圖體系的替代品。

關鍵詞原生界面開發Kotlin 語言聲明式 UI

1.2 命令式UI 與 聲明式UI

傳統的 View 視圖體系屬於命令式的編程範式,什麼是命令式?就是我們需要用命令的方式告訴計算機如何去做事情。比如先獲取到一個 view 的對象的引用,再通過調用它的 setXXX() 方法去改變這個 view 的屬性,以此來更改 UI 狀態。手動操縱視圖會提高出錯的可能性。如果一條數據在多個位置呈現,很容易忘記更新顯示它的某個視圖。此外,當兩項更新以出人意料的方式發生衝突時,也很容易造成異常狀態。例如,某項更新可能會嘗試設置剛剛從界面中移除的節點的值。一般來說,軟件維護的複雜性會隨着需要更新的視圖數量而增長。

前面提到,Jetpack Compose 是聲明式 UI 框架,對於聲明式 UI 框架,我們只需要在描述一個頁面的時候附帶上各個控件的狀態,然後當有任何狀態發生改變時,頁面會自動進行刷新。初次瞭解聲明式 UI 的人可能會有疑問,這樣爲了更新頁面中的某個控件的,去刷新整個頁面,那不是很低效,頁面複雜起來了,不是要卡爆? 事實上,所有的聲明式 UI 框架都採用了相似的優化策略,再刷新頁面時,只會更新哪些狀態有變化的控件,而那些狀態沒有變化的控件會跳過執行。在 Jetpack Compose 中狀態改變,頁面進行刷新,稱之爲重組,狀態沒有變化的控件則會跳過重組,關於重組和跳過重組,會在後面的文章中詳細講述。

下面舉個實際的例子:
考慮一個帶有未讀郵件圖標的電子郵件應用程序。如果沒有消息,應用程序將呈現一個空白信封。如果有一些消息,我們在信封中渲染一些紙,如果有100條消息,我們將圖標渲染爲着火的樣子。。

對於命令式接口,我們可能需要編寫這樣的更新計數函數:

fun updateCount(count: Int) {
  if (count > 0 && !hasBadge()) {
    addBadge()
  } else if (count == 0 && hasBadge()) {
    removeBadge()
  }
  if (count > 99 && !hasFire()) {
    addFire()
    setBadgeText("99+")
  } else if (count <= 99 && hasFire()) {
    removeFire()
  }
  if (count > 0 && !hasPaper()) {
   addPaper()
  } else if (count == 0 && hasPaper()) {
   removePaper()
  }
  if (count <= 99) {
    setBadgeText("$count")
  }
}

在這段代碼中,我們收到了新的計數,必須弄清楚如何更新當前 UI 以反映該狀態。這裏有很多邊界情況,儘管這是一個相對簡單的例子,但這種邏輯並不容易。
那在聲明式接口中,如何編寫呢?

@Composable
fun BadgedEnvelope(count: Int) {
  Envelope(fire=count > 99, paper=count > 0) {
    if (count > 0) {
      Badge(text="$count")
    }
  }
}

在這裏我們說:
如果計數超過99,顯示火力。
如果計數超過0,顯示紙張,
如果計數超過0,則渲染計數徽章。

這就是聲明性API的含義。我們編寫的代碼描述了我們想要的 UI,但沒有描述如何轉換到那種狀態。這裏的關鍵是,在編寫這樣的聲明性代碼時,您不再需要擔心UI的上一個狀態,只需要指定當前狀態。框架控制如何從一個狀態轉換到另一個狀態。因此,我們不再需要考慮它。

其實,在 Jetpack Compose 之前,Google 推出的 Data Binding 框架也屬於聲明式編程範式。

小結:命令式關注過程,聲明式關注狀態。

二、Google 爲什麼力推 Jetpack Compose

View 視圖體系自 Android 誕生起,一路發展至今,爲何 Google 還要推出 Jetpack Compose 呢?

2.1 開發效率更高

傳統的 Android UI 開發採用的是 View 視圖體系,它是一種命令式的編程模型。開發者需要通過編寫大量的代碼來描述 View 的外觀和行爲,包括佈局、樣式、交互等。這種方式需要開發者花費大量的時間和精力,而且代碼量也非常龐大,維護和擴展也比較困難。

與之相比,Jetpack Compose 是一種聲明式的編程模型,採用的是函數式編程的思想。開發者只需要描述 UI 的樣式和行爲,然後 Jetpack Compose 會自動處理 View 的佈局和繪製。這種方式不僅減少了代碼量,還可以提高開發效率,同時也使得代碼更加易於維護和擴展。

那 Data Binding 呢?用過的人自然能明白其中的痛苦,在 xml 文件中編寫代碼。並且還需要先解析 xml 文件,效率自然會有影響。而 Jetpack Compose 代碼完全基於 Kotlin DSL 實現,相較於 Data Binding 需要有 xml 的依賴,Compose 只依賴單一語言,避免了因爲語言間交互而帶來的性能開銷及安全問題。

2.2 組合優於繼承

組合優於繼承,是面向對象設計模式中反覆強調的原則。而現實是,繼承用起來很方便,很多時候,大家很難從組合的視角思考問題。然而在複雜的體系中,繼承關係往往很難梳理清楚。
傳統的 View 視圖是基於繼承的結構體系,也已經隨着 Android 發展了十多年。View.java 本身也變得十分臃腫,目前已經超過三萬行。臃腫的父類控件,也會造成子類視圖的功能不合理。以 Button 爲例,爲了能讓 Button 具備顯示文字的功能,Button 被設計成了繼承自 TextView 的子類。這樣有很多不適用於按鈕的功能也被繼承下來了,TextView 的剪貼板功能,對 Button 來說顯得不合理。而且隨着 TextView 自身功能的迭代,Button 可能引入更多不必要的功能。
另一方面,想 Button 這類基礎控件,只能跟隨系統升級而更新,及使發現了問題,也得不到及時的修復。這也是爲什麼如今很多新的視圖都以 Jetpack 擴展庫的形式單獨發佈的原因。
而 Jetpack Compose 則是作爲函數,相互之間沒有繼承關係。讓開發這更多的是以組合的思想去思考問題。

那在 Compose 中 Button 如何展示呢?

Button(
    onClick = { /* 處理點擊事件 */ },
    modifier = Modifier.wrapContentSize()
) {
    Text("I'm a button")
}

作爲按鈕,它本身的職責是提供點擊事件,至於他內部的樣式,則需要和對應的其它可組合函數(暫且稱之爲控件)進行組合。

三、爲什麼要學習 Jetpack Compose

這裏,我不再提 Jetpack Compose 相對於 View 視圖體系的優勢。而是想從行業背景,領域發展角度述說。

3.1 聲明式 UI 的編程範式發展的趨勢

從變成範式的維度來說,聲明式 UI 相對傳統的命令式 UI,更加先進, React, iOS 的 SwiftUI, flutter, Jetpack Compose, 聲明式 UI 開發思想已經席捲了整個前端開發領域。如果你還在使用傳統 View 視圖體系開發 Android 原生界面,那你應該嘗試聲明式 UI。

3.2 Google 確定的 Android 官方推薦的 UI 框架

如同 Kotlin 取代 Java 成爲 Google 推薦的 Android 官方推薦的編程語言一樣,Jetpack Compose 也是 Google 推薦的 Android 官方 UI 框架。並且,新版的 Android Studio 中集成了大量的特性來支持 Jetpack Compose 更高效的開發。這表明 Jetpack Compose 在未來的 Android 開發中將扮演重要的角色。目前來看,Jetpack Compose 並不會完全取代傳統的 View 視圖體系,而是會和傳統 View 視圖體系共存, 但是 Jetpack Compose 在未來會能成爲 Android 開發的主流。從 Android 14 Framework 中我們能看到 Compose 的影子了。在 2018 年,SystemUI 模塊中,我們能看到 Kotlin 改寫的部分模塊,在 Android 14 中,我們再 Settings 中再次看到了 Kotlin 的代碼了。並且 Google 新建了一個 spa 包,這個包裏面嘗試把 “應用管理”、“應用管理通知”、“使用情況” 頁面用 Jetpack Compose 重寫了。

除此之外,還有 3 個主頁面也進行了重寫,

出於穩健考慮,Google 沒有默認使能這些頁面。如果你想提前喫螃蟹,可以使用以下命令來開啓: adb shell settings put global settings_enable_spa true 通過搜索代碼,也可以輕鬆找到這個開關在哪裏被使用:

我們可以去到相關頁面看一下前後對比:

由於Jetpack Compose的各個組件一直都在很好地踐行Material Design 3,可以看到後者的視覺還原要更好,也不會出現前者那樣MenuItem背景顏色不正確的問題。

至此,可能連 framework 開發人員都要開始學習 Jetpack Compose 了,應用開發還有什麼理由不學習呢?

四、Jetpack Compose 與 View 視圖體系對比

Compose 重新定義了 Android UI 開發方式,大幅提升了開發效率,主要體現在:

  1. 聲明式 UI, 不需要手動刷新數據
  2. 取消 XML, 完全解除了混合寫法的(XML + java、Kotlin) 的侷限性,以及性能開銷
  3. 超強兼容性,大多數 jetpack 庫(如 Navigation、ViewModel) 以及 Kotlin 協程都適用於 Compose, Compose 能夠與現有 View 體系並存,你可以爲一個既有項目引入 Compose。
  4. 加速開發,更簡單的動畫和觸摸事件Api, Compose 爲我們提供了很多開箱即用的 Material 組件,如果的APP是使用的material設計的話,那麼使用Jetpack Compose 能讓你節省不少精力。
  5. 精簡代碼數量,減少 bug 的出現
  6. 強大的預覽功能, Compose 預覽機制可以實時預覽動畫,交互式預覽,真正所見即所得。

4.1 Jetpack Compose 使用前後對比

官網示例: Tivi Jetpack Compose 使用前後對比

4.1.1 APK 尺寸縮減


△ 展示 Tivi APK 大小的圖表


△ 展示 Tivi 方法數的圖表

在將遷移後的應用與接入 Compose 前的應用做比較後,我們發現 APK 大小縮減了 41%,方法數減少了 17%。

4.1.2 代碼行數

儘管比較軟件項目時,計算源代碼行數不是特別有用的統計方式;但這種方式能夠提供一個視角,幫助我們瞭解事物是如何變化的。
使用了 cloc 工具。使用下面的命令可以排除各種構建文件、自動生成文件以及配置文件。

cloc . --exclude-dir=build,.idea,schemas


△ 展示 Tivi 源代碼行數的圖表
毫不意外的,XML 行數大幅減少了 76%。再見了,佈局文件,以及 styles、theme 等其他的 XML 文件 。

有趣的是,Kotlin 代碼的總行數也下降了。我對此現象的理解是,現在應用中的模板代碼減少了,同時我們也得以移除大量的視圖輔助類和工具類代碼。您可以看到,我在 這個 PR 中刪除了多年來編寫的近 3,000 行代碼。

4.1.3 構建速度


△ 展示 Tivi 構建時間中位數的圖表
考慮到 Kotlin 編譯器與 Compose 編譯器插件爲我們所做的事情,如位置記憶化、細粒度重組等工作,構建時間能夠 減少 29%, 可以說十分驚人。

寫在最後

Jetpack Compose 雖然是 Google 推出的 UI 框架,但它也不僅僅是 Android UI 框架。Compose 從設計的角度可以分爲四層, 每一層都可以被單獨使用。

  • Material 位於最上層,基於 Material Design 系統實現了各種 Composable,同時提供了基於 Material Design 的主題、圖標等。
  • Foundation 模塊提供了一些基礎的 Composable, 例如 Row、Column、LazColumn 等佈局類 UI,以及特定的手勢識別等,這些基礎 Composable 可以在不同的平臺通用。
  • UI 層的功能衆多,包含多個模塊,是上層 Composable 運行的基礎,例如 Composable 的測量、佈局、繪製、時間處理以及 Modifier 管理等。
  • Runtime 層實現了基本的 UI 樹的管理能力,支撐了 Compose 通過 UI 樹的 diff 驅動界面刷新的功能。如果只需要 Compose 樹的管理功能,不需要 UI, 則可以直接基於此層進行構建。

其中 Runtime 層是核心,上層 UI 只是其應用場景之一。

目前 Jetbrains 公司已經基於 Jetpack Compose,啓動了 Compose Multiplatform 項目,未來,Compose 可能是跨平臺方案的選擇之一。作爲 Jetpack Compose 系列開篇文章,本文只是拋磚引玉。接下來如果有興趣繼續學習下去,可是要下一番大功夫了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章