MAD,現代安卓開發技術:Android 領域開發方式的重大變革!

語言: CN / TW / HK

theme: Chinese-red

「2022 年什麼會火?什麼該學?本文正在參與“聊聊 2022 技術趨勢”徵文活動 」

Android 誕生已久,其開發方式保持着高頻更迭,相較於早期的開發方式已大不相同,尤其是近幾年 Google 熱切推崇的 MAD 開發技術。其實很多開發者已經有意或無意地正在使用這門技術,藉着 2022 開年探討技術趨勢的契機,想要完整地總結 MAD 的願景、構成、優勢以及一些學習建議。

MAD,全稱 Modern Android Development:是 Google 針對 Android 平台提出的全新開發技術。旨在指導我們利用官方推出的各項技術來進行高效的 App 開發。有的時候 Google 會將其翻譯成現代安卓開發,有的時候又翻譯成新式安卓開發,個人覺得前者的翻譯雖然激進、倒也貼切。

下面按照 MAD 的構成要點逐步展開,幫助大家快速瞭解 MAD 的技術理念。如果大家對其中的語言、工具包或框架產生了興趣,一定要在日後的開發中嘗試和掌握。

內容前瞻

  1. 【Modern Android Development】講述 Android 全新開發技術的由來和構成
  2. 【Android Studio】演示 Android 官方 IDE 的重要特性
  3. 【Android App Bundle】簡要普及 Google 推崇的 App 新格式
  4. Kotlin】解讀 Android 首推的開發語言的優點
  5. Jetpack】講述 Android 持續更新的重大框架集合,並逐個演示重要框架解決的問題和優勢
  6. Jetpack Compose】帶領大家感受 Android 上 UI 開發方式的重大變革

1.Modern Android Development

官方一直在優化 App 的開發體驗:從 IDE 到語言再到框架,這些新技術愈發完善也愈發瑣碎。提出一個全新的概念來整合這些鬆散的技術方便介紹和推廣,也方便開發者們理解。

MAD 便是提出的全新理念,期望在語言、工具、框架等多個層面提供卓越的開發體驗,其願景和優勢:

  • 傾力打造:匯聚 Google 在 Android 行業十餘年的前言開發經驗
  • 入門簡單:提供大量 Demo 和詳盡文檔,適用於各階段各規模的項目
  • 迅速起步:提供顯著降低樣板代碼的開發框架 Jetpack 和 UI 工具包 Jetpack Compose
  • 自由選擇:框架豐富多樣,可與傳統語言、原生開發、開源框架自由搭配
  • 統合一致:兼容不同設備的開發框架達到的一致性開發體驗

其涵蓋的內容:

  • Android Studio :持續改進的官方 IDE
  • Android App Bundle :先進的應用打包和分發方式
  • Kotlin :首推的編程語言
  • Jetpack :獨立於 AOSP 以外,彙集了大量開發框架的開發套件
  • Jetpack Compose:Android 平台重大變革的 UI 工具包

同時,官方針對 MAD 技術提供了認證考試和技能的計分插件,大家在實踐一段時間之後可以體驗一下: * MAD 資格認證 * Android Studio 的 MAD Skills 計分插件

2.Android Studio

Android Studio 剛推出的初期飽受批評,吃內存、Bug 多、不好用,開發者一度對 Eclipse 戀戀不捨。隨着 Google 和開發者的不斷協力,AS 愈加穩定、功能愈加強大,大家可以活用 AS 的諸多特性以提高開發效率。和 Chrome 一樣,針對不同需求,AS 提供了三個版本供開發者靈活選擇。

| 版本 | 説明 | | --------------------- | ------------------------------------------------------------ | | Stable Release | 穩定發行版,最新版為 Arctic Fox|2020.3.1 | | Release candidate | 即將發佈的下一代版本,可以提前體驗新特性和優化,最新版為 Bunblebee|2021.1.1 | | Canary | 試驗版本,不穩定但可以試用領先的實驗功能,最新版為 Chipmunk|2021.2.1 |

接下來介紹 AS 其中幾個好用的特性。

2.1 Database Inspector

Database Inspector 可以實時查看 Jetpack Room 框架生成的數據庫文件,同時也支持實時編輯和部署到設備當中。相較之前需要的 SQLite 命令或者額外導出並藉助 DB 工具的方式更為高效和直觀。

2.2 Layout / Motion Editor

Layout Editor 擁有諸多優點,不知大家熟練運用了沒有:

  • 可以直觀地編輯 UI:隨意拖動視圖控件和更改約束指向
  • 在不同配置(設備、主題、語言、屏幕方向等)下靈活切換預覽,免去實機調試
  • 搭配 Tools 標籤自由定製 UI,確保只面向調試而不影響實際邏輯。比如:佈局中有上下兩個控件,上面的默認為 invisible,想確認下上面的控件如果可見的話對整體佈局的影響。無需更改控件的 visibility 屬性,添加 Tools:visibility=true 即可預覽佈局的變化

Motion Editor 則是支持 MotionLayout 類型佈局的視覺設計編輯器,可讓更輕鬆地創建和預覽和調試動畫。

Layout Inspector 則可以查看某進程某畫面的詳細布局,完整展示 View 樹的各項屬性。在不方便代碼調試或剖析其他 App 的情況下非常好用。同時已經支持直接檢查 Compose 編寫的 UI 佈局了,喜極而泣。

2.3 Realtime Profilers

