如何構建 Android MVVM 應用程式

發表於2016-06-25

1、概述

Databinding 是一種框架,MVVM是一種模式,兩者的概念是不一樣的。我的理解DataBinding是一個實現資料和UI繫結的框架,只是一個實現MVVM模式的工具。ViewModel和View可以通過DataBinding來實現單向繫結和雙向繫結,這套UI和資料之間的動態監聽和動態更新的框架Google已經幫我們做好了。在MVVM模式中ViewModel和View是用繫結關係來實現的,所以有了DataBinding 使我們構建Android MVVM 應用程式成為可能。
之前看了很多關於DataBinding的部落格和相關的一些Demo,大多數就是往xml佈局檔案傳入一些資料,然後把這些資料繫結到控制元件上( 如TextView binding:text=“@{user.name} ),接著在這些控制元件上(如Button binding:setOnClickListener=”@{user.listener}”) 設定一些事件到控制元件上,基本講述都是DataBinding的基本用法。但是並沒有人告訴你把一個onClickListener 寫到一個類並把這個listener繫結到xml裡面上是不是不太好,也沒有人告訴你這個和xml佈局繫結的ViewModel類應該放哪些資料,應該做什麼事?應該如何設計?更是很少有博文來告訴你在Android 中如何通過Data Binding 去構建MVVM 的應用框架。這也就是是本篇文章的重點。接下來,我們先來看看什麼是MVVM,然後在一步一步來設計整個應用程式框架。

原始碼地址 https://github.com/Kelin-Hong/MVVMLight

2、MVC、MVP、MVVM

