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("https://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("https://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:官方出品,專業、全面。 https://github.com/android/compose-samples

  • 俄羅斯方塊 fun 神將自定義 Compose 元件和狀態管理髮揮到了極致,搭配定時器和各式動畫實現,非常值得用來深入學習 Compose 技術。 https://github.com/vitaviva/compose-tetris

未來展望

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

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

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

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

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

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

資料資源

官方資料

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

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

官方 Sample

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

| 學習物件 | 地址 | | -------- | ----------------------------------------------------------- | | Kotlin | https://github.com/MindorksOpenSource/from-java-to-kotlin | | Compose | https://github.com/android/compose-samples | | Jetpack | https://github.com/android/sunflower |

我的文章

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

Jetpack 系列:

我的 Sample

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