AS 的 Realtime Profilers 工具可以幫助我們在如下四個方面監測和發現問題,有的時候在沒有其他 App 代碼的情況下通過 Memory Profilers 還可以查看其內部的實例和變量細節。

  • CPU:性能剖析器檢查 CPU 活動,切換到 Frames 視圖還可以界面卡頓追蹤
  • Memory:識別可能會導致應用卡頓、凍結甚至崩潰的內存泄漏和內存抖動,可以捕獲堆轉儲、強制執行垃圾回收以及跟蹤內存分配以定位內存方面的問題
  • Battery:會監控 CPU、網絡無線裝置和 GPS 傳感器的使用情況,並直觀地顯示其中每個組件消耗的電量,瞭解應用在哪裏耗用了不必要的電量
  • Network:顯示實時網絡活動,包括髮送和接收的數據以及當前的連接數。這便於您檢查應用傳輸數據的方式和時間,並適當優化代碼

2.4 APK Analyzer

Apk 的下載會耗費網絡流量,安裝了還會佔用存儲空間。其體積的大小會對 App 安裝和留存產生影響,分析和優化其體積顯得尤為必要。

藉助 AS 的 APK Analyzer 可以幫助完成如下幾項工作:

  • 快速分析 Apk 構成,包括 DEX、Resources 和 Manifest 的 Size 和佔比,助力我們優化代碼或資源的方向
  • Diff Apk 以瞭解版本的前後差異,精準定位體積變大的源頭
  • 分析其他 Apk,包括查看大致的資源和分析代碼邏輯,進而拆解、Bug 定位

2.5 其他特性

篇幅原因只介紹了少部分特性,其他的還有很多,需要各位自行探索:

  • 性能提升、內嵌到 AS 界面內的的 Fast Emulator
  • 實時預覽和編輯 Compose 佈局,並支持直接交互的 Compose Preview
  • 針對 Jetpack WorkManagerBackground Task Inspector
  • 。。。

相比之下,Google 官方的這篇「Android Studio 新特性詳解」介紹得更新、更全,大家可以一看。

3.Android App Bundle

android app bundle 是一種發佈格式,其中包含您應用的所有經過編譯的代碼和資源,它會將 APK 生成及簽名交由 Google Play 來完成。

這個新格式對面向海外市場的 3rd Party App 影響較大,對面向國內市場的 App 影響不大。但作為未來的構建格式,瞭解和適配是遲早的事。

  • 其針對目標設備優化 Apk 的構建,比如只預設對應架構的 so文件、圖片和語言資源。得以壓縮體積,進而提升安裝成功率並減少卸載量
  • 支持便捷創建 Instant App,可以免安裝、直接啟動、體驗試用
  • 滿足模塊化應用開發,提升大型項目的編譯速度和開發效率

Google 對 .aab 格式非常重視,也極力推廣:從去年也就是 2021 年 8 月起,規定新的 App 必須採用該格式才能在 Google Play 上架。

fun 神的「AAB 扶正!APK 將退出歷史舞台」文章針對 AAB 技術有完整的説明,可以進一步瞭解。

4.Kotlin

A modern programming language that makes developers happier.

Kotlin是 大名鼎鼎的 JetBrains 公司於 2011 年開發的面向 JVM 的新語言,對於 Android 開發者來説,選擇 Kotlin 開發 App 有如下理由:

  • Google IO 2019 宣佈 Kotlin 成為了官方認定的 Android 平台首選編程語言,這意味着會得到 Google 巨佬在 Android 端的鼎力支持以實現超越 Java 的優秀編程體驗
  • 通過 KMM(Kotlin Multiplatform Mobile)實現跨移動端的支持
  • Server-side,天然支持後端開發
  • 通過 Kotlin/JS 編譯成 JavaScript,支持前端開發
  • 和 Java 幾乎同等的編譯速度,增量編譯下性能甚至超越 Java

4.1 Kotlin 在 Android上優秀的編程體驗

  • Kotlin 代碼簡潔、可讀性高:縮減了大量樣板代碼,以縮短編寫和閲讀代碼的時間

  • 可與 Java 互相調用,靈活搭配

  • 容易上手,尤其是熟悉 Java 的 Android 開發者

  • 代碼安全,編譯器嚴格檢查代碼錯誤

  • 專屬的協程機制,大大簡化異步編程

  • 提供了大量 Android 專屬的 KTX 擴展

  • 唯一支持 Android 全新 UI 編程方式 Compose 的開發語言

很多知名 App 都已經採用 Kotlin 進行開發,比如 Evernote、Twiiter、Pocket、WeChat 等。

下面我們選取 Kotlin 的幾個典型特性,結合代碼簡單介紹下其優勢。

4.2 簡化函數聲明

Kotlin 語法的簡潔體現在很多地方,就比如函數聲明的簡化。

如下是一個包含條件語句的 Java 函數的寫法:

java String generateAnswerString(int count, int countThreshold) { if (count > countThreshold) { return "I have the answer."; } else { return "The answer eludes me."; } } Java 支持三元運算符可以進一步簡化。 java String generateAnswerString(int count, int countThreshold) { return count > countThreshold ? "I have the answer." : "The answer eludes me."; }

Kotlin 的語法並不支持三元運算符,但可以做到同等的簡化效果:

kotlin fun generateAnswerString(count: Int, countThreshold: Int): String { return if (count > countThreshold) "I have the answer." else "The answer eludes me." } 它同時還可以省略大括號和 return 關鍵字,採用賦值形式進一步簡化。這樣子的寫法已經很接近於語言的日常表達,高級~ kotlin fun generateAnswerString(count: Int, countThreshold: Int): String = if (count > countThreshold) "I have the answer." else "The answer eludes me." 反編譯 Class 之後發現其實際上仍採用的三元運算符的寫法,這種語法糖會體現在 Kotlin 的很多地方😅。 kotlin public final String generateAnswerString2(int count, int countThreshold) { return count > countThreshold ? "I have the answer." : "The answer eludes me."; }