首先,我們先大致瞭解Android開發中常見的模式,以便我們更深入瞭解MVVM 模式。

  • MVC

    View:對應於xml佈局檔案
    Model:實體模型
    Controllor:對應於Activity業務邏輯,資料處理和UI處理

    從上面看起來各個元件的職責視乎還挺耦合MVC的,但是開啟Android的一個Activity檔案,一看一言難盡, Android中經常會出現數千行的Activity程式碼,究其原因,Android中純粹作為View的各個XML檢視功能太弱,Activity基本上都是View和Controller的合體,既要負責檢視的顯示又要加入控制邏輯,承擔的功能過多,程式碼量大也就不足為奇。所有更貼切的目前常規的開發說應該是View-Model 模式,大部分都是通過Activity的協調,連線,和處理邏輯的。

  • MVP

    View: 對應於Activity和xml,負責View的繪製以及與使用者互動
    Model: 依然是實體模型
    Presenter: 負責完成View於Model間的互動和業務邏輯

    在Android開發中MVP的設計思想用得比較多,利用MVP的設計模型可以把部分的邏輯的程式碼從Fragment和Activity業務的邏輯移出來,在Presenter中持有View(Activity或者Fragment)的引用,然後在Presenter呼叫View暴露的介面對檢視進行操作,這樣有利於把檢視操作和業務邏輯分開來。MVP能夠讓Activity成為真正的View而不是View和Control的合體,Activity只做UI相關的事。但是這個模式還是存在一些不好的地方,比較如說:

    • Activity需要實現各種跟UI相關的介面,同時要在Activity中編寫大量的事件,然後在事件處理中呼叫presenter的業務處理方法,View和Presenter只是互相持有引用並互相做回撥,程式碼不美觀。
    • 這種模式中,程式的主角是UI,通過UI事件的觸發對資料進行處理,更新UI就有考慮執行緒的問題。而且UI改變後牽扯的邏輯耦合度太高,一旦控制元件更改(比較TextView 替換 EditText等)牽扯的更新UI的介面就必須得換。
    • 複雜的業務同時會導致presenter層太大,程式碼臃腫的問題。
  • MVVM

    View: 對應於Activity和xml,負責View的繪製以及與使用者互動
    Model: 實體模型
    ViewModel: 負責完成View於Model間的互動,負責業務邏輯

    MVVM的目標和思想MVP類似,利用資料繫結(Data Binding)、依賴屬性(Dependency Property)、命令(Command)、路由事件(Routed Event)等新特性,打造了一個更加靈活高效的架構。

  • 資料驅動
    在MVVM中,以前開發模式中必須先處理業務資料,然後根據的資料變化,去獲取UI的引用然後更新UI,通過也是通過UI來獲取使用者輸入,而在MVVM中,資料和業務邏輯處於一個獨立的View Model中,ViewModel只要關注資料和業務邏輯,不需要和UI或者控制元件打交道。由資料自動去驅動UI去自動更新UI,UI的改變又同時自動反饋到資料,資料成為主導因素,這樣使得在業務邏輯處理只要關心資料,方便而且簡單很多。
  • 低耦合度
    MVVM模式中,資料是獨立於UI的,ViewModel只負責處理和提供資料,UI想怎麼處理資料都由UI自己決定,ViewModel 不涉及任何和UI相關的事也不持有UI控制元件的引用,即使控制元件改變(TextView 換成 EditText)ViewModel 幾乎不需要更改任何程式碼,專注自己的資料處理就可以了,如果是MVP遇到UI更改,就可能需要改變獲取UI的方式,改變更新UI的介面,改變從UI上獲取輸入的程式碼,可能還需要更改訪問UI物件的屬性程式碼等等。
  • 更新 UI
    在MVVM中,我們可以在工作執行緒中直接修改View Model的資料(只要資料是執行緒安全的),剩下的資料繫結框架幫你搞定,很多事情都不需要你去關心。
  • 團隊協作
    MVVM的分工是非常明顯的,由於View和View Model之間是鬆散耦合的。一個是處理業務和資料,一個是專門的UI處理。完全有兩個人分工來做,一個做UI(xml 和 Activity)一個寫ViewModel,效率更高。
  • 可複用性
    一個View Model複用到多個View中,同樣的一份資料,用不同的UI去做展示,對於版本迭代頻繁的UI改動,只要更換View層就行,對於如果想在UI上的做AbTest 更是方便的多。
  • 單元測試
    View Model裡面是資料和業務邏輯,View中關注的是UI,這樣的做測試是很方便的,完全沒有彼此的依賴,不管是UI的單元測試還是業務邏輯的單元測試,都是低耦合的。

通過上面對MVVM的簡述和其他兩種模式的對比,我們發現MVVM對比MVC和MVP來說還是存在比較大的優勢,雖然目前Android開發中可能真正在使用MVVM的很少,但是是值得我們去做一些探討和調研。

3、如何構建MVVM應用程式

1. 如何分工

