是讓人耳目一新的 Jetpack MVVM 精講啊!

南方吳彥祖_藍斯發表於2021-10-31

前言

最近在後臺 時有收到 讀者的留言,說能不能出一期 Jetpack MVVM 精講,以及配套一份簡練的案例,好 把玩把玩、感受感受、加深對 MVVM 的印象。

答案是肯定的。

面向標準化開發已成現實

金九銀十,相信有不少讀者在抓緊機會面試。

Android 市場已今非昔比。在過去,迫於招人的壓力,應試者只需瞭解四大元件、檢視、網路請求,即可謀得一份滿意的工作。

現如今,Jetpack 架構元件 及 標準化開發模式 的確立,意味著 Android 開發已步入成熟階段:

許多 樣板程式碼 不再需要開發者手寫,而是可以透過模版工具 自動生成,在取締繁雜耗時的重複工作的同時, 避免因人工操作的疏忽,而造成難以排查、不可預期的錯誤

這十分符合企業的利益,因而面試官在招人的時候,也更加看重應試者對 架構元件 —— 至少是 MVVM 的理解程度。

像“解耦”等 含糊其辭的說法,已經不能夠被面試官所認可,稍微對 MVVM 有一點經驗的面試官都會請你舉例說明,好證明你確實對 MVVM 有著正確、深入的理解,能夠自然而然地寫出標準化、規範化的程式碼,能夠迅速適應 各家公司自制的 自動化模版工具。

本文的目標

結合前幾期我們分別 深入淺出 介紹過的 Lifecycle、LiveData、ViewModel、DataBinding,來融匯貫通地演繹一下:

作為 應用開發骨架 的 標準化狀態管理框架,究竟為 快速開發過程中 減少不可預期的錯誤 做了哪些努力。

不同於 東拼西湊、人云亦云、徒添困擾 的網文,願意將 標準化開發模式的  深度思考知識 和  實戰反思經驗 無保留地分享,全網僅此一家。 這樣的文章可以說是 看一篇、少一篇,因此,就算不去 hold 住面試官,也請務必跟隨本文的腳步,無障礙地將 Jetpack MVVM 過一遍!

文章目錄一覽

  • 前言
  • 面向標準化開發已成現實
  • 本文的目標
  • Jetpack Lifecycle
    • Lifecycle 存在前的混沌世界
    • Lifecycle 為什麼能解決上述這些問題?


  • Jetpack LiveData
    • LiveData 存在前的混沌世界
    • LiveData 為什麼能解決上述這些問題?
    • LiveData 有個坑需要注意
    • Note: 2020.07.09


  • Jetpack ViewModel
    • ViewModel 存在前的混沌世界
    • ViewModel 為什麼能做到這幾點?


  • Jetpack DataBinding
    • DataBinding 存在前的混沌世界
    • DataBinding 就是來解決這些問題


  • 綜上

Jetpack Lifecycle

Lifecycle 的存在,主要是為了解決 生命週期管理 的一致性問題

Lifecycle 存在前的混沌世界

在 Lifecycle 面市前,生命週期管理 純靠手工維持,這樣就容易滋生大量的一致性問題。

例如跨頁面共享的 GpsManager 元件,在每個依賴它的 Activity 的 onResume 和 onPause 中都需要  手工 啟用、解綁 和 叫停

那麼  隨著 Activity 的增多,這種手工操作 埋下的一致性隱患 就會指數級增長

一方面,凡是手工維持的,開發者容易疏忽,特別是工作交接給其他同事時,同事並不能及時注意到這些細節。
另一方面,分散的程式碼不利於修改,日後除了啟用、叫停,若有其他操作需要補充(例如狀態監聽),那麼每個 Activity 都需要額外書寫一遍。
是讓人耳目一新的 Jetpack MVVM 精講啊!

Lifecycle 為什麼能解決上述這些問題?

Lifecycle 透過 模板方法模式 和 觀察者模式,將生命週期管理的複雜操作,全部在作為 LifecycleOwner 的基類中(例如檢視控制器的基類)封裝好,默默地在背後為開發者運籌帷幄,

開發者因而得以在檢視控制器(子類)中只需一句  getLifecycle().addObserver(GpsManager.getInstance) ,優雅地完成 第三方元件在自己內部 對 LifecycleOwner 生命週期的感知。

是讓人耳目一新的 Jetpack MVVM 精講啊!

除了解決一致性問題,這樣做還  順帶地提供了其他 2 個好處

1.規避 為監聽狀態 而 注入檢視控制器 的做法