4.3 高階函數

介紹高階函數之前,我們先看一個向函數內傳入回調接口的例子。

一般來説,需要先定義一個回調接口,調用函數傳入接口實現的實例,函數進行一些處理之後執行回調,藉助Lambda 表達式可以對接口的實現進行簡化。

```java interface Mapper { int map(String input); }

class Temp { void main() { stringMapper("Android", input -> input.length() + 2); }

int stringMapper(String input, Mapper mapper) {
    // Do something
    ...
    return mapper.map(input);
}

} ``` Kotlin 則無需定義接口,直接將匿名回調函數作為參數傳入即可。(匿名函數是最後一個參數的話,方法體可單獨拎出,增加可讀性)

這種接受函數作為參數或返回值的函數稱之為高階函數,非常方便。 ```kotlin class Temp { fun main() { stringMapper("Android") {input -> input.length + 2} }

fun stringMapper(input: String, mapper: (String) -> Int): Int {
    // Do something
    ...
    return mapper(input)
}

} ``` 事實上這也是語法糖,編譯器會預設默認接口來幫忙實現高階函數。

4.4 Null 安全

可以説 Null 安全是 Kotlin 語言的一大特色。試想一下 Java 傳統的 Null 處理無非是在調用之前加上空判斷或衞語句,這種寫法既繁瑣,更容易遺漏。

```java void function(Bean bean) { // Null check if (bean != null) { bean.doSometh(); }

// 或者衞語句
if (bean == null) {
    return;
}
bean.doSometh();

} `` 而 Kotlin 要求變量在定義的時候需要聲明是否可為空:帶上?` 即表示可能為空,反之不為空。作為參數傳遞給函數的話也要保持是否為空的類型一致,否則無法通過編譯。

比如下面的 functionA() 調用 functionB() 將導致編譯失敗,但 functionB() 的參數在聲明的時候沒有添加 ? 即為非空類型,那麼函數內可直接使用該參數,沒有 NPE 的風險。

```kotlin fun functionA() { var bean: Bean? = null functionB(bean) }

fun functionB(bean: Bean) { bean.doSometh() } ``` 為了通過編譯,可以將變量 bean 聲明中的 ? 去掉, 並賦上正常的值。

但很多時候變量的值是不可控的,我們無法保證它不為空。那麼為了通過編譯,還可以選擇將參數 bean 添加上 ? 的聲明。這個時候函數內不就不可直接使用該參數了,需要做明確的 Null 處理,比如:

  • 在使用之前也加上 ? 的限定,表示該參數不為空的情況下才觸發調用
  • 在使用之前加上 !! 的限定也可以,但表示無論參數是否為空的情況下都觸發調用,這種強制的調用即會吿知開發者此處有 NPE 的風險

```kotlin fun functionB(bean: Bean?) { // bean.doSometh() // 仍然直接調用將導致編譯失敗

    // 不為空才調用
    bean?.doSometh()

    // 或強制調用,開發者已知 NPE 風險
    bean!!.doSometh()
}

```

總結起來將很好理解: * 參數為非空類型,傳遞的實例也必須不為空 * 參數為可空類型,內部的調用必須明確地 Null 處理

反編譯一段 Null 處理後可以看到,非空類型本質上是利用 @NotNull 的註解,可空類型調用前的 ? 則是手動的 null 判斷。 ```java public final int stringMapper(@NotNull String str, @NotNull Function1 mapper) { ... return ((Number)mapper.invoke(str)).intValue(); }

private final void function(String bean) { if (bean != null) { boolean var3 = false; Double.parseDouble(bean); } } ```

4.5 協程 Coroutines

介紹 Coroutines 之前,先來回顧下 Java 或 Android 如何進行線程間通信?有何痛點?

比如:AsyncTaskHandlerHandlerThreadIntentServiceRxJavaLiveData 等。它們都有複雜易錯、不簡潔、回調宂餘的痛點。

比如一個請求網絡登錄的簡單場景:我們需要新建線程去請求,然後將結果通過 Handler 或 RxJava 回傳給主線程,其中的登錄請求必須明確寫在非 UI 線程中。

```java void login(String username, String token) { String jsonBody = "{ username: \"$username\", token: \"$token\"}"; Executors.newSingleThreadExecutor().execute(() -> { Result result; try { result = makeLoginRequest(jsonBody); } catch (IOException e) { result = new Result(e); } Result finalResult = result; new Handler(Looper.getMainLooper()).post(() -> updateUI(finalResult)); }); }

Result makeLoginRequest(String jsonBody) throws IOException { URL url = new URL("http://example.com/login"); HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(); httpURLConnection.setRequestMethod("POST"); ... httpURLConnection.connect();

int code = httpURLConnection.getResponseCode();
if (code == 200) {
    // Handle input stream ...
    return new Result(bean);
} else {
    return new Result(code);
}

} ```

Kotlin 的 Coroutines 則是以順序的編碼方式實現異步操作、同時不阻塞調用線程的簡化併發處理的設計模式。

其具備如下的異步編程優勢:

  • 掛起線程不阻塞原線程
  • 支持取消
  • 通過 KTX 擴展對 Jetpack 組件更好支持