構建MVVM框架首先要具體瞭解各個模組的分工,接下來我們來講解View,ViewModel,Model 的它們各自的職責所在。

  • View
    View層做的就是和UI相關的工作,我們只在XML和Activity或Fragment寫View層的程式碼,View層不做和業務相關的事,也就是我們的Activity 不寫和業務邏輯相關程式碼,也不寫需要根據業務邏輯來更新UI的程式碼,因為更新UI通過Binding實現,更新UI在ViewModel裡面做(更新繫結的資料來源即可),Activity 要做的事就是初始化一些控制元件(如控制元件的顏色,新增 RecyclerView 的分割線),Activity可以更新UI,但是更新的UI必須和業務邏輯和資料是沒有關係的,只是單純的根據點選或者滑動等事件更新UI(如 根據滑動顏色漸變、根據點選隱藏等單純UI邏輯),Activity(View層)是可以處理UI事件,但是處理的只是處理UI自己的事情,View層只處理View層的事。簡單的說:View層不做任何業務邏輯、不涉及運算元據、不處理資料、UI和資料嚴格的分開。
  • ViewModel
    ViewModel層做的事情剛好和View層相反,ViewModel 只做和業務邏輯和業務資料相關的事,不做任何和UI、控制元件相關的事,ViewModel 層不會持有任何控制元件的引用,更不會在ViewModel中通過UI控制元件的引用去做更新UI的事情。ViewModel就是專注於業務的邏輯處理,操作的也都是對資料進行操作,這些個資料來源繫結在相應的控制元件上會自動去更改UI,開發者不需要關心更新UI的事情。DataBinding 框架已經支援雙向繫結,這使得我們在可以通過雙向繫結獲取View層反饋給ViewModel層的資料,並進行操作。關於對UI控制元件事件的處理,我們也希望能把這些事件處理繫結到控制元件上,並把這些事件統一化,方便ViewModel對事件的處理和程式碼的美觀。為此我們通過BindingAdapter 對一些常用的事件做了封裝,把一個個事件封裝成一個個Command,對於每個事件我們用一個ReplyCommand<T>去處理就行了,ReplyCommand<T>會把可能你需要的資料帶給你,這使得我們處理事件的時候也只關心處理資料就行了,具體見MVVM Light Toolkit 使用指南的 Command 部分。再強調一遍ViewModel 不做和UI相關的事。
  • Model
    Model 的職責很簡單,基本就是實體模型(Bean)同時包括Retrofit 的Service ,ViewModel 可以根據Model 獲取一個Bean的Observable<Bean>( RxJava ),然後做一些資料轉換操作和對映到ViewModel 中的一些欄位,最後把這些欄位繫結到View層上。

2. 如何協作

關於協作,我們先來看下面的一張圖:

966283-78b410b9af8b18fa

圖 1

上圖反應了MVVM框架中各個模組的聯絡和資料流的走向,由上圖可知View和Model 直接是解耦的,是沒有直接聯絡的,也就是我之前說到的View 不做任何和業務邏輯和資料處理相關的事。我們從每個模組一一拆分來看。那麼我們重點就是下面的三個協作。

  • ViewModel與View的協作
  • ViewModel與Model的協作
  • ViewModel與ViewModel的協作
  • ViewModel與View的協作

966283-d2985f45c0c1e618

圖 2

圖 2 中ViewModel 和View 是通過繫結的方式連線在一起的,繫結的一種是資料繫結,一種是命令繫結。資料的繫結 DataBinding 已經提供好了,簡單的定義一些ObservableField就能把資料和控制元件繫結在一起了(如TextView的text屬性),但是DataBinding框架提供的不夠全面,比如說如何讓一個URL繫結到一個ImageView讓這個ImageView能自動去載入url指定的圖片,如何把資料來源和佈局模板繫結到一個ListView,讓ListView可以不需要去寫Adapter和ViewHolder 相關的東西,而只是通過簡單的繫結的方式把ViewModel的資料來源繫結到Xml的控制元件裡面就能快速的展示列表呢?這些就需要我們做一些工作和簡單的封裝。MVVM Light Toolkit 已經幫我們做了一部分的工作,詳情可以檢視MVVM Light Toolkit 使用指南。關於事件繫結也是一樣,MVVM Light Toolkit 做了簡單的封裝,對於每個事件我們用一個ReplyCommand<T>去處理就行了,ReplyCommand<T>會把可能你需要的資料帶給你,這使得我們處理事件的時候也只關心處理資料就行了。

圖 1 中ViewModel的模組中我們可以看出ViewModel類下面一般包含下面5個部分:

  • Context (上下文)
  • Model (資料模型Bean)
  • Data Field (資料繫結)
  • Command (命令繫結)
  • Child ViewModel (子ViewModel)

