Android官方架構組件ViewModel:從前世今生到追本溯源

轉載: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正是其中之一,也是本文的主角。

有趣的是,相比較于惹眼的 LifecycleLiveDataViewModel 顯得非常低調,它主要提供了這些特性:

  • 配置更改期間自動保留其數據 (比如屏幕的橫豎旋轉)
  • ActivityFragment等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終止后,框架調用ViewModelonCleared()方法釋放對應資源:

[圖片上傳失敗...(image-c38c25-1604915081141)]

這樣看來,ViewModel是有一定的 作用域 的,它不會在指定的作用域內生成更多的實例,從而節省了更多關于 狀態維護(數據的存儲、序列化和反序列化)的代碼。

ViewModel在對應的 作用域 內保持生命周期內的 局部單例,這就引發一個更好用的特性,那就是FragmentActivity等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就可以使當前FragmentActivity重建時存活下來

這似乎和我們的功能非常吻合,于是我們不禁這樣想,可不可以讓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,并讓HolderFragmentViewModel存進HashMap中。

這樣,具有生命周期的Activity在旋轉屏幕銷毀重建時,因為不可見的HolderFragment中的ViewModelStore容器持有了ViewModelViewModel和其內部的狀態并沒有被回收銷毀。

這需要一個條件,在實例化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;
}

這之后,因為我們傳入了一個ViewModelClass對象,我們默認就可以通過反射的方式實例化對應的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);
    }
}

其實很簡單,只需要在上一步實例化ViewModelget()方法中加一個判斷就行了:

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 實戰篇

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容