當需要監聽狀態時,以往我們的做法是 透過方法手工注入 Activity 等引數,這埋下了記憶體洩漏的隱患 —— 因為團隊中的新手容易因這是個 Activity,而在日後誤將其依賴給元件中的其他成員。

現如今,我們可以直接在元件內部 點到為止 地監聽 LifecycleOwner 的狀態,從而規避這種不恰當的使用。

2.規避 為追溯事故來源 而 注入檢視控制器 的做法

當發生事故時,以往我們若想在元件中  追溯事故來源,同樣不得不從方法中直接注入 Activity 等,這同樣埋下了記憶體洩漏的隱患。現如今元件因實現了 DefaultLifecycleObserver,而得以透過生命週期回撥方法中的 LifecycleOwner 引數, 在方法作用域中 即可得知事故來源,無需更多帶有隱患的操作。

如果這麼說還不理解的話,可具體參考我在  《為你還原一個真實的 Jetpack Lifecycle》 中提供的 GpsManager 案例,本文不再累述。

Jetpack LiveData

LiveData 的存在,主要是為了幫助  新手老手 都能不假思索地遵循 透過唯一可信源分發狀態 的標準化開發理念,從而使在快速開發過程中 難以追溯、難以排查、不可預期 的問題所發生的機率降低到最小。

LiveData 存在前的混沌世界

在 LiveData 面市前,我們分發狀態,多是透過 EventBus 或 Java Interface 來完成的。不管你是用於網路請求回撥的情況,還是跨頁面通訊的情況。

那這造成了什麼問題呢?首先,EventBus 只是純粹的傳話筒,它  缺乏上述提到的 標準化開發理念 的約束,那麼人們在使用這個框架時,容易因 去中心化 地濫用,而造成 諸如 毫無防備地收到 預期外的 不明來源的推送、拿到過時的資料 及 事件源追溯複雜度 為 n² 的局面

並且, EventBus 本身缺乏 Lifecycle 的加持,存在生命週期管理的一致性問題。這是 EventBus 的硬傷,也是我拒絕使用 EventBus 的最主要因素。

對上述狀況不理解的,可具體參考我在  《LiveData 鮮為人知的 身世背景 和 獨特使命》 中提供的 播放器狀態全域性通知 的案例

LiveData 為什麼能解決上述這些問題?

首先, LiveData 是在 Google 希望確立 標準化、規範化 的開發模式 —— 這樣一種背景下誕生的,因而為了達成這個艱鉅的  使命,LiveData 被十分克制地設計為, 僅支援狀態的輸入和監聽,並且<mark>可基於 “訪問許可權控制” 來實現 “讀寫分離”</mark>

