Android 官方架構元件 ViewModel:從前世今生到追本溯源
2017年的Google I/O大會上,Google推出了一系列譬如 ofollow,noindex">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
也會自動重新整理列表:

對於開發者來講,在開發過程中可以大幅減少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()
方法釋放對應資源:

這樣看來, 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
元件良好的設計和這些不起眼的小特性就隨時有可能成為璀璨奪目的閃光點,幫你攻城拔寨。
--------------------------廣告分割線------------------------------
關於我
Hello,我是 卻把清梅嗅 ,如果您覺得文章對您有價值,歡迎 :heart:,也歡迎關注我的部落格或者 Github 。
如果您覺得文章還差了那麼點東西,也請通過 關注 督促我寫出更好的文章——萬一哪天我進步了呢?