採用協程實現異步處理的將變得清晰、簡潔,同時因為指定耗時邏輯運行在工作線程的緣故,無需管理線程切換可直接更新 UI。

```kotlin fun login(username: String, token: String) { val jsonBody = "{ username: \"\$username\", token: \"\$token\"}" GlobalScope.launch(Dispatchers.Main) { val result = try { makeLoginRequest(jsonBody) } catch(e: Exception) { Result(e) } updateUI(result) } }

@Throws(IOException::class) suspend fun makeLoginRequest(jsonBody: String): Result { val url = URL("http://example.com/login") var result: Result withContext(Dispatchers.IO) { val httpURLConnection = url.openConnection() as HttpURLConnection httpURLConnection.run { requestMethod = "POST" ... } httpURLConnection.connect() val code = httpURLConnection.responseCode result = if (code == 200) { Result(bean) } else { Result(code) } } return result } ```

4.6 KTX

KTX 是專門為 Android 庫設計的 Kotlin 擴展程序,以提供簡潔易用的 Kotlin 代碼。

比如使用 SharedPreferences 寫入數據的話,我們會這麼編碼:

java void updatePref(SharedPreferences sharedPreferences, boolean value) { sharedPreferences .edit() .putBoolean("key", value) .apply(); }

引入 KTX 擴展函數之後將變得更加簡潔。

kotlin fun updatePref(sharedPreferences: SharedPreferences, value: Boolean) { sharedPreferences.edit { putBoolean("key", value) }

這只是 KTX 擴展的冰山一角,還有大量好用的擴展以及 Kotlin 的優勢值得大家學習和實踐,比如:

  • 大大簡潔語法的 let, also 等擴展函數
  • 節省內存開銷的 inline 函數
  • 靈活豐富的 DSL 特性
  • 異步獲取數據的 Flow

5.Jetpack

Jetpack 單詞的本意是火箭人,框架的 Logo 也可以看出來是個綁着火箭的 Android。Google 用它命名,含義非常明顯,希望這些框架能夠成為 Android 開發的助推器:助力 App 開發,體驗飛速提升。

Jetpack 分為架構、UI、基礎功能和特定功能等幾個方面,其中架構板塊是全新設計的,涵蓋了 Google 花費大量精力開發的系列框架,是本章節着力講解的方面。

架構以外的部分實際上是 AOSP 本身的一些組件進行優化之後集成到了Jetpack 體系內而已,這裏不再提及。

  • 架構:全新設計,框架的核心
  • 以外:AOSP 本身組件的重新設計
  • UI
  • 基礎功能
  • 特定功能

Jetpack 具備如下的優勢供我們在實現某塊功能的時候收腰選擇:

  • 提供 Android 平台的最佳實踐
  • 消除樣板代碼
  • 不同版本、廠商上達到設備一致性的框架表現
  • Google 官方穩定的指導、維護和持續升級

如果對 Jetpack 的背景由來感興趣的朋友可以看我之前寫的一篇文章:「從Preference組件的更迭看Jetpack的前世今生」。下面,我們選取 Jetpack 中幾個典型的框架來了解和學習下它具體的優勢。

5.1 View Binding

通常的話綁定佈局裏的 View 實例有哪些辦法?又有哪些缺點?

| 通常做法 | 缺點 | | ---------------- | ------------------------------------------------------------ | | findViewById() | NPE 風險、大量的綁定代碼、類型轉換危險 | | @ButterKnife | NPE 風險、額外的註解代碼、不適用於多模塊項目(APT 工具解析 Library 受限) | | KAE 插件 | NPE 風險、操作其他佈局的風險、Kotlin 語言獨佔、已經廢棄 |

AS 現在默認採用 ViewBinding 框架幫我們綁定 View。

來簡單瞭解一下它的用法:

```xml

```

ViewBinding 框架初始化之後,無需額外的綁定處理,即可直接操作 View 實例。

```kotlin class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle) { super.onCreate(savedInstanceState) val binding = ResultProfileBinding.inflate(layoutInflater)

    setContentView(binding.root)
    binding.name.text = "Hello world"
}

} ``` 原理比較簡單:編譯器將生成佈局同名的綁定類文件,然後在初始化的時候將佈局裏的 Root View 和其他預設了 ID 的 View 實例緩存起來。事實上無論是上面的註解,插件還是這個框架,其本質上都是通過 findViewById 實現的 View 綁定,只是進行了封裝。

ViewBinding 框架能改善通常做法的缺陷,但也並非完美。特殊情況下仍需使用通常做法,比如操作佈局以外的系統 View 實例 ContentView,ActionBar 等。

| 優勢 | 侷限 | | ------------------------------------------------------------ | --------------------------------------------------------- | | Null 安全:預設 ID 的 View 才會被緩存,否則無法通過 ViewBinding 使用,在編譯階段就阻止了 NPE 的可能 | 綁定佈局以外的 View 仍需藉助 findViewById | | 類型安全:ViewBinding 緩存 View 實例的時候已經處理了匹配的類型 | 依賴配置採用不同佈局仍需處理 Null(比如橫豎屏的佈局不同) | | 代碼簡潔:無需綁定的樣板代碼 | | | 佈局專屬:不混亂、佈局文件為單位的專屬類 | |

5.2 Data Binding

一般來説,將數據反映到 UI 上需要經過如下步驟: 1. 創建 UI 佈局 2. 綁定佈局中 View 實例 3. 數據逐一更新到 View 的對應屬性

DataBinding 框架可以免去上面的步驟 2 和 3。它需要我們在步驟 1 的佈局當中就聲明好數據和 UI 的關係,比如文本內容的數據來源、是否可見的邏輯條件等。 ```xml

<LinearLayout ...>
    <TextView
        ...
        android:text="@{viewModel.userName}"
        android:visibility="@{viewModel.age >= 18 ? View.VISIBLE : View.GONE}"/>
</LinearLayout>

```

上述 DataBinding 佈局展示的是當 ViewModel 的 age 屬性大於 18 歲才顯示文本,而文本內容來自於 ViewModel 的 userName 屬性。

kotlin val binding = ResultProfileBinding.inflate(layoutInflater) binding.viewModel = viewModel

Activity 中無需綁定和手動更新 View,像 ViewBinding 一樣初始化之後指定數據來源即可,後續的 UI 展示和刷新將被自動觸發。DataBinding 還有諸多妙用,大家可自行了解。

5.3 Lifecycle

監聽 Activity 的生命週期並作出相應處理是 App 開發的重中之重,通常有如下兩種思路。

| 通常思路 | 具體 | 缺點 | | -------- | ----------------------------------------------------- | ----------------------------------------------------- | | 基礎 | 直接覆寫 Activity 對應的生命週期函數 | 繁瑣、高耦合 | | 進階 | 利用 Application#registerLifecycleCallback 統一管理 | 回調固定、需要區分各 Activity、邏輯侵入到 Application |

Lifecycle 框架則可以高效管理生命週期。

使用 Lifecycle 框架需要先定義一個生命週期的觀察者 LifecycleObserver,給生命週期相關處理添加上 OnLifecycleEvent 註解,並指定對應的生命狀態。比如 onCreate 的時候執行初始化,onStart 的時候開始連接,onPause 的時候斷開連接。

```kotlin class MyLifecycleObserver( private val lifecycle: Lifecycle ) : LifecycleObserver { ... @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) fun init() { enabled = checkStatus() }

@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun start() {
    if (enabled) {
        connect()
    }
}

@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun stop() {
    if (connected) {
        disconnect()
    }
}

} ```

然後在對應的 Activity 裏添加觀察:

kotlin class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle) { ... MyLifecycleObserver(lifecycle).also { lifecycle.addObserver(it) } } }