我們先來看下示例程式碼,然後在一一講解5個部分是幹嘛用的:

  • Context
    Context 是幹嘛用的呢,為什麼每個ViewModel都最好需要持了一個Context的引用呢?ViewModel 不做和UI相關的事,不操作控制元件,也不更新UI,那為什麼要有Context呢?原因主要有以下兩點,當然也有其他用處,呼叫工具類、幫助類可能需要context引數等:
    • 通過圖1中,我們發現ViewModel 通過傳參給Model 然後得到一個Observable<Bean>,其實這就是網路請求部分,做網路請求我們必須把Retrofit Service返回的Observable<Bean>繫結到Context的生命週期上,防止在請求回來時Activity已經銷燬等異常,其實這個Context的目的就是把網路請求繫結到當前頁面的生命週期中。
    • 在圖1中,我們可以看到兩個ViewModel 之間的聯絡是通過Messenger來做,這個Messenger 是需要用到Context,這個我們後續會講解。
  • Model
    Model 是什麼呢,其實就是資料原型,也就是我們用Json轉過來的Java Bean,我們可能都知道,ViewModel要把資料對映到View中可能需要大量對Model的資料拷貝,拿Model 的欄位去生成對應的ObservableField(我們不會直接拿Model的資料去做展示),這裡其實是有必要在一個ViewModel 保留原始的Model引用,這對於我們是非常有用的,因為可能使用者的某些操作和輸入需要我們去改變資料來源,可能我們需要把一個Bean 從列表頁點選後傳給詳情頁,可能我們需要把這個model 當做表單提交到伺服器。這些都需要我們的ViewModel
    持有相應的model。
  • Data Field (資料繫結)
    Data Field 就是需要繫結到控制元件上的ObservableField欄位, 無可厚非這是ViewModel的必須品。這個沒有什麼好說,但是這邊有一個建議:
    這些欄位是可以稍微做一下分類和包裹的,比如說可能一些欄位繫結到控制元件的一些Style屬性上(如果說:長度,顏色,大小)這些根據業務邏輯的變化而動態去更改的,對於著一類針對View Style的的欄位可以宣告一個ViewStyle類包裹起來,這樣整個程式碼邏輯會更清晰一些,不然ViewModel裡面可能欄位氾濫,不易管理和閱讀性較差。而對於其他一些欄位,比如說title,imageUrl,name這些屬於資料來源型別的欄位,這些欄位也叫資料欄位,是和業務邏輯息息相關的,這些欄位可以放在一塊。
  • Command (命令繫結)
    Command (命令繫結)說白了就是對事件的處理(下拉重新整理,載入更多,點選,滑動等事件處理),我們之前處理事件是拿到UI控制元件的引用,然後設定Listener,這些Listener 其實就是Command,但是考慮到在一個ViewModel 寫各種Listener 並不美觀,可能實現一個Listener就需要實現多個方法,但是我們可能只想要其中一個有用的方法實現就好了。同時實現Listener 會拿到UI的引用,可能會去做一些和UI相關的事情,這和我們之前說的ViewModel 不持有控制元件的引用,ViewModel不更改UI 有相悖。更重要一點是實現一個Listener 可能需要寫一些UI邏輯才能最終獲取我們想要的,簡單一點的比如說,你想要監聽ListView滑到最底部然後觸發載入更多的事件,這時候你就要在ViewModel裡面寫一個OnScrollListener,然後在裡面的onScroll方法中做計算,計算什麼時候ListView滑動底部了,其實ViewModel的工作並不想去處理這些事件,它專注做的應該是業務邏輯和資料處理,如果有一個東西它不需要你自己去計算是否滑到底部,而是在滑動底部自動觸發一個Command,同時把當前列表的總共的item數量返回給你,方便你通過 page=itemCount/LIMIT+1去計算出應該請求伺服器哪一頁的資料那該多好啊。MVVM Light Toolkit 幫你實現了這一點:

