轉載:Android官方架構組件ViewModel:從前世今生到追本溯源
概述
2017年的Google I/O大會上,Google推出了一系列譬如 Lifecycle、ViewModel、LiveData等一系列 更適合用于MVVM模式開發 的架構組件。
本文的主角就是 ViewModel ,也許有朋友會提出質疑:
ViewModel 這么簡單的東西,從API的使用到源碼分析,相關內容都爛大街了,你這篇文章還能翻出什么花來?
我無法反駁,事實上,閱讀本文的您可能對MVVM的代碼已經 駕輕就熟,甚至是經歷了完整項目的洗禮,但我依然想做一次大膽地寫作嘗試—— 即使對于MVVM模式的思想噗之以鼻,或者已經熟練使用MVVM,本文也盡量讓您有所收獲,至少閱讀體驗不那么枯燥。
ViewModel的前世今生
ViewModel,或者說 MVVM (Model-View-ViewModel),并非是一個新鮮的詞匯,它的定義最早起源于前端,代表著 數據驅動視圖 的思想。
比如說,我們可以通過一個String
類型的狀態來表示一個TextView
,同理,我們也可以通過一個List<T>
類型的狀態來維護一個RecyclerView
的列表——在實際開發中我們通過觀察這些數據的狀態,來維護UI的自動更新,這就是 數據驅動視圖(觀察者模式)。
每當String
的數據狀態發生變更,View層就能檢測并自動執行UI的更新,同理,每當列表的數據源List<T>
發生變更,RecyclerView
也會自動刷新列表:
[圖片上傳失敗...(image-8eb93d-1604915081142)]
對于開發者來講,在開發過程中可以大幅減少UI層和Model層相互調用的代碼,轉而將更多的重心投入到業務代碼的編寫。
ViewModel 的概念就是這樣被提出來的,我對它的形容類似一個 狀態存儲器 , 它存儲著UI中各種各樣的狀態, 以 登錄界面 為例,我們很容易想到最簡單的兩種狀態 :
class LoginViewModel {
val username: String // 用戶名輸入框中的內容
val password: String // 密碼輸入框中的內容
}
先不糾結于代碼的細節,現在我們知道了ViewModel的重心是對 數據狀態的維護。接下來我們來看看,在17年之前Google還沒有推出ViewModel組件之前,Android領域內MVVM 百花齊放的各種形態 吧。
1.群雄割據時代的百花齊放
說到MVVM就不得不提Google在2015年IO大會上提出的DataBinding
庫,它的發布直接促進了MVVM在Android領域的發展,開發者可以直接通過將數據狀態通過 偽Java代碼 的形式綁定在xml
布局文件中,從而將MVVM模式的開發流程形成一個 閉環:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="User" />
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{ user.name }"
android:textSize="20sp" />
</layout>
通過 偽Java代碼 將UI的邏輯直接粗暴的添加進xml
布局文件中達到和View
的綁定,DataBinding
這種實現方式引起了 強烈的爭論。直至如今,依然有很多開發者無法接受DataBinding
,這是完全可以理解的,因為它確實 很難定位語法的錯誤和運行時的崩潰原因。
MVVM模式并不一定依賴于DataBinding
,但是除了DataBinding
,開發者當時并沒有足夠多的選擇——直至目前,仍然有部分的MVVM開發者堅持不使用 DataBinding
,取而代之使用生態圈極為豐富的RxJava
(或者其他)代替 DataBinding
的數據綁定。
如果說當時對于 數據綁定 的庫至少還有官方的DataBinding
可供參考,ViewModel
的規范化則是非常困難——基于ViewModel
層進行狀態的管理這個基本的約束,不同的項目、不同的依賴庫加上不同的開發者,最終代碼中對于 狀態管理 的實現方式都有很大的不同。
比如,有的開發者,將 ViewModel 層像 MVP 一樣定義為一個接口:
interface IViewModel
open class BaseViewModel: IViewModel
也有開發者(比如這個repo)直接將ViewModel層繼承了可觀察的屬性(比如dataBinding
庫的BaseObservable
),并持有Context
的引用:
public class CommentViewModel extends BaseObservable {
@BindingAdapter("containerMargin")
public static void setContainerMargin(View view, boolean isTopLevelComment) {
//...
}
}
一千個人有一千個哈姆雷特,不同的MVVM也有截然不同的實現方式,這種百花齊放的代碼風格、難以嚴格統一的 開發流派 導致代碼質量的參差不齊,代碼的可讀性更是天差地別。
再加上DataBinding
本身導致代碼閱讀性的降低,真可謂南門北派華山論劍,各種思想噴涌而出——從思想的碰撞交流來講,這并非壞事,但是對于當時想學習MVVM的我來講,實在是看得眼花繚亂,在學習接觸的過程中,我也不可避免的走了許多彎路。
2.Google對于ViewModel的規范化嘗試
我們都知道Google在去年的 I/O 大會非常隆重地推出了一系列的 架構組件, ViewModel正是其中之一,也是本文的主角。
有趣的是,相比較于惹眼的 Lifecycle
和 LiveData
, ViewModel
顯得非常低調,它主要提供了這些特性:
- 配置更改期間自動保留其數據 (比如屏幕的橫豎旋轉)
-
Activity
、Fragment
等UI組件之間的通信
如果讓我直接吹捧ViewModel
多么多么優秀,我會非常犯難,因為它表面展現的這些功能實在不夠惹眼,但是有幸截止目前為止,我花費了一些筆墨闡述了ViewModel
在這之前的故事——它們是接下來正文不可缺少的鋪墊。
3.ViewModel在這之前的窘境
也許您尚未意識到,在官方的ViewModel
發布之前,MVVM開發模式中,ViewModel層的一些窘境,但實際上我已經盡力通過敘述的方式將這些問題描述出來:
3.1 更規范化的抽象接口
在官方的ViewModel
發布之前,ViewModel
層的基類多種多樣,內部的依賴和公共邏輯更是五花八門。新的ViewModel
組件直接對ViewModel
層進行了標準化的規范,即使用ViewModel
(或者其子類AndroidViewModel
)。
同時,Google官方建議ViewModel
盡量保證 純的業務代碼,不要持有任何View層(Activity
或者Fragment
)或Lifecycle
的引用,這樣保證了ViewModel
內部代碼的可測試性,避免因為Context
等相關的引用導致測試代碼的難以編寫(比如,MVP中Presenter層代碼的測試就需要額外成本,比如依賴注入或者Mock,以保證單元測試的進行)。
3.2 更便于保存數據
由系統響應用戶交互或者重建組件,用戶無法操控。當組件被銷毀并重建后,原來組件相關的數據也會丟失——最簡單的例子就是屏幕的旋轉,如果數據類型比較簡單,同時數據量也不大,可以通過onSaveInstanceState()
存儲數據,組件重建之后通過onCreate()
,從中讀取Bundle
恢復數據。但如果是大量數據,不方便序列化及反序列化,則上述方法將不適用。
ViewModel
的擴展類則會在這種情況下自動保留其數據,如果Activity
被重新創建了,它會收到被之前相同ViewModel
實例。當所屬Activity
終止后,框架調用ViewModel
的onCleared()
方法釋放對應資源:
[圖片上傳失敗...(image-c38c25-1604915081141)]
這樣看來,ViewModel
是有一定的 作用域 的,它不會在指定的作用域內生成更多的實例,從而節省了更多關于 狀態維護(數據的存儲、序列化和反序列化)的代碼。
ViewModel
在對應的 作用域 內保持生命周期內的 局部單例,這就引發一個更好用的特性,那就是Fragment
、Activity
等UI組件間的通信。
3.3 更方便UI組件之間的通信
一個Activity
中的多個Fragment
相互通訊是很常見的,如果ViewModel
的實例化作用域為Activity
的生命周期,則兩個Fragment
可以持有同一個ViewModel的實例,這也就意味著數據狀態的共享:
public class AFragment extends Fragment {
private CommonViewModel model;
public void onActivityCreated() {
model = ViewModelProviders.of(getActivity()).get(CommonViewModel.class);
}
}
public class BFragment extends Fragment {
private CommonViewModel model;
public void onActivityCreated() {
model = ViewModelProviders.of(getActivity()).get(CommonViewModel.class);
}
}
上面兩個Fragment
getActivity()
返回的是同一個宿主Activity
,因此兩個Fragment
之間返回的是同一個ViewModel
。
我不知道正在閱讀本文的您,有沒有冒出這樣一個想法:
ViewModel提供的這些特性,為什么感覺互相之間沒有聯系呢?
這就引發下面這個問題,那就是:
這些特性的本質是什么?
4. ViewModel:對狀態的持有和維護
ViewModel
層的根本職責,就是負責維護UI的狀態,追根究底就是維護對應的數據——畢竟,無論是MVP還是MVVM,UI的展示就是對數據的渲染。
- 1.定義了
ViewModel
的基類,并建議通過持有LiveData
維護保存數據的狀態; - 2.
ViewModel
不會隨著Activity
的屏幕旋轉而銷毀,減少了維護狀態的代碼成本(數據的存儲和讀取、序列化和反序列化); - 3.在對應的作用域內,保正只生產出對應的唯一實例,多個
Fragment
維護相同的數據狀態,極大減少了UI組件之間的數據傳遞的代碼成本。
現在我們對于ViewModel
的職責和思想都有了一定的了解,按理說接下來我們應該闡述如何使用ViewModel
了,但我想先等等,因為我覺得相比API的使用,掌握其本質的思想會讓你在接下來的代碼實踐中如魚得水。
不,不是源碼解析…
通過庫提供的API接口作為開始,閱讀其內部的源碼,這是標準掌握代碼內部原理的思路,這種方式的時間成本極高,即使有相關源碼分析的博客進行引導,文章中大片大片的源碼和注釋也足以讓人望而卻步,于是我理所當然這么想:
先學會怎么用,再抽空系統學習它的原理和思想吧…
發現沒有,這和上學時候的學習方式竟然截然相反,甚至說本末倒置也不奇怪——任何一個物理或者數學公式,在使用它做題之前,對它背后的基礎理論都應該是優先去系統性學習掌握的(比如,數學公式的學習一般都需要先通過一定方式推導和證明),這樣我才能拿著這個知識點對課后的習題舉一反三。這就好比,如果一個老師直接告訴你一個公式,然后啥都不說讓你做題,這個老師一定是不合格的。
我也不是很喜歡大篇幅地復制源碼,我準備換個角度,站在Google工程師的角度看看怎么樣設計出一個ViewModel
。
站在更高的視角,設計ViewModel
現在我們是Google工程師,讓我們再回顧一下ViewModel
應起到的作用:
- 1.規范化了
ViewModel
的基類; - 2.
ViewModel
不會隨著Activity
的屏幕旋轉而銷毀; - 3.在對應的作用域內,保正只生產出對應的唯一實例,保證UI組件間的通信。
1.設計基類
這個簡直太簡單了:
public abstract class ViewModel {
protected void onCleared() {
}
}
我們定義一個抽象的ViewModel
基類,并定義一個onCleared()
方法以便于釋放對應的資源,接下來,開發者只需要讓他的XXXViewModel
繼承這個抽象的ViewModel
基類即可。
2.保證數據不隨屏幕旋轉而銷毀
這是一個很神奇的功能,但它的實現方式卻非常簡單,我們先了解這樣一個知識點:
setRetainInstance(boolean)
是Fragment
中的一個方法。將這個方法設置為true就可以使當前Fragment
在Activity
重建時存活下來
這似乎和我們的功能非常吻合,于是我們不禁這樣想,可不可以讓Activity
持有這樣一個不可見的Fragment
(我們干脆叫他HolderFragment
),并讓這個HolderFragment
調用setRetainInstance(boolean)
方法并持有ViewModel
——這樣當Activity
因為屏幕的旋轉銷毀并重建時,該Fragment
存儲的ViewModel
自然不會被隨之銷毀回收了:
public class HolderFragment extends Fragment {
public HolderFragment() { setRetainInstance(true); }
private ViewModel mViewModel;
// getter、setter...
}
當然,考慮到一個復雜的UI組件可能會持有多個ViewModel
,我們更應該讓這個不可見的HolderFragment
持有一個ViewModel
的數組(或者Map)——我們干脆封裝一個叫ViewModelStore
的容器對象,用來承載和代理所有ViewModel
的管理:
public class ViewModelStore {
private final HashMap<String, ViewModel> mMap = new HashMap<>();
// put(), get(), clear()....
}
public class HolderFragment extends Fragment {
public HolderFragment() { setRetainInstance(true); }
private ViewModelStore mViewModelStore = new ViewModelStore();
}
好了,接下來需要做的就是,在實例化ViewModel
的時候:
1.當前Activity
如果沒有持有HolderFragment
,就實例化并持有一個HolderFragment
2.Activity
獲取到HolderFragment
,并讓HolderFragment
將ViewModel
存進HashMap
中。
這樣,具有生命周期的Activity
在旋轉屏幕銷毀重建時,因為不可見的HolderFragment
中的ViewModelStore
容器持有了ViewModel
,ViewModel
和其內部的狀態并沒有被回收銷毀。
這需要一個條件,在實例化ViewModel
的時候,我們似乎還需要一個Activity
的引用,這樣才能保證 獲取或者實例化內部的HolderFragment
并將ViewModel
進行存儲。
于是我們設計了這樣一個的API,在ViewModel
的實例化時,加入所需的Activity
依賴:
CommonViewModel viewModel = ViewModelProviders.of(activity).get(CommonViewModel.class)
我們注入了Activity
,因此HolderFragment
的實例化就交給內部的代碼執行:
HolderFragment holderFragmentFor(FragmentActivity activity) {
FragmentManager fm = activity.getSupportFragmentManager();
HolderFragment holder = findHolderFragment(fm);
if (holder != null) {
return holder;
}
holder = createHolderFragment(fm);
return holder;
}
這之后,因為我們傳入了一個ViewModel
的Class
對象,我們默認就可以通過反射的方式實例化對應的ViewModel
,并交給HolderFragment
中的ViewModelStore
容器存起來:
public <T extends ViewModel> T get(Class<T> modelClass) {
// 通過反射的方式實例化ViewModel,并存儲進ViewModelStore
viewModel = modelClass.getConstructor(Application.class).newInstance(mApplication);
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}
3.在對應的作用域內,保正只生產出對應的唯一實例
如何保證在不同的Fragment中,通過以下代碼生成同一個ViewModel的實例呢?
public class AFragment extends Fragment {
private CommonViewModel model;
public void onActivityCreated() {
model = ViewModelProviders.of(getActivity()).get(CommonViewModel.class);
}
}
public class BFragment extends Fragment {
private CommonViewModel model;
public void onActivityCreated() {
model = ViewModelProviders.of(getActivity()).get(CommonViewModel.class);
}
}
其實很簡單,只需要在上一步實例化ViewModel
的get()
方法中加一個判斷就行了:
public <T extends ViewModel> T get(Class<T> modelClass) {
// 先從ViewModelStore容器中去找是否存在ViewModel的實例
ViewModel viewModel = mViewModelStore.get(key);
// 若ViewModel已經存在,就直接返回
if (modelClass.isInstance(viewModel)) {
return (T) viewModel;
}
// 若不存在,再通過反射的方式實例化ViewModel,并存儲進ViewModelStore
viewModel = modelClass.getConstructor(Application.class).newInstance(mApplication);
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}
現在,我們成功實現了預期的功能——事實上,上文中的代碼正是ViewModel
官方核心部分功能的源碼,甚至默認ViewModel
實例化的API也沒有任何改變:
CommonViewModel viewModel = ViewModelProviders.of(activity).get(CommonViewModel.class);
當然,因為篇幅所限,我將源碼進行了簡單的刪減,同時沒有講述構造方法中帶參數的ViewModel
的實例化方式,但對于目前已經掌握了設計思想和原理的你,學習這些API的使用幾乎不費吹灰之力。
總結與思考
ViewModel
是一個設計非常精巧的組件,它功能并不復雜,相反,它簡單的難以置信,你甚至只需要了解實例化ViewModel
的API如何調用就行了。
同時,它的背后摻雜的思想和理念是值得去反復揣度的。比如,如何保證對狀態的規范化管理?如何將純粹的業務代碼通過良好的設計下沉到ViewModel
中?對于非常復雜的界面,如何將各種各樣的功能抽象為數據狀態進行解耦和復用?隨著MVVM開發的深入化,這些問題都會一個個浮出水面,這時候ViewModel
組件良好的設計和這些不起眼的小特性就隨時有可能成為璀璨奪目的閃光點,幫你攻城拔寨。
--------------------------廣告分割線------------------------------
系列文章
爭取打造 Android Jetpack 講解的最好的博客系列:
Android Jetpack 實戰篇: