2017年的Google I/O大會上,Google推出了一系列譬如 Lifecycle、ViewModel、LiveData等一系列 更適合用於MVVM模式開發 的架構組件。php
本文的主角就是 ViewModel ,也許有朋友會提出質疑:前端
ViewModel 這麼簡單的東西,從API的使用到源碼分析,相關內容都爛大街了,你這篇文章還能翻出什麼花來?java
我沒法反駁,事實上,閱讀本文的您可能對MVVM的代碼已經 得心應手,甚至是經歷了完整項目的洗禮,但我依然想作一次大膽地寫做嘗試—— 即便對於MVVM模式的思想噗之以鼻,或者已經熟練使用MVVM,本文也儘可能讓您有所收穫,至少閱讀體驗不那麼枯燥。android
ViewModel,或者說 MVVM (Model-View-ViewModel),並不是是一個新鮮的詞彙,它的定義最先起源於前端,表明着 數據驅動視圖 的思想。git
好比說,咱們能夠經過一個String
類型的狀態來表示一個TextView
,同理,咱們也能夠經過一個List<T>
類型的狀態來維護一個RecyclerView
的列表——在實際開發中咱們經過觀察這些數據的狀態,來維護UI的自動更新,這就是 數據驅動視圖(觀察者模式)。github
每當String
的數據狀態發生變動,View層就能檢測並自動執行UI的更新,同理,每當列表的數據源List<T>
發生變動,RecyclerView
也會自動刷新列表:編程
對於開發者來說,在開發過程當中能夠大幅減小UI層和Model層相互調用的代碼,轉而將更多的重心投入到業務代碼的編寫。數組
ViewModel 的概念就是這樣被提出來的,我對它的形容相似一個 狀態存儲器 , 它存儲着UI中各類各樣的狀態, 以 登陸界面 爲例,咱們很容易想到最簡單的兩種狀態 :架構
class LoginViewModel {
val username: String // 用戶名輸入框中的內容
val password: String // 密碼輸入框中的內容
}
複製代碼
先不糾結於代碼的細節,如今咱們知道了ViewModel的重心是對 數據狀態的維護。接下來咱們來看看,在17年以前Google尚未推出ViewModel組件以前,Android領域內MVVM 百花齊放的各類形態 吧。app
說到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的我來說,實在是看得眼花繚亂,在學習接觸的過程當中,我也不可避免的走了許多彎路。
咱們都知道Google在去年的 I/O 大會很是隆重地推出了一系列的 架構組件, ViewModel正是其中之一,也是本文的主角。
有趣的是,相比較於惹眼的 Lifecycle
和 LiveData
, ViewModel
顯得很是低調,它主要提供了這些特性:
Activity
、Fragment
等UI組件之間的通訊若是讓我直接吹捧ViewModel
多麼多麼優秀,我會很是犯難,由於它表面展示的這些功能實在不夠惹眼,可是有幸截止目前爲止,我花費了一些筆墨闡述了ViewModel
在這以前的故事——它們是接下來正文不可缺乏的鋪墊。
也許您還沒有意識到,在官方的ViewModel
發佈以前,MVVM開發模式中,ViewModel層的一些窘境,但實際上我已經盡力經過敘述的方式將這些問題描述出來:
在官方的ViewModel
發佈以前,ViewModel
層的基類多種多樣,內部的依賴和公共邏輯更是五花八門。新的ViewModel
組件直接對ViewModel
層進行了標準化的規範,即便用ViewModel
(或者其子類AndroidViewModel
)。
同時,Google官方建議ViewModel
儘可能保證 純的業務代碼,不要持有任何View層(Activity
或者Fragment
)或Lifecycle
的引用,這樣保證了ViewModel
內部代碼的可測試性,避免由於Context
等相關的引用致使測試代碼的難以編寫(好比,MVP中Presenter層代碼的測試就須要額外成本,好比依賴注入或者Mock,以保證單元測試的進行)。
由系統響應用戶交互或者重建組件,用戶沒法操控。當組件被銷燬並重建後,原來組件相關的數據也會丟失——最簡單的例子就是屏幕的旋轉,若是數據類型比較簡單,同時數據量也不大,能夠經過onSaveInstanceState()
存儲數據,組件重建以後經過onCreate()
,從中讀取Bundle
恢復數據。但若是是大量數據,不方便序列化及反序列化,則上述方法將不適用。
ViewModel
的擴展類則會在這種狀況下自動保留其數據,若是Activity
被從新建立了,它會收到被以前相同ViewModel
實例。當所屬Activity
終止後,框架調用ViewModel
的onCleared()
方法釋放對應資源:
這樣看來,ViewModel
是有必定的 做用域 的,它不會在指定的做用域內生成更多的實例,從而節省了更多關於 狀態維護(數據的存儲、序列化和反序列化)的代碼。
ViewModel
在對應的 做用域 內保持生命週期內的 局部單例,這就引起一個更好用的特性,那就是Fragment
、Activity
等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提供的這些特性,爲何感受互相之間沒有聯繫呢?
這就引起下面這個問題,那就是:
這些特性的本質是什麼?
ViewModel
層的根本職責,就是負責維護UI的狀態,追根究底就是維護對應的數據——畢竟,不管是MVP仍是MVVM,UI的展現就是對數據的渲染。
ViewModel
的基類,並建議經過持有LiveData
維護保存數據的狀態;ViewModel
不會隨着Activity
的屏幕旋轉而銷燬,減小了維護狀態的代碼成本(數據的存儲和讀取、序列化和反序列化);Fragment
維護相同的數據狀態,極大減小了UI組件之間的數據傳遞的代碼成本。如今咱們對於ViewModel
的職責和思想都有了必定的瞭解,按理說接下來咱們應該闡述如何使用ViewModel
了,但我想先等等,由於我以爲相比API的使用,掌握其本質的思想會讓你在接下來的代碼實踐中如魚得水。
經過庫提供的API接口做爲開始,閱讀其內部的源碼,這是標準掌握代碼內部原理的思路,這種方式的時間成本極高,即便有相關源碼分析的博客進行引導,文章中大片大片的源碼和註釋也足以讓人望而卻步,因而我理所固然這麼想:
先學會怎麼用,再抽空系統學習它的原理和思想吧......
發現沒有,這和上學時候的學習方式居然截然相反,甚至說本末倒置也不奇怪——任何一個物理或者數學公式,在使用它作題以前,對它背後的基礎理論都應該是優先去系統性學習掌握的(好比,數學公式的學習通常都須要先經過必定方式推導和證實),這樣我才能拿着這個知識點對課後的習題觸類旁通。這就比如,若是一個老師直接告訴你一個公式,而後啥都不說讓你作題,這個老師必定是不合格的。
我也不是很喜歡大篇幅地複製源碼,我準備換個角度,站在Google工程師的角度看看怎麼樣設計出一個ViewModel
。
如今咱們是Google工程師,讓咱們再回顧一下ViewModel
應起到的做用:
ViewModel
的基類;ViewModel
不會隨着Activity
的屏幕旋轉而銷燬;這個簡直太簡單了:
public abstract class ViewModel {
protected void onCleared() {
}
}
複製代碼
咱們定義一個抽象的ViewModel
基類,並定義一個onCleared()
方法以便於釋放對應的資源,接下來,開發者只須要讓他的XXXViewModel
繼承這個抽象的ViewModel
基類便可。
這是一個很神奇的功能,但它的實現方式卻很是簡單,咱們先了解這樣一個知識點:
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;
}
複製代碼
如何保證在不一樣的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 實戰篇:
Hello,我是卻把清梅嗅,若是您以爲文章對您有價值,歡迎 ❤️,也歡迎關注個人我的博客或者Github。
若是您以爲文章還差了那麼點東西,也請經過關注督促我寫出更好的文章——萬一哪天我進步了呢?