close

Android 誕生已久,其開發方式保持着高頻更迭,相較於早期的開發方式已大不相同,尤其是近幾年 Google 熱切推崇的 MAD 開發技術。

其實很多開發者已經有意或無意地正在使用這門技術,借着 2022 開年探討技術趨勢的契機,想要完整地總結 MAD 的願景、構成、優勢以及一些學習建議。

MAD,全稱 Modern Android Development:是 Google 針對 Android 平台提出的全新開發技術。旨在指導我們利用官方推出的各項技術來進行高效的 App 開發。

有的時候 Google 會將其翻譯成現代安卓開發,有的時候又翻譯成新式安卓開發,個人覺得前者的翻譯雖然激進、倒也貼切。

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

內容前瞻
【Modern Android Development】講述 Android 全新開發技術的由來和構成
【Android Studio】演示 Android 官方 IDE 的重要特性
【Android App Bundle】簡要普及 Google 推崇的 App 新格式
【Kotlin】解讀 Android 首推的開發語言的優點
【Jetpack】講述 Android 持續更新的重大框架集合,並逐個演示重要框架解決的問題和優勢
【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.1Release candidate即將發布的下一代版本,可以提前體驗新特性和優化,最新版為 Bunblebee|2021.1.1Canary試驗版本,不穩定但可以試用領先的實驗功能,最新版為 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 WorkManager 的 Background 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 函數的寫法:

StringgenerateAnswerString(intcount,intcountThreshold){if(count>countThreshold){return"Ihavetheanswer.";}else{return"Theanswereludesme.";}}

Java 支持三元運算符可以進一步簡化。

StringgenerateAnswerString(intcount,intcountThreshold){returncount>countThreshold?"Ihavetheanswer.":"Theanswereludesme.";}

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

fungenerateAnswerString(count:Int,countThreshold:Int):String{returnif(count>countThreshold)"Ihavetheanswer."else"Theanswereludesme."}

它同時還可以省略大括號和 return 關鍵字,採用賦值形式進一步簡化。這樣子的寫法已經很接近於語言的日常表達,高級~

fungenerateAnswerString(count:Int,countThreshold:Int):String=if(count>countThreshold)"Ihavetheanswer."else"Theanswereludesme."

反編譯 Class 之後發現其實際上仍採用的三元運算符的寫法,這種語法糖會體現在 Kotlin 的很多地方😅。

publicfinalStringgenerateAnswerString2(intcount,intcountThreshold){returncount>countThreshold?"Ihavetheanswer.":"Theanswereludesme.";}4.3 高階函數

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

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