Lifecycle 的簡單例子可以看出生命週期的管理變得很清晰,同時能和 Activity 的代碼解耦。

繼續看上面的小例子:假使初始化操作 init() 是異步耗時操作怎麼辦?

init 異步的話,onStart 狀態回調的時候 init 可能沒有執行完畢,這時候 start 的連接處理 connect 可能被跳過。這時候 Lifecycle 提供的 State 機制就可以派上用場了。

使用很簡單,在異步初始化回調的時候再次執行一下開始鏈接的處理,但需要加上 STARTED 的 State 條件。這樣既可以保證 onStart 時跳過連接之後能手動執行連接,還能保證只有在 Activity 處於 STARTED 及以後的狀態下才執行連接

```kotlin class MyLifecycleObserver(...) : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) fun init() { checkStatus { result -> if (result) { enable() } } }

fun enable() {
    enabled = true
    // 初始化完畢的時候確保只有在 STARTED 及以後的狀態下執行連接
    if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
        if (!connected) {
            connect()
        }
    }
}
...

} ```

5.4 Live Data

LiveData 是一種新型的可觀察的數據存儲框架,比如下面的使用示例,數據的封裝和發射非常便捷:

```kotlin class StockLiveData(symbol: String) : LiveData() { private val stockManager = StockManager(symbol)

private val listener = { price: BigDecimal ->
    // 將請求到的數據發射出去
    value = price
}

// 畫面活動狀態下才請求
override fun onActive() {
    stockManager.requestPriceUpdates(listener)
}

// 非活動狀態下移除請求
override fun onInactive() {
    stockManager.removeUpdates(listener)
}

}

class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { // 註冊觀察 StockLiveData("Tesla").run { observe([email protected], Observer { ... })} } } ```

支持異步傳遞數據以外,LiveData 還有很多優勢: * 與 Lifecycle 框架深度綁定 * 具有生命週期感知能力,數據不會發射給非活動狀態的觀察者 * 觀察者銷燬了自動釋放數據,避免內存泄露 * 支持 RoomRetrofit 框架 * 支持合併多個數據源統一觀察的 MediatorLiveData(省去多個 LiveData 多次 observe 的醜陋處理))

但必須要説 LiveData 的定位和使用有這樣那樣的問題,官方的態度也一直在變,瞭解之後多使用 Flow 來完成異步的數據提供。

5.5 Room

Android 上開發數據庫有哪些痛點? * 需要實現 SQLite 相關的 Helper 實例並實裝初始化和 CRUD 等命令 * 自行處理異步操作 * Cursor實例需要小心處理 * 字段對應關係 * index 對齊 * 關閉

官方推出的 Room 是在 SQLite 上提供了一個抽象層,通過註解簡化數據庫的開發。以便在充分利用 SQLite 的強大功能的同時,能夠高效地訪問數據庫。

需要定義 Entity,Dao 以及 Database 三塊即可完成數據庫的配置,其他的數據庫實現交由框架即可。

```kotlin @Entity class Movie() : BaseObservable() { @PrimaryKey(autoGenerate = true) var id = 0

@ColumnInfo(name = "movie_name", defaultValue = "Harry Potter")
lateinit var name: String
...

} ```

