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

卻把清梅嗅發表於2018-12-03

爭取打造 Android Jetpack 講解的最好的部落格系列

概述

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也會自動重新整理列表:

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

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

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

這樣看來,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的原理分析中,程式碼參照版本為v1.1.1,多謝評論區提醒,特此標註。

總結與思考

ViewModel是一個設計非常精巧的元件,它功能並不複雜,相反,它簡單的難以置信,你甚至只需要瞭解例項化ViewModel的API如何呼叫就行了。

同時,它的背後摻雜的思想和理念是值得去反覆揣度的。比如,如何保證對狀態的規範化管理?如何將純粹的業務程式碼通過良好的設計下沉到ViewModel中?對於非常複雜的介面,如何將各種各樣的功能抽象為資料狀態進行解耦和複用?隨著MVVM開發的深入化,這些問題都會一個個浮出水面,這時候ViewModel元件良好的設計和這些不起眼的小特性就隨時有可能成為璀璨奪目的閃光點,幫你攻城拔寨。

--------------------------廣告分割線------------------------------

關於我

Hello,我是卻把清梅嗅,如果您覺得文章對您有價值,歡迎 ❤️,也歡迎關注我的部落格或者Github

如果您覺得文章還差了那麼點東西,也請通過關注督促我寫出更好的文章——萬一哪天我進步了呢?

相關文章