interfaceMapper{intmap(Stringinput);}classTemp{voidmain(){stringMapper("Android",input->input.length()+2);}intstringMapper(Stringinput,Mappermapper){//Dosomething...returnmapper.map(input);}}

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

這種接受函數作為參數或返回值的函數稱之為高階函數,非常方便。

classTemp{funmain(){stringMapper("Android"){input->input.length+2}}funstringMapper(input:String,mapper:(String)->Int):Int{//Dosomething...returnmapper(input)}}

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

4.4 Null 安全

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

voidfunction(Beanbean){//Nullcheckif(bean!=null){bean.doSometh();}//或者衛語句if(bean==null){return;}bean.doSometh();}

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

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

funfunctionA(){varbean:Bean?=nullfunctionB(bean)}funfunctionB(bean:Bean){bean.doSometh()}

為了通過編譯,可以將變量 bean 聲明中的 ? 去掉, 並賦上正常的值。

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

在使用之前也加上 ? 的限定,表示該參數不為空的情況下才觸發調用
在使用之前加上 !! 的限定也可以,但表示無論參數是否為空的情況下都觸發調用,這種強制的調用即會告知開發者此處有 NPE 的風險
funfunctionB(bean:Bean?){//bean.doSometh()//仍然直接調用將導致編譯失敗//不為空才調用bean?.doSometh()//或強制調用,開發者已知NPE風險bean!!.doSometh()}

總結起來將很好理解:

參數為非空類型,傳遞的實例也必須不為空
參數為可空類型,內部的調用必須明確地 Null 處理

反編譯一段 Null 處理後可以看到,非空類型本質上是利用 @NotNull 的註解,可空類型調用前的 ? 則是手動的 null 判斷。

publicfinalintstringMapper(@NotNullStringstr,@NotNullFunction1mapper){...return((Number)mapper.invoke(str)).intValue();}privatefinalvoidfunction(Stringbean){if(bean!=null){booleanvar3=false;Double.parseDouble(bean); }}4.5 協程 Coroutines

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

比如:AsyncTask、Handler、HandlerThread、IntentService、RxJava、LiveData 等。它們都有複雜易錯、不簡潔、回調冗餘的痛點。

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

voidlogin(Stringusername,Stringtoken){StringjsonBody="{username:\"$username\",token:\"$token\"}";Executors.newSingleThreadExecutor().execute(()->{Resultresult;try{result=makeLoginRequest(jsonBody);}catch(IOExceptione){result=newResult(e);}ResultfinalResult=result;newHandler(Looper.getMainLooper()).post(()->updateUI(finalResult));});}ResultmakeLoginRequest(StringjsonBody)throwsIOException{URLurl=newURL("https://example.com/login");HttpURLConnectionhttpURLConnection=(HttpURLConnection)url.openConnection();httpURLConnection.setRequestMethod("POST");...httpURLConnection.connect();intcode=httpURLConnection.getResponseCode();if(code==200){//Handleinputstream...returnnewResult(bean);}else{returnnewResult(code);}}

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

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

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

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

funlogin(username:String,token:String){valjsonBody="{username:\"\$username\",token:\"\$token\"}"GlobalScope.launch(Dispatchers.Main){valresult=try{makeLoginRequest(jsonBody)}catch(e:Exception){Result(e)}updateUI(result)}}@Throws(IOException::class)suspendfunmakeLoginRequest(jsonBody:String):Result{valurl=URL("https://example.com/login")varresult:ResultwithContext(Dispatchers.IO){valhttpURLConnection=url.openConnection()asHttpURLConnectionhttpURLConnection.run{requestMethod="POST"...}httpURLConnection.connect()valcode=httpURLConnection.responseCoderesult=if(code==200){Result(bean)}else{Result(code)}}returnresult}4.6 KTX

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

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

voidupdatePref(SharedPreferencessharedPreferences,booleanvalue){sharedPreferences.edit().putBoolean("key",value).apply();}

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

funupdatePref(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 風險、大量的綁定代碼、類型轉換危險@ButterKnifeNPE 風險、額外的註解代碼、不適用於多模塊項目(APT 工具解析 Library 受限)KAE 插件NPE 風險、操作其他布局的風險、Kotlin 語言獨占、已經廢棄

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

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

<!--result_profile.xml--><LinearLayout...><TextViewandroid:id="@+id/name"/></LinearLayout>

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

classMainActivity:AppCompatActivity(){overridefunonCreate(savedInstanceState:Bundle){super.onCreate(savedInstanceState)valbinding=ResultProfileBinding.inflate(layoutInflater)setContentView(binding.root)binding.name.text="Helloworld"}}

原理比較簡單:編譯器將生成布局同名的綁定類文件,然後在初始化的時候將布局裡的 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 上需要經過如下步驟:

創建 UI 布局
綁定布局中 View 實例
數據逐一更新到 View 的對應屬性

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

<layout...><data><importtype="android.view.View"/><variablename="viewModel"type="com.example.splash.ViewModel"/></data><LinearLayout...><TextView...android:text="@{viewModel.userName}"android:visibility="@{viewModel.age>=18?View.VISIBLE:View.GONE}"/></LinearLayout></layout>

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

valbinding=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 的時候斷開連接。

classMyLifecycleObserver(privatevallifecycle:Lifecycle):LifecycleObserver{...@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)funinit(){enabled=checkStatus()}@OnLifecycleEvent(Lifecycle.Event.ON_START)funstart(){if(enabled){connect()}}@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)funstop(){if(connected){disconnect()}}}

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

classMainActivity:AppCompatActivity(){overridefunonCreate(savedInstanceState:Bundle){...MyLifecycleObserver(lifecycle).also{lifecycle.addObserver(it)}}}

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

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

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

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

classMyLifecycleObserver(...):LifecycleObserver{@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)funinit(){checkStatus{result->if(result){enable()}}}funenable(){enabled=true//初始化完畢的時候確保只有在STARTED及以後的狀態下執行連接if(lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)){if(!connected){connect()}}}...}5.4 Live Data

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

classStockLiveData(symbol:String):LiveData<BigDecimal>(){privatevalstockManager=StockManager(symbol)privatevallistener={price:BigDecimal->//將請求到的數據發射出去value=price}//畫面活動狀態下才請求overridefunonActive(){stockManager.requestPriceUpdates(listener)}//非活動狀態下移除請求overridefunonInactive(){stockManager.removeUpdates(listener)}}classMainActivity:AppCompatActivity(){overridefunonCreate(savedInstanceState:Bundle?){//註冊觀察StockLiveData("Tesla").run{observe(this@MainActivity,Observer{...})}}}

支持異步傳遞數據以外,LiveData 還有很多優勢:

與 Lifecycle 框架深度綁定
具有生命周期感知能力,數據不會發射給非活動狀態的觀察者
觀察者銷毀了自動釋放數據,避免內存泄露
支持 Room 、Retrofit 框架
支持合併多個數據源統一觀察的 MediatorLiveData(省去多個 LiveData 多次 observe 的醜陋處理))

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

5.5 Room

Android 上開發數據庫有哪些痛點?

需要實現 SQLite 相關的 Helper 實例並實裝初始化和 CRUD 等命令
自行處理異步操作
Cursor實例需要小心處理
字段對應關係
index 對齊
關閉

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

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

@EntityclassMovie():BaseObservable(){@PrimaryKey(autoGenerate=true)varid=0@ColumnInfo(name="movie_name",defaultValue="HarryPotter")lateinitvarname:String...}@DaointerfaceMovieDao{@Insertfuninsert(varargmovies:Movie?):LongArray?@Deletefundelete(movie:Movie?):Int@Updatefunupdate(varargmovies:Movie?):Int@get:Query("SELECT*FROMmovie")valallMovies:LiveData<List<Movie?>?>}@Database(entities=[Movie::class],version=1)abstractclassMovieDataBase:RoomDatabase(){abstractfunmovieDao():MovieDaocompanionobject{@VolatileprivatevarsInstance:MovieDataBase?=nullprivateconstvalDATA_BASE_NAME="jetpack_movie.db"@JvmStaticfungetInstance(context:Context):MovieDataBase?{if(sInstance==null){synchronized(MovieDataBase::class.java){if(sInstance==null){sInstance=createInstance(context)}}}returnsInstance}privatefuncreateInstance(context:Context):MovieDataBase{returnRoom.databaseBuilder(context.applicationContext,MovieDataBase::class.java,DATA_BASE_NAME).build()}}}

在 ViewModel 初始化 DataBase 接口之後即可利用其提供的 DAO 接口執行操作,接着利用 LiveData 將數據發射到 UI。

classMovieViewModel(application:Application):AndroidViewModel(application){privatevalmediatorLiveData=MediatorLiveData<List<Movie?>?>()privatevaldb:MovieDataBase?init{db=MovieDataBase.getInstance(application)if(db!=null){mediatorLiveData.addSource(db.movieDao().allMovies){movieList->if(db.databaseCreated.value!=null){mediatorLiveData.postValue(movieList)}}};}fungetMovieList(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 可自動恢復,無需額外處理。

classPersonContextModel(application:Application):AndroidViewModel(application){valpersonLiveData=MutableLiveData<Person>()valpersonInWork:Unitget(){valtestPerson=Person(30,"Ellison")personLiveData.postValue(testPerson)}}classMainActivity:AppCompatActivity(){overridefunonCreate(savedInstanceState:Bundle?){...valmodel=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進行開發的話,幾十行代碼即可完成預覽功能。

privatevoidsetupCamera(PreviewViewpreviewView){ListenableFuture<ProcessCameraProvider>cameraProviderFuture=ProcessCameraProvider.getInstance(this);cameraProviderFuture.addListener(()->{try{mCameraProvider=cameraProviderFuture.get();bindPreview(mCameraProvider,previewView);}catch(ExecutionException|InterruptedExceptione){e.printStackTrace();}},ContextCompat.getMainExecutor(this));}privatevoidbindPreview(@NonNullProcessCameraProvidercameraProvider,PreviewViewpreviewView){mPreview=newPreview.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、MMKVStartUp簡化應用啟動的組件初始化,提高應用啟動性能的框架-Navigation簡化畫面跳轉,支持標籤導航、抽屜導航等複雜設計的路由框架ARouterActivityResultActivity、Fragment 之間傳遞數據的新框架onActivityResult/IntentPaging3按需加載節省網絡流量和內存消耗的分頁加載框架-WorkManager調度退出應用或重啟設備後仍可運行的可延期異步任務框架。JobService、Alarm、BroadcastHiltAndroid 專用的DI框架,快速建立之間的依賴關係和生命周期Dagger2、KoilAppCompat提供Activity、Dialog 和 View 的 Base 類,兼容 Jetpack 的大量處理-ViewPager2實現經典的標籤導航設計的新框架ViewPager...

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

5.9 官方推薦的應用架構

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

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

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

6.Jetpack Compose

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

原因:

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

其發展歷程:

17 年立項
之後長達三年的內部調查和實驗
20 年初 dev 版公開,年中 alpha 版推出
21 年初 beta 版發布
21 年 4 月全球挑戰推廣
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 放進去,之後就交給系統來加載。

<androidx.constraintlayout.widget.ConstraintLayout...><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="HelloWorld".../></androidx.constraintlayout.widget.ConstraintLayout>classMainActivity:AppCompatActivity(){overridefunonCreate(savedInstanceState:Bundle?){...setContentView(R.layout.activity_main)}}
Compose 編程方式

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

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

classMainActivity:AppCompatActivity(){overridefunonCreate(savedInstanceState:Bundle?){...setContent{SimpleComposable()}}@ComposablefunSimpleComposable(){Text("HelloWorld")}}6.3 進階示例

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

採用傳統的 View 方式,無非是如下的思路,布局加上邏輯少於 100 行代碼不太容易實現:
布局:CardView + LinearLayout(ImageView + TextView)
代碼:監聽、展開和隱藏 TextView,並考慮陰影和淡出動畫)

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

Composable 函數組合圓角組件 Card + 垂直布局組件 Column
Column 嵌套圖片組件 Image 和動畫組件 AnimatedVisibility 包裹的文本組件 Text
Column 的 click 事件更新展開或隱藏的 state,進而觸發 AnimatedVisibility 的重組,刷新 Text 的展示與否
@ComposablefunJetpackCompose(){Card{varexpandedbyremember{mutableStateOf(false)}Column(Modifier.clickable{expanded=!expanded}){Image(painterResource(R.drawable.jetpack_compose))AnimatedVisibility(expanded){Text(text="JetpackCompose",style=MaterialTheme.typographt.h2,)}}}}6.4 優勢

篇幅有限,事實上 Compose 具備非常多的優勢,亟待大家的挖掘:

聲明式 UI:只負責描述界面,Compose 系統負責其餘工作

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

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

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

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

支持 Material Design 設計語言

擁有 Jetpack 框架的大力配合

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

跨平台亦有布局:Desktop、 Web

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

6.5 Sample

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

Movie客戶端 本人使用 Compose 的大部分 UI 組件、視圖切換和數據刷新重構的電影搜索 App。https://github.com/ellisonchan/ComposeMovie

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

ComposeBird 本人在 fun 神的俄羅斯方塊遊戲的激勵下使用 Compose 復刻了風靡一時的 Flappy Bird,感興趣的也可以學習實現思路。https://github.com/ellisonchan/ComposeBird

未來展望

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

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

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

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

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

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

END


推薦文章

打造一個 Compose 版的俄羅斯方塊

歡迎 Jetpack 新成員 SplashScreen

AAB 什麼鬼?竟敢打壓鴻蒙?

網友提議為 Kotlin 引入這些新特性 ...


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()