```kotlin @Dao interface MovieDao { @Insert fun insert(vararg movies: Movie?): LongArray?

@Delete
fun delete(movie: Movie?): Int

@Update
fun update(vararg movies: Movie?): Int

@get:Query("SELECT * FROM movie")
val allMovies: LiveData<List<Movie?>?>

} kotlin @Database(entities = [Movie::class], version = 1) abstract class MovieDataBase : RoomDatabase() { abstract fun movieDao(): MovieDao

companion object {
    @Volatile
    private var sInstance: MovieDataBase? = null
    private const val DATA_BASE_NAME = "jetpack_movie.db"

    @JvmStatic
    fun getInstance(context: Context): MovieDataBase? {
        if (sInstance == null) {
            synchronized(MovieDataBase::class.java) {
                if (sInstance == null) {
                    sInstance = createInstance(context)
                }
            }
        }
        return sInstance
    }

    private fun createInstance(context: Context): MovieDataBase {
        return Room.databaseBuilder(context.applicationContext,
                MovieDataBase::class.java, DATA_BASE_NAME).build()
    }
}

} 在 ViewModel 初始化 DataBase 接口之後即可利用其提供的 DAO 接口執行操作,接着利用 LiveData 將數據發射到 UI。kotlin class MovieViewModel(application: Application) : AndroidViewModel(application) { private val mediatorLiveData = MediatorLiveData?>() private val db: MovieDataBase? init { db = MovieDataBase.getInstance(application) if (db != null) { mediatorLiveData.addSource(db.movieDao().allMovies) { movieList -> if (db.databaseCreated.value != null) { mediatorLiveData.postValue(movieList) } } }; }

fun getMovieList(owner: LifecycleOwner?, observer: Observer<List<Movie?>?>?) {
    if (owner != null && observer != null)
        mediatorLiveData.observe(owner, observer)
}

} ```

Room 具備很多優勢值得選作數據庫的開發首選:

  • 簡潔高效,通過簡單註解即可完成數據庫的創建和 CRUD 封裝
  • 直接返回目標 POJO 實例,避免自行處理 Cursor 的風險
  • 支持事務處理、數據庫遷移、關係數據庫等完整功能
  • 支持 LiveData、Flow 等方式觀察式查詢
  • AS 的 Database Inspector 可以實時查看、編輯和部署 Room 的數據庫
  • 內置異步處理

5.6 View Model

ViewModel 框架和 AppCompat、Lifecycle 框架一樣,可謂是 Jetpack 框架最重要的幾個基礎框架。雖功能不僅限於此,但我們想要藉此探討一下它在數據緩存方面的作用。

通常怎麼處理橫豎屏切換導致的 Activity 重繪?一可以選擇自生自滅,只有部分 View 存在自行恢復的處理、也可以配置 ConfigurationChange 手動復原重要的狀態、或者保存數據至 BundleState,在 onCreate 等時機去手動恢復。

得益於 ViewModel 實例在 Activity 重繪之後不銷燬,其緩存的數據不受外部配置變化的影響,進而確保數據可以自動恢復數據,無需處理。

這裏定義一個 ViewModel,其中提供一個獲取數據的方法,用來返回一個 30 歲名叫 Ellison 的朋友。Activity 取得 vm 實例之後觀察數據的變化,並將數據反映到 UI 上。當屏幕方向變化後,名字和年齡的 TextView 可自動恢復,無需額外處理。

```kotlin class PersonContextModel(application: Application) : AndroidViewModel(application) { val personLiveData = MutableLiveData() val personInWork: Unit get() { val testPerson = Person(30, "Ellison") personLiveData.postValue(testPerson) } }

class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { ... val model = ViewModelProvider(this).get( PersonContextModel::class.java )

    model.personLiveData.observe(this, Observer { person: Person ->
        binding.name.setText(person.name)
        binding.age.setText(person.age.toString())
    })
    binding.get.setOnClickListener({ view -> model.personInWork })
}

} ```

ViewModel 的眾多優勢:

  • 基於 Lifecycle 實現以注重生命週期的方式存儲和管理界面相關的數據
  • 畫面銷燬前存儲 vm 實例並在重建後恢復,讓數據可在發生屏幕旋轉等配置更改後繼續留存
  • 可用於 Fragment 之間共享數據
  • 作為數據和 UI 交互的媒介,用作 MVVM 架構的 VM 層
  • 。。。

5.7 CameraX

完成一個相機預覽的功能,使用 Camera2 的話需要如下諸多流程,會比較繁瑣:

而採用 CameraX 進行開發的話,幾十行代碼即可完成預覽功能。

```java private void setupCamera(PreviewView previewView) { ListenableFuture cameraProviderFuture = ProcessCameraProvider.getInstance(this); cameraProviderFuture.addListener(() -> { try { mCameraProvider = cameraProviderFuture.get(); bindPreview(mCameraProvider, previewView); } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); } }, ContextCompat.getMainExecutor(this)); }

private void bindPreview(@NonNull ProcessCameraProvider cameraProvider,
                         PreviewView previewView) {
    mPreview = new Preview.Builder().build();
    mCamera = cameraProvider.bindToLifecycle(this,
            CameraSelector.DEFAULT_BACK_CAMERA, mPreview);
    mPreview.setSurfaceProvider(previewView.getSurfaceProvider());
}

```

上面是 CameraX 的架構,可以看到其底層仍然是 Camera2,外加高度封裝的接口,以及 Vendor 自定義的功能庫。

使用它來作為全新的相機使用框架,具備很多優勢:

  • 代碼簡單,易用
  • 自動綁定 Lifecycle,自動確定打開相機、何時創建拍攝會話以及何時停止和關閉
  • 多設備的相機開發體驗統一:國內外主流平台的設備都支持,國內的華米 OV 都在對這個框架支持和貢獻
  • 完美支持人像、HDR、夜間和美顏模式等拍攝模式的 Extensions