接著在XML 佈局檔案中通過bind:onLoadMoreCommand繫結上去就行了

  • 具體想了解更多請檢視 MVVM Light Toolkit 使用指南,裡面有比較詳細的講解Command的使用。當然Command並不是必須的,你完全可以依照你的習慣和喜好在ViewModel 寫Listener,不過使用Command 可以使你的ViewModel 更簡潔易讀,你也可以自己定義更多的Command,自己定義其他功能Command,那麼ViewModel的事件處理都是託管ReplyCommand<T>來處理,這樣的程式碼看起來會特別美觀和清晰。
  • Child ViewModel (子ViewModel)
    子ViewModel 的概念就是在ViewModel 裡面巢狀其他的ViewModel,這種場景還是很常見的。比如說你一個Activity裡面有兩個Fragment,ViewModel 是以業務劃分的,兩個Fragment做的業務不一樣,自然是由兩個ViewModel來處理,Activity 本身可能就有個ViewModel 來做它自己的業務,這時候Activity的這個ViewModel裡面可能包含了兩個Fragment分別的ViewModel。這就是巢狀的子ViewModel。還有另外一種就是對於AdapterView 如ListView RecyclerView,ViewPager等。

它們的每個Item 其實就對應於一個ViewModel,然後在當前的ViewModel 通過ObservableList<ItemViewModel>持有引用(如上述程式碼),這也是很常見的巢狀的子ViewModel。我們其實還建議,如果一個頁面業務非常複雜,不要把所有邏輯都寫在一個ViewModel,可以把頁面做業務劃分,把不同的業務放到不同的ViewModel,然後整合到一個總的ViewModel,這樣做起來可以使我們的程式碼業務清晰,簡短意賅,也方便後人的維護。

總得來說ViewModel 和View 之前僅僅只有繫結的關係,View層需要的屬性和事件處理都是在xml裡面繫結好了,ViewModel層不會去操作UI,只會運算元據,ViewModel只是根據業務要求處理資料,這些資料自動對映到View層控制元件的屬性上。關於ViewModel類中包含哪些模組和欄位,這個需要開發者自己去衡量,這邊建議ViewModel 不要引入太多的成員變數,成員變數最好只有上面的提到的5種(context、model、…),能不進入其他型別的變數就儘量不要引進來,太多的成員變數對於整個程式碼結構破壞很大,後面維護的人要時刻關心成員變數什麼時候被初始化,什麼時候被清掉,什麼時候被賦值或者改變,一個細節不小心可能就出現潛在的Bug。太多不清晰定義的成員變數又沒有註釋的程式碼是很難維護的。

  • ViewModel與Model的協作
    從圖1 中,Model 是通過Retrofit 去獲取網路資料的,返回的資料是一個Observable<Bean>( RxJava ),Model 層其實做的就是這些。那麼ViewModel 做的就是通過傳引數到Model層獲取到網路資料(資料庫同理)然後把Model的部分資料對映到ViewModel的一些欄位(ObservableField),並在ViewModel 保留這個Model的引用,我們來看下這一塊的大致程式碼(程式碼涉及到簡單RxJava,如看不懂可以查閱入門一下):

  • 以上程式碼基本把註釋補全了,基本思路比較清晰,,Rxjava涉及的操作符都是比較基本的,如有不懂,可以稍微去入門,之後的原始碼裡面ViewModel資料邏輯處理都是用Rxjava做,所以需要提前學習一下方便你看懂原始碼。

    注:我們推薦使用MVVM 和 RxJava一塊使用,雖然兩者皆有觀察者模式的概念,但是我們RxJava不使用在針對View的監聽,更多是業務資料流的轉換和處理。DataBinding框架其實是專用於View-ViewModel的動態繫結的,它使得我們的ViewModel 只需要關注資料,而RxJava 提供的強大資料流轉換函式剛好可以用來處理ViewModel中的種種資料,得到很好的用武之地,同時加上Lambda表示式結合的鏈式程式設計,使ViewModel 的程式碼非常簡潔同時易讀易懂。

  • ViewModel與ViewModel的協作
    在圖 1 中 我們看到兩個ViewModel 之間用一條虛線連線著,中間寫著Messenger,Messenger 可以理解是一個全域性訊息通道,引入messenger最主要的目的就實現ViewModel和ViewModel的通訊,也可以用做View和ViewModel的通訊,但是並不推薦這樣做。ViewModel主要是用來處理業務和資料的,每個ViewModel都有相應的業務職責,但是在業務複雜的情況下,可能存在交叉業務,這時候就需要ViewModel和ViewModel交換資料和通訊,這時候一個全域性的訊息通道就很重要的。關於Messenger 的詳細使用方法可以參照 MVVM Light Toolkit 使用指南的 Messenger 部分,這邊給出一個簡單的例子僅供參考:
    場景是這樣的,你的MainActivity對應一個MainViewModel,MainActivity 裡面除了自己的內容還包含一個Fragment,這個Fragment 的業務處理對應於一個FragmentViewModel,FragmentViewModel請求伺服器並獲取資料,剛好這個資料MainViewModel也需要用到,我們不可能在MainViewModel重新請求資料,這樣不太合理,這時候就需要把資料傳給MainViewModel,那麼應該怎麼傳,彼此沒有引用或者回撥。那麼只能通過全域性的訊息通道Messenger。FragmentViewModel 獲取訊息後通知MainViewModel 並把資料傳給它:

MainViewModel 接收訊息並處理:

在MainActivity onDestroy 取消註冊就行了(不然導致記憶體洩露)

  • 當然上面的例子也只是簡單的說明下,Messenger可以用在很多場景,通知,廣播都可以,不一定要傳資料,在一定條件下也可以用在View層和ViewModel 上的通訊和廣播。運用範圍特別廣,需要開發者結合實際的業務中去做更深層次的挖掘。

4、總結和原始碼

  • 本篇博文講解主要是一些個人開發過程中總結的Android MVVM構建思想,更多是理論上各個模組如何分工,程式碼如何設計,雖然現在業界使用Android MVVM模式開發還比較少,但是隨著DataBinding 1.0 的釋出,相信在Android MVVM 這塊領域會更多的人來嘗試,剛好最近用MVVM開發了一段時間,有點心得,寫出來僅供參考。
  • 文中講解的過程程式碼比較少,程式碼用到了自己開發的一個MVVM Light Toolkit 庫,而且還是RxJava + Lambda 的程式碼,估計很多人看著都暈菜了,這邊會把原始碼公佈出來。如果你還沒有嘗試過用RxJava+Retrofit+DataBinding 構建Android MVVM 應用程式,那麼你可以試著看一下這邊的原始碼並且做一下嘗試,說不定你會喜歡上這樣的開發框架。
  • 關於MVVM Light Toolkit 只是一個工具庫,主要目的是更快捷方便的構建Android MVVM應用程式,在裡面新增了一些控制元件額外屬性和做了一些事件的封裝,同時引進了全域性訊息通道Messenger,用起來確實非常方便,你可以嘗試一下,當然還有不少地方沒有完善和優化,後續也會不斷更新和優化,如果不能達到你的業務需求時,你也可以自己新增自己需要的屬性和事件。如果想更深入瞭解MVVM Light Toolkit 請看我這篇博文 MVVM Light Toolkit 使用指南
  • 原始碼地址 https://github.com/Kelin-Hong/MVVMLight
    • library —> library是MVVM Light Toolkit 的原始碼,原始碼很簡單,感興趣的同學可以看看,沒什麼多少的技術難度,可以根據自己的需求,新增更多的控制元件的屬性和事件繫結。
    • sample —> 本文涉及的程式碼均處出於這個專案,sample 一個知乎日報的App的簡單實現,程式碼包含了一大部分 MVVM Light Toolkit 的使用場景,(Data、Command、Messenger均有涉及),同時sample嚴格按照博文闡述的MVVM的設計思想開發的,對理解本文有很大的幫助,歡迎clone下來看看。
      Sample 截圖

966283-e86159db8c9114b0

相關文章