這使得任何一次資料推送,都可被限制為 “只能單方面地從唯一可信源推送而來”,從而避免了訊息同步不一致、不可靠、或是在事件追溯複雜度為 n² 的迷宮中白費時間。(也即,無論是從哪個檢視控制器發起的 對某個共享狀態改變的請求,狀態最終的改變 都由 作為唯一可信源的 單例或 SharedViewModel  在其內部統一決策,並一對多地通知改變

是讓人耳目一新的 Jetpack MVVM 精講啊!

並且,這種承上啟下的方式,使得單向依賴成為可能:單例無需透過 Java Interface 回撥通知檢視控制器,從而規避了檢視控制器 被生命週期更長的單例 依賴 所埋下的記憶體洩漏的隱患。

LiveData 有個坑需要注意

不過,LiveData 的設計有個坑,這裡我順帶提一下。

為了在檢視控制器發生重建後,能夠 自動倒灌 所觀察的 LiveData 的最後一次資料, LiveData 被設計為粘性的事件

—— 我姑且認為這是個擴充性不佳的設計,甚至可以說是一個 bug,

因為  ViewModel 支援共享作用域,並且官方文件都推薦了透過 共享 ViewModel 來實現跨頁面通訊的需求

那麼基於 “開閉原則”,LiveData 理應提供一個與 MutableLiveData 平級的底層支援,專門用於非粘性的事件通訊的情況,否則直接在跨頁面通訊中使用 MutableLiveData  必造成 事件回撥的一致性問題 及 難以預期的錯誤

關於非粘性 LiveData 的實現,網上存在透過 “事件包裝類”(只適合 kotlin 的情況) 和 “反射干預 LastVersion” (適用於 Java 的情況)兩種方式來解決:

《在 SnackBar 和其他事件中使用 LiveData》(SingleLiveEvent 案例)
《LiveDataBus實現原理#用法詳解#LiveData擴充套件》(反射案例)

無論是使用哪一種實現,我都建議 遵循傳統 LiveData 所遵循的開發理念, 透過唯一可信源分發狀態,來確保訊息同步的可預期和可追溯。對於 “去中心化” 的 Bus 方式,我拒絕在專案中這樣使用。

Note 2020.07.09:

Event 包裝器

是讓人耳目一新的 Jetpack MVVM 精講啊!

非入侵重寫:

是讓人耳目一新的 Jetpack MVVM 精講啊!

UnPeekLiveData:

是讓人耳目一新的 Jetpack MVVM 精講啊!
考慮到手寫 Event 事件包裝器,在 Java 中存在 null 安全的一致性問題;而反射干預 Version 的方式又存在延遲(無法用於對實時性有要求的場景)、並且資料會隨著 SharedViewModel 長久滯留在記憶體中得不到釋放。
於是重寫並封裝了  專用於 “一次性事件” 場景需求 的  UnPeek-LiveData

UnPeek-LiveData 經過小夥伴們的熱心嘗試和反饋,現已演化成熟並滿足:

  1. 一條訊息能被多個觀察者消費
  2. 訊息被所有觀察者消費完畢後才開始阻止倒灌
  3. 可以透過 clear 方法手動將訊息從記憶體中移除
  4. 讓非入侵設計成為可能,遵循開閉原則
  5. 基於 “訪問許可權控制” 支援 "讀寫分離”,遵循唯一可信源的訊息分發理念
具體可參考  《UnPeek-LiveData》 最新原始碼。

Jetpack ViewModel

ViewModel 的存在,主要是為了解決 狀態管理 和 頁面通訊 的問題。

ViewModel 存在前的混沌世界

ViewModel 的本職工作是  狀態託管 和  狀態管理的分治,也即當檢視控制器重建時,

對於輕量的狀態,可以透過檢視控制器基類的 saveInstanceState 機制,以序列化的方式完成儲存和恢復。
對於重量級的狀態,例如透過網路請求得到的 List,可以透過生命週期長於檢視控制器的 ViewModel 持有,從而得以直接從 ViewModel 恢復,而不是以效率較低的序列化方式。

在 Jetpack ViewModel 面市之前,MVP 的 Presenter 和 MVVM - Clean 的 ViewModel 都不具備狀態管理分治的能力。

Presenter 和 Clean ViewModel 的生命週期都與檢視控制器同生共死,因而它們頂多是為 DataBinding 提供狀態的託管,而無法實現狀態的分治。

到了 Jetpack 這一版,ViewModel 以精妙的設計,達成了狀態管理,以及可共享的作用域。

ViewModel 為什麼能做到這幾點?

其實這版主要是基於  工廠模式,使得 ViewModel  被 LifecycleOwner 所持有、透過 ViewModelProvider 來引用

所以  它既類似於單例: —— 當被作為 LifecycleOwner 的 Activity 持有時,能夠脫離 Activity 旗下 Fragment 的生命週期,從而實現作用域共享,

實際上又不是單例: —— 生命週期跟隨 作為 LifecycleOwner 的檢視控制器,當 Owner(Activity 或 Fragment)被銷燬時,它也被 clear。

此外,出於對檢視控制器重建的考慮,Google 在檢視控制器基類中透過 retain 機制對 ViewModel 進行了保留。
因此,對於 作用域共享 和 檢視重建 的情況,狀態因完好地被保留,而得以被檢視控制器在恢復時直接使用。

再者,由於存在 共享作用域的考慮,所以 ViewModel 本身也承擔了跨頁面通訊(例如事件回撥)的職責。前面在介紹 LiveData 時,對於 LiveData 在事件通訊時粘性設計的問題已經介紹過了,這裡不再累述。

Note:截至 2020.2.1,ViewModel 在 Fragment 中的 retain 設計已發生劇變,具體緣由可參考我在  《在頁面開發中 左右逢源的 Jetpack ViewModel》 文末及評論區的最新補充。

Jetpack DataBinding

DataBinding 的存在,主要是為了解決 檢視呼叫 的一致性問題。

DataBinding 存在前的混沌世界

在 DataBinding 面市前,我們若要改變檢視的狀態,首先就要引用該檢視,例如 textView.setText(),

這造成什麼問題呢?

是讓人耳目一新的 Jetpack MVVM 精講啊!
當頁面存在橫、豎佈局,且兩種佈局的控制元件存在差異,例如橫屏存在 textView 控制元件,而豎屏沒有,那麼我們就不得不在檢視控制器中為 textView 做判空處理,這就造成了一致性問題 —— 容易疏忽而忘記判空,畢竟頁面多達數十個、每個頁面呼叫控制元件的地方也無數。

那怎麼辦呢?

DataBinding 就是來解決這些問題

透過讓 “佈局中存在的控制元件” 與 “可觀察的資料” 發生繫結,那麼當該資料被 set 新的內容時,被繫結了該資料的控制元件即可獲得通知和重新整理。

換言之,在使用 DataBinding 後,唯一的改變是,你無需手工呼叫檢視來 set 新狀態,你只需 set 資料本身。

因而, DataBinding 並非許多人不假思索認為的,將 UI 邏輯搬到 XML 中寫 從而難以除錯 —— 事實根本不是這樣的:

DataBinding 只負責繫結資料、負責作為 UI 邏輯末端的狀態的改變(也即它是一個不可再分的原子操作,本來就不需要除錯),原本在檢視控制器中 UI 邏輯怎麼寫,現在還是怎麼寫,只不過不再需要 textView.setText(xxx),而是直接 xxx.set()。

所以在 DataBinding 的幫助下,好處總共有多少個呢?

1.規避了檢視呼叫的 一致性問題 —— 無需手工判空。
2.規避了檢視呼叫的 一致性問題,乃至無需手動呼叫檢視,從而完全不用編寫 findViewById。
3.就算要呼叫檢視,也不用 findViewById,而是直接透過 binding 來引用。
4.先前的 UI 邏輯基本不用改動,改的只是作為末端的狀態改變的方式。

……

此外, DataBinding 有個大殺器就是,能為控制元件提供自定義屬性的 BindingAdapter,它不僅可以解決 圓角 Drawable 複用的問題(你懂得),還可以實現 imageView 直接繫結 url 等需求,總之,沒有它辦不到的,只有你想不到的,DataBinding 的好處等著你挖掘。

關於 DataBinding 的注意事項、屢試不爽的排坑技巧,以及獨家解析的 <mark>“ DataBinding 嚴格模式”</mark>,可具體參考  《從被誤解到 “真香” 的 Jetpack DataBinding》,這裡不做累述。

綜上

Lifecycle 的存在,主要是為了解決  生命週期管理 的一致性問題

LiveData 的存在,主要是為了幫助 新手老手 都能不假思索地  遵循 透過唯一可信源分發狀態 的標準化開發理念,從而在快速開發過程中 規避一系列  難以追溯、難以排查、不可預期 的問題。

ViewModel 的存在,主要是為了解決  狀態管理 和 頁面通訊 的問題

DataBinding 的存在,主要是為了解決  檢視呼叫 的一致性問題

它們的存在 大都是為了 在軟體工程的背景下 解決一致性的問題、將容易出錯的操作在後臺封裝好, 方便使用者快速、穩定、不產生預期外錯誤地編碼

全文完

本文配套專案

是讓人耳目一新的 Jetpack MVVM 精講啊!

GitHub : Jetpack-MVVM-Best-Practice

版權宣告

本文以  CC 署名-非商業性使用-禁止演繹 4.0 國際協議 發行。

Copyright © 2019-present KunMinX

是讓人耳目一新的 Jetpack MVVM 精講啊!

文中提到的 “xxx 架構元件的存在,主要是為了在 多人協作的軟體工程背景下  解決 xxx 的一致性問題”,以及 “LiveData 在頁面通訊、事件回撥的場景下發生  資料倒灌” 等多處  對特定現象及其本質的匹配和概括,均屬於本人獨立原創的成果,本人對此享有所有權和最終解釋權。

當您借鑑或引用本文的引言、思路、結論進行二次創作,或全文轉載時,須註明連結出處,否則我們保留追責的權利。

未經與作者本人當面溝通許可,不得將文章內容用於洗稿、廣告包裝等商業用途。

文章轉自  juejin.cn/post/68449039 ,如有侵權,請聯絡刪除。


相關影片推薦:

Android 效能最佳化學習【二】:APP啟動速度最佳化_嗶哩嗶哩_bilibili
【Android進階系統學習】:位元組碼插樁技術實現自動化方法耗時記錄_嗶哩嗶哩_bilibili
Android 效能最佳化學習【二】:APP啟動速度最佳化_嗶哩嗶哩_bilibili
【Android面試專題】:面試又被問到程式間通訊,你卻連Binder是什麼都不知道?_嗶哩嗶哩_bilibili
BAT面試技巧——Android面試必問的網路程式設計你瞭解多少?_嗶哩嗶哩_bilibili

編輯於剛剛


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2839906/,如需轉載,請註明出處,否則將追究法律責任。

相關文章