Monzo 利用 CameraX 縮減了 9,000 多行代碼並使註冊流程中的訪問者流失率降低了 5 倍

這是一家銀行服務公司並提供了同名應用,僅在移動設備上提供數字金融服務。他們的使命是向每個人傳授生財之道。為了完成新客户註冊,Monzo 應用會拍攝身份證明文件(例如護照、駕照或身份證)的圖片,並拍攝自拍視頻來證明身份證明文件屬於申請者。

早期版本使用的是 camera2 API。在某些設備上會隨機發生崩潰和異常行為,這導致 25% 的潛在客户無法繼續進行身份證明拍攝和自拍視頻步驟。

5.8 其他框架

篇幅有限,Jetpack 集合中還有非常多其他的優質框架等待大家的挖掘。

| 框架 | 作用 | 競品 | | -------------- | ------------------------------------------------------------ | ---------------------------- | | DataStore | 異步、一致性的輕量級數據的存儲框架,支持鍵值對和對象數據 | SharedPreferences、MMKV | | StartUp | 簡化應用啟動的組件初始化,提高應用啟動性能的框架 | - | | Navigation | 簡化畫面跳轉,支持標籤導航、抽屜導航等複雜設計的路由框架 | ARouter | | ActivityResult | Activity、Fragment 之間傳遞數據的新框架 | onActivityResult/Intent | | Paging3 | 按需加載節省網絡流量和內存消耗的分頁加載框架 | - | | WorkManager | 調度退出應用或重啟設備後仍可運行的可延期異步任務框架。 | JobService、Alarm、Broadcast | | Hilt | Android 專用的DI框架,快速建立之間的依賴關係和生命週期 | Dagger2、Koil | | AppCompat | 提供Activity、Dialog 和 View 的 Base 類,兼容 Jetpack 的大量處理 | - | | ViewPager2 | 實現經典的標籤導航設計的新框架 | ViewPager | | ... | | |

在開發某個功能的時候,看看是否有輪子可用,尤其是官方的。

5.9 官方推薦的應用架構

我在官方的推薦架構上做了些補充,一般的 App 推薦採用如下的架構組件。

  • 嘗試單 Activity 多 Fragment 的 UI 架構
  • 通過 Navigation 導航
  • ViewModel 完成數據和 UI 交互
  • LiveData 觀察數據
  • RoomDataStore 負責本地數據
  • Retrofit 負責網絡數據
  • 整體通過 Hilt 注入依賴

架構絕非固定模式,依實際需求和最佳實踐自由搭配~

6.Jetpack Compose

Jetpack Compose 是 Google 耗費五年傾力打造,用於構建 Android 原生界面的全新 UI 工具包。Android 誕生多年,UI 體系早已成熟,為什麼這麼要重造一個輪子?🤔

原因:

  • XML 佈局宂長、繁瑣:遇到複雜的佈局,把屏幕豎過來都看不全
  • View 編程方式的嵌套會帶來性能影響:不合理的佈局導致測量性能翻倍
  • 手動更新視圖複雜、易錯
  • 聲明性界面模型逐漸流行:這種方式可以簡化 UI 的構建和更新步驟,僅執行必要的更改

其發展歷程:

  1. 17 年立項
  2. 之後長達三年的內部調查和實驗
  3. 20 年初 dev 版公開,年中 alpha 版推出
  4. 21 年初 beta 版發佈
  5. 21 年 4 月全球挑戰推廣
  6. 21 年 7 月正式發佈

6.1 Compose 挑戰賽

去年上半年 Google 啟動了為期四周的全球 Compose 挑戰賽,提供了 500 多份樂高聯名積木,十幾部 Pixel 手機獎品,引發數萬計Android開發者嚐鮮,提交作品。

  • 第一週的挑戰做一個寵物領養 App,我花了一個週末做了個 LovePet 並拿到了這個飄洋過海的樂高積木,在推特上提交作品截圖之後還有好多老外點贊,是很不錯的體驗。
  • 後面的挑戰還有定時器 App,復刻 App 設計作品,發揮想象做個天氣 App 等

這些比賽內容其實涵蓋了 Compose 所需要用到的大部分技術。Google 的大力推廣也足見其決心和重視程度,日後必將成為Android平台上重要的UI編寫方式,早日上車!💪

6.2 編程思想

我們通過一個展示 “Hello World” 文本的小例子,來直觀感受一下 Compose 編程思想的明顯差異。

  • 傳統的 UI 編程方式

    我們再熟悉不過了。常見的操作是先定義一個 xml,然後通過 Activity 的 setContentView() 將 xml 放進去,之後就交給系統來加載。

xml <androidx.constraintlayout.widget.ConstraintLayout ...> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World" ... /> </androidx.constraintlayout.widget.ConstraintLayout> kotlin class MainActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { ... setContentView(R.layout.activity_main) } } * Compose 編程方式

Compose UI 工具包則依賴 Composable 註解將展示 UI 的函數聲明為可組合函數,Compose 編譯器負責標記可組合函數內的組件,並進行展示。

佈局的部分均需要放在該函數內交由 Compose 組合。

```kotlin class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { ... setContent { SimpleComposable() } }

@Composable
fun SimpleComposable() {
    Text("Hello World")
}

} ```

6.3 進階示例

來看一下下面這個簡單的動態效果,並思考一下:如果採用傳統的 View 編程方式來實現,你需要多少代碼量?

採用傳統的 View 方式,無非是如下的思路,佈局加上邏輯少於 100 行代碼不太容易實現:

  • 佈局:CardView + LinearLayout(ImageView + TextView)
  • 代碼:監聽、展開和隱藏 TextView,並考慮陰影和淡出動畫)

那如果採用 Compose 來實現呢?只需要 10 行即可。

  1. Composable 函數組合圓角組件 Card + 垂直佈局組件 Column
  2. Column 嵌套圖片組件 Image 和動畫組件 AnimatedVisibility 包裹的文本組件 Text
  3. Column 的 click 事件更新展開或隱藏的 state,進而觸發 AnimatedVisibility 的重組,刷新 Text 的展示與否

kotlin @Composable fun JetpackCompose() { Card { var expanded by remember { mutableStateOf(false) } Column(Modifier.clickable { expanded = !expanded }) { Image(painterResource(R.drawable.jetpack_compose)) AnimatedVisibility(expanded) { Text( text = "Jetpack Compose", style = MaterialTheme.typographt.h2, ) } } } }

6.4 優勢

篇幅有限,事實上 Compose 具備非常多的優勢,亟待大家的挖掘: * 聲明式 UI:只負責描述界面,Compose 系統負責其餘工作

  • 狀態驅動:界面隨着狀態自動更新

  • 高效渲染:固定測量,層級嵌套性能仍是 O(n)

  • 結合 AS 的 Preview 視圖可實時查看和直接交互 UI

  • 兼容傳統 View 樹編程方式,可混合使用

  • 支持 Material Design 設計語言

  • 擁有 Jetpack 框架的大力配合

  • 基於 Kotlin,代碼簡潔,大量 Kotlin 專屬 API

  • 跨平台亦有佈局:DesktopWeb

大家可以利用 Compose 先來實現一個新畫面,或者改造一個現有畫面,逐步推進 Compose 的學習和實踐。但是 Compose UI 工具包目前在部分場景下的組件支持有限,比如 WebViewCameraView 等,這些場景下仍需要配合 Android 原生的 View 方式來完成。

6.5 Sample

  • 官方 Sample

完全使用 Compose 設計的八大主流場景的 App:官方出品,專業、全面。 http://github.com/android/compose-samples

  • 俄羅斯方塊 fun 神將自定義 Compose 組件和狀態管理髮揮到了極致,搭配定時器和各式動畫實現,非常值得用來深入學習 Compose 技術。 http://github.com/vitaviva/compose-tetris
  • ComposeBird 本人在 fun 神的俄羅斯方塊遊戲的激勵下使用 Compose 復刻了風靡一時的 Flappy Bird,感興趣的也可以學習實現思路。 http://github.com/ellisonchan/ComposeBird

未來展望

本次介紹了 MAD 涵蓋的諸多新技術,大家可以感受到 Google 在一刻不停地革新技術。從工具到語言、框架到發行方式都在進行全方位地改良,之前耕耘多年的技術説廢就廢,絕不手軟。

究其原因,繞不開產品生命的兩大角色:開發者和消費者。

  • 提升開發者的開發效率
  • 改善消費者的產品體驗

然而新事物的出現必然伴隨着舊事物的衰落,開發者該如何對待老技術、如何看待層出不窮、前途不明的新技術?光跨平台這一項,Google 和 Jetbrains 就推出了 Flutter、KMM、Compose Multiplatform 三個技術,任何人都卷不過來的。

我總結了幾句四字短語,與你分享我的感受和態度:

  • 不可無視,適當瞭解,跟上形勢:保持關注,防止日後看不懂人家用了什麼技術,甚至無法理解別人的代碼
  • 擁抱變化,勇於嚐鮮,有備無患:找個感興趣的切入點虛心學習、體會新技術的動機
  • 不可依賴,瞭解原理,學習模仿:光使用還不夠,需要深入瞭解其實現,確保坑來臨的時候遊刃有餘
  • 是否深入,見仁見智,自行評估:適當取捨、甚至觀望,一些技術是曇花一現的

資料資源

官方資料

各類學習點的文檔主頁,主頁和分支頁面從背景、思想、API到使用方法等層面進行了充分説明。可以幫助你快速瞭解和掌握相關技術。

| 技術 | 地址 | | ------- | ------------------------------------------------------------ | | MAD | http://developer.android.google.cn/modern-android-development | | AS | http://developer.android.google.cn/studio | | AAP | http://developer.android.google.cn/platform/technology/app-bundle | | Kotlin | http://kotlinlang.org/http://developer.android.google.cn/kotlin | | Jetpack | http://developer.android.google.cn/jetpack | | Compose | http://developer.android.google.cn/jetpack/compose |

官方 Sample

Google優秀的開發者關係工程師的誠心之作,針對語言、工具和框架開發和持續維護着詳盡的Sample。輔助大家學習這些技術,並進行適當地借鑑。

| 學習對象 | 地址 | | -------- | ----------------------------------------------------------- | | Kotlin | http://github.com/MindorksOpenSource/from-java-to-kotlin | | Compose | http://github.com/android/compose-samples | | Jetpack | http://github.com/android/sunflower |

我的文章

Compose 系列: * 「一氣呵成:用Compose完美復刻Flappy Bird!」

Jetpack 系列:

我的 Sample

| 內容 | 地址/名稱 | | ------------- | --------------------------------------------- | | Jetpack Demo | http://github.com/ellisonchan/JetpackDemo | | ComposeBird | http://github.com/ellisonchan/ComposeBird |