Android MVVM 應(yīng)用框架構(gòu)建過(guò)程詳解
概述
說(shuō)到Android MVVM,相信大家都會(huì)想到Google 2015年推出的DataBinding框架。然而兩者的概念是不一樣的,不能混為一談。MVVM是一種架構(gòu)模式,而DataBinding是一個(gè)實(shí)現(xiàn)數(shù)據(jù)和UI綁定的框架,是構(gòu)建MVVM模式的一個(gè)工具。
之前看過(guò)很多關(guān)于Android MVVM的博客,但大多數(shù)提到的都是DataBinding的基本用法,很少有文章仔細(xì)講解在Android中是如何通過(guò)DataBinding去構(gòu)建MVVM的應(yīng)用框架的。View、ViewModel、Model每一層的職責(zé)如何?它們之間聯(lián)系怎樣、分工如何、代碼應(yīng)該如何設(shè)計(jì)?這是我寫這篇文章的初衷。
接下來(lái),我們先來(lái)看看什么是MVVM,然后再一步一步來(lái)設(shè)計(jì)整個(gè)MVVM框架。
MVC、MVP、MVVM
首先,我們先大致了解下Android開發(fā)中常見的模式。
MVC
View:XML布局文件。
Model:實(shí)體模型(數(shù)據(jù)的獲取、存儲(chǔ)、數(shù)據(jù)狀態(tài)變化)。
Controllor:對(duì)應(yīng)于Activity,處理數(shù)據(jù)、業(yè)務(wù)和UI。
從上面這個(gè)結(jié)構(gòu)來(lái)看,Android本身的設(shè)計(jì)還是符合MVC架構(gòu)的,但是Android中純粹作為View的XML視圖功能太弱,我們大量處理View的邏輯只能寫在Activity中,這樣Activity就充當(dāng)了View和Controller兩個(gè)角色,直接導(dǎo)致Activity中的代碼大爆炸。相信大多數(shù)Android開發(fā)者都遇到過(guò)一個(gè)Acitivty數(shù)以千行的代碼情況吧!所以,更貼切的說(shuō)法是,這個(gè)MVC結(jié)構(gòu)最終其實(shí)只是一個(gè)Model-View(Activity:View&Controller)的結(jié)構(gòu)。
MVP
View: 對(duì)應(yīng)于Activity和XML,負(fù)責(zé)View的繪制以及與用戶的交互。
Model: 依然是實(shí)體模型。
Presenter: 負(fù)責(zé)完成View與Model間的交互和業(yè)務(wù)邏輯。
前面我們說(shuō),Activity充當(dāng)了View和Controller兩個(gè)角色,MVP就能很好地解決這個(gè)問(wèn)題,其核心理念是通過(guò)一個(gè)抽象的View接口(不是真正的View層)將Presenter與真正的View層進(jìn)行解耦。Persenter持有該View接口,對(duì)該接口進(jìn)行操作,而不是直接操作View層。這樣就可以把視圖操作和業(yè)務(wù)邏輯解耦,從而讓Activity成為真正的View層。
但MVP也存在一些弊端:
- Presenter(以下簡(jiǎn)稱P)層與View(以下簡(jiǎn)稱V)層是通過(guò)接口進(jìn)行交互的,接口粒度不好控制。粒度太小,就會(huì)存在大量接口的情況,使代碼太過(guò)碎版化;粒度太大,解耦效果不好。同時(shí)對(duì)于UI的輸入和數(shù)據(jù)的變化,需要手動(dòng)調(diào)用V層或者P層相關(guān)的接口,相對(duì)來(lái)說(shuō)缺乏自動(dòng)性、監(jiān)聽性。如果數(shù)據(jù)的變化能自動(dòng)響應(yīng)到UI、UI的輸入能自動(dòng)更新到數(shù)據(jù),那該多好!
- MVP是以UI為驅(qū)動(dòng)的模型,更新UI都需要保證能獲取到控件的引用,同時(shí)更新UI的時(shí)候要考慮當(dāng)前是否是UI線程,也要考慮Activity的生命周期(是否已經(jīng)銷毀等)。
- MVP是以UI和事件為驅(qū)動(dòng)的傳統(tǒng)模型,數(shù)據(jù)都是被動(dòng)地通過(guò)UI控件做展示,但是由于數(shù)據(jù)的時(shí)變性,我們更希望數(shù)據(jù)能轉(zhuǎn)被動(dòng)為主動(dòng),希望數(shù)據(jù)能更有活性,由數(shù)據(jù)來(lái)驅(qū)動(dòng)UI。
- V層與P層還是有一定的耦合度。一旦V層某個(gè)UI元素更改,那么對(duì)應(yīng)的接口就必須得改,數(shù)據(jù)如何映射到UI上、事件監(jiān)聽接口這些都需要轉(zhuǎn)變,牽一發(fā)而動(dòng)全身。如果這一層也能解耦就更好了。
- 復(fù)雜的業(yè)務(wù)同時(shí)也可能會(huì)導(dǎo)致P層太大,代碼臃腫的問(wèn)題依然不能解決。
MVVM
View: 對(duì)應(yīng)于Activity和XML,負(fù)責(zé)View的繪制以及與用戶交互。
Model: 實(shí)體模型。
ViewModel: 負(fù)責(zé)完成View與Model間的交互,負(fù)責(zé)業(yè)務(wù)邏輯。
MVVM的目標(biāo)和思想與MVP類似,利用數(shù)據(jù)綁定(Data Binding)、依賴屬性(Dependency Property)、命令(Command)、路由事件(Routed Event)等新特性,打造了一個(gè)更加靈活高效的架構(gòu)。
數(shù)據(jù)驅(qū)動(dòng)
在常規(guī)的開發(fā)模式中,數(shù)據(jù)變化需要更新UI的時(shí)候,需要先獲取UI控件的引用,然后再更新UI。獲取用戶的輸入和操作也需要通過(guò)UI控件的引用。在MVVM中,這些都是通過(guò)數(shù)據(jù)驅(qū)動(dòng)來(lái)自動(dòng)完成的,數(shù)據(jù)變化后會(huì)自動(dòng)更新UI,UI的改變也能自動(dòng)反饋到數(shù)據(jù)層,數(shù)據(jù)成為主導(dǎo)因素。這樣MVVM層在業(yè)務(wù)邏輯處理中只要關(guān)心數(shù)據(jù),不需要直接和UI打交道,在業(yè)務(wù)處理過(guò)程中簡(jiǎn)單方便很多。
低耦合度
MVVM模式中,數(shù)據(jù)是獨(dú)立于UI的。
數(shù)據(jù)和業(yè)務(wù)邏輯處于一個(gè)獨(dú)立的ViewModel中,ViewModel只需要關(guān)注數(shù)據(jù)和業(yè)務(wù)邏輯,不需要和UI或者控件打交道。UI想怎么處理數(shù)據(jù)都由UI自己決定,ViewModel不涉及任何和UI相關(guān)的事,也不持有UI控件的引用。即便是控件改變了(比如:TextView換成EditText),ViewModel也幾乎不需要更改任何代碼。它非常完美的解耦了View層和ViewModel,解決了上面我們所說(shuō)的MVP的痛點(diǎn)。
更新UI
在MVVM中,數(shù)據(jù)發(fā)生變化后,我們?cè)诠ぷ骶€程直接修改(在數(shù)據(jù)是線程安全的情況下)ViewModel的數(shù)據(jù)即可,不用再考慮要切到主線程更新UI了,這些事情相關(guān)框架都幫我們做了。
團(tuán)隊(duì)協(xié)作
MVVM的分工是非常明顯的,由于View和ViewModel之間是松散耦合的:一個(gè)是處理業(yè)務(wù)和數(shù)據(jù)、一個(gè)是專門的UI處理。所以,完全由兩個(gè)人分工來(lái)做,一個(gè)做UI(XML和Activity)一個(gè)寫ViewModel,效率更高。
可復(fù)用性
一個(gè)ViewModel可以復(fù)用到多個(gè)View中。同樣的一份數(shù)據(jù),可以提供給不同的UI去做展示。對(duì)于版本迭代中頻繁的UI改動(dòng),更新或新增一套View即可。如果想在UI上做A/B Testing,那MVVM是你不二選擇。
單元測(cè)試
有些同學(xué)一看到單元測(cè)試,可能腦袋都大。是啊,寫成一團(tuán)漿糊的代碼怎么可能做單元測(cè)試?如果你們以代碼太爛無(wú)法寫單元測(cè)試而逃避,那可真是不好的消息了。這時(shí)候,你需要MVVM來(lái)拯救。
我們前面說(shuō)過(guò)了,ViewModel層做的事是數(shù)據(jù)處理和業(yè)務(wù)邏輯,View層中關(guān)注的是UI,兩者完全沒(méi)有依賴。不管是UI的單元測(cè)試還是業(yè)務(wù)邏輯的單元測(cè)試,都是低耦合的。在MVVM中數(shù)據(jù)是直接綁定到UI控件上的(部分?jǐn)?shù)據(jù)是可以直接反映出UI上的內(nèi)容),那么我們就可以直接通過(guò)修改綁定的數(shù)據(jù)源來(lái)間接做一些Android UI上的測(cè)試。
通過(guò)上面的簡(jiǎn)述以及模式的對(duì)比,我們可以發(fā)現(xiàn)MVVM的優(yōu)勢(shì)還是非常明顯的。雖然目前Android開發(fā)中可能真正在使用MVVM的很少,但是值得我們?nèi)プ鲆恍┨接懞驼{(diào)研。
如何構(gòu)建MVVM應(yīng)用框架
如何分工
構(gòu)建MVVM框架首先要具體了解各個(gè)模塊的分工。接下來(lái)我們來(lái)講解View、ViewModel、Model它們各自的職責(zé)所在。
View
View層做的就是和UI相關(guān)的工作,我們只在XML、Activity和Fragment寫View層的代碼,View層不做和業(yè)務(wù)相關(guān)的事,也就是我們?cè)贏ctivity不寫業(yè)務(wù)邏輯和業(yè)務(wù)數(shù)據(jù)相關(guān)的代碼,更新UI通過(guò)數(shù)據(jù)綁定實(shí)現(xiàn),盡量在ViewModel里面做(更新綁定的數(shù)據(jù)源即可),Activity要做的事就是初始化一些控件(如控件的顏色,添加RecyclerView的分割線),View層可以提供更新UI的接口(但是我們更傾向所有的UI元素都是通過(guò)數(shù)據(jù)來(lái)驅(qū)動(dòng)更改UI),View層可以處理事件(但是我們更希望UI事件通過(guò)Command來(lái)綁定)。簡(jiǎn)單地說(shuō):View層不做任何業(yè)務(wù)邏輯、不涉及操作數(shù)據(jù)、不處理數(shù)據(jù),UI和數(shù)據(jù)嚴(yán)格的分開。
ViewModel
ViewModel層做的事情剛好和View層相反,ViewModel只做和業(yè)務(wù)邏輯和業(yè)務(wù)數(shù)據(jù)相關(guān)的事,不做任何和UI相關(guān)的事情,ViewModel 層不會(huì)持有任何控件的引用,更不會(huì)在ViewModel中通過(guò)UI控件的引用去做更新UI的事情。ViewModel就是專注于業(yè)務(wù)的邏輯處理,做的事情也都只是對(duì)數(shù)據(jù)的操作(這些數(shù)據(jù)綁定在相應(yīng)的控件上會(huì)自動(dòng)去更改UI)。同時(shí)DataBinding框架已經(jīng)支持雙向綁定,讓我們可以通過(guò)雙向綁定獲取View層反饋給ViewModel層的數(shù)據(jù),并對(duì)這些數(shù)據(jù)上進(jìn)行操作。關(guān)于對(duì)UI控件事件的處理,我們也希望能把這些事件處理綁定到控件上,并把這些事件的處理統(tǒng)一化,為此我們通過(guò)BindingAdapter對(duì)一些常用的事件做了封裝,把一個(gè)個(gè)事件封裝成一個(gè)個(gè)Command,對(duì)于每個(gè)事件我們用一個(gè)ReplyCommand去處理就行了,ReplyCommand會(huì)把你可能需要的數(shù)據(jù)帶給你,這使得我們?cè)赩iewModel層處理事件的時(shí)候只需要關(guān)心處理數(shù)據(jù)就行了,具體見MVVM Light Toolkit 使用指南的 Command 部分。再?gòu)?qiáng)調(diào)一遍:ViewModel 不做和UI相關(guān)的事。
Model
Model層最大的特點(diǎn)是被賦予了數(shù)據(jù)獲取的職責(zé),與我們平常Model層只定義實(shí)體對(duì)象的行為截然不同。實(shí)例中,數(shù)據(jù)的獲取、存儲(chǔ)、數(shù)據(jù)狀態(tài)變化都是Model層的任務(wù)。Model包括實(shí)體模型(Bean)、Retrofit的Service ,獲取網(wǎng)絡(luò)數(shù)據(jù)接口,本地存儲(chǔ)(增刪改查)接口,數(shù)據(jù)變化監(jiān)聽等。Model提供數(shù)據(jù)獲取接口供ViewModel調(diào)用,經(jīng)數(shù)據(jù)轉(zhuǎn)換和操作并最終映射綁定到View層某個(gè)UI元素的屬性上。
如何協(xié)作
關(guān)于協(xié)作,我們先來(lái)看下面的一張圖:
上圖反映了MVVM框架中各個(gè)模塊的聯(lián)系和數(shù)據(jù)流的走向,我們從每個(gè)模塊一一拆分來(lái)看。那么我們重點(diǎn)就是下面的三個(gè)協(xié)作。
- ViewModel與View的協(xié)作。
- ViewModel與Model的協(xié)作。
- ViewModel與ViewModel的協(xié)作。
ViewModel與View的協(xié)作
圖2中ViewModel和View是通過(guò)綁定的方式連接在一起的,綁定分成兩種:一種是數(shù)據(jù)綁定,一種是命令綁定。數(shù)據(jù)的綁定DataBinding已經(jīng)提供好了,簡(jiǎn)單地定義一些ObservableField就能把數(shù)據(jù)和控件綁定在一起了(如TextView的text屬性),但是DataBinding框架提供的不夠全面,比如說(shuō)如何讓一個(gè)URL綁定到一個(gè)ImageView,讓這個(gè)ImageView能自動(dòng)去加載url指定的圖片,如何把數(shù)據(jù)源和布局模板綁定到一個(gè)ListView,讓ListView可以不需要去寫Adapter和ViewHolder相關(guān)的東西?這些就需要我們做一些工作和簡(jiǎn)單的封裝。MVVM Light Toolkit 已經(jīng)幫我們做了一部分的工作,詳情可以查看MVVM Light Toolkit 使用指南。關(guān)于事件綁定也是一樣,MVVM Light Toolkit 做了簡(jiǎn)單的封裝,對(duì)于每個(gè)事件我們用一個(gè)ReplyCommand去處理就行了,ReplyCommand會(huì)把可能需要的數(shù)據(jù)帶給你,這樣我們處理事件的時(shí)候也只關(guān)心處理數(shù)據(jù)就行了。
由圖1中ViewModel的模塊中我們可以看出ViewModel類下面一般包含下面5個(gè)部分:
- Context (上下文)
- Model (數(shù)據(jù)源 Java Bean)
- Data Field (數(shù)據(jù)綁定)
- Command (命令綁定)
- Child ViewModel (子ViewModel)
我們先來(lái)看下示例代碼,然后再一一講解5個(gè)部分是干嘛用的:
//contextprivate Activity context;//model(數(shù)據(jù)源 Java Bean)private NewsService.News news;private TopNewsService.News topNews;//數(shù)據(jù)綁定,綁定到UI的字段(data field)public final ObservableField<String> imageUrl = new ObservableField<>();public final ObservableField<String> html = new ObservableField<>();public final ObservableField<String> title = new ObservableField<>();// 一個(gè)變量包含了所有關(guān)于View Style 相關(guān)的字段public final ViewStyle viewStyle = new ViewStyle();//命令綁定(command)public final ReplyCommand onRefreshCommand = new ReplyCommand<>(() -> {
})public final ReplyCommand<Integer> onLoadMoreCommand = new ReplyCommand<>((itemCount) -> {
});//Child ViewModelpublic final ObservableList<NewItemViewModel> itemViewModel = new ObservableArrayList<>();/** * ViewStyle 關(guān)于控件的一些屬性和業(yè)務(wù)數(shù)據(jù)無(wú)關(guān)的Style 可以做一個(gè)包裹,這樣代碼比較美觀,
ViewModel 頁(yè)面也不會(huì)有太多太雜的字段。 **/public static class ViewStyle {
public final ObservableBoolean isRefreshing = new ObservableBoolean(true);
public final ObservableBoolean progressRefreshing = new ObservableBoolean(true);
}
Context
Context是干嘛用的呢,為什么每個(gè)ViewModel都最好需要持了一個(gè)Context的引用呢?ViewModel不處理和UI相關(guān)的事也不操作控件,更不更新UI,那為什么要有Context呢?原因主要有以下兩點(diǎn):
- 通過(guò)圖1中,然后得到一個(gè)Observable,其實(shí)這就是網(wǎng)絡(luò)請(qǐng)求部分。其實(shí)這就是網(wǎng)絡(luò)請(qǐng)求部分,做網(wǎng)絡(luò)請(qǐng)求我們必須把Retrofit Service返回的Observable綁定到Context的生命周期上,防止在請(qǐng)求回來(lái)時(shí)Activity已經(jīng)銷毀等異常,其實(shí)這個(gè)Context的目的就是把網(wǎng)絡(luò)請(qǐng)求綁定到當(dāng)前頁(yè)面的生命周期中。
- 在圖1中,我們可以看到兩個(gè)ViewModel之間的聯(lián)系是通過(guò)Messenger來(lái)做,這個(gè)Messenger是需要用到Context,這個(gè)我們后續(xù)會(huì)講解。
當(dāng)然,除此以外,調(diào)用工具類、幫助類有時(shí)候需要Context做為參數(shù)等也是原因之一。
Model (數(shù)據(jù)源)
Model是什么呢?其實(shí)就是數(shù)據(jù)源,可以簡(jiǎn)單理解是我們用JSON轉(zhuǎn)過(guò)來(lái)的Bean。ViewModel要把數(shù)據(jù)映射到UI中可能需要大量對(duì)Model的數(shù)據(jù)拷貝和操作,拿Model的字段去生成對(duì)應(yīng)的ObservableField然后綁定到UI(我們不會(huì)直接拿Model的數(shù)據(jù)去做綁定展示),這里是有必要在一個(gè)ViewModel保留原始的Model引用,這對(duì)于我們是非常有用的,因?yàn)榭赡苡脩舻哪承┎僮骱洼斎胄枰覀內(nèi)ジ淖償?shù)據(jù)源,可能我們需要把一個(gè)Bean在列表頁(yè)點(diǎn)擊后傳給詳情頁(yè),可能我們需要把這個(gè)Model當(dāng)做表單提交到服務(wù)器。這些都需要我們的ViewModel持有相應(yīng)的Model(數(shù)據(jù)源)。
Data Field(數(shù)據(jù)綁定)
Data Field就是需要綁定到控件上的ObservableField字段,這是ViewModel的必需品,這個(gè)沒(méi)有什么好說(shuō)。但是這邊有一個(gè)建議:
這些字段是可以稍微做一下分類和包裹的。比如說(shuō)可能一些字段是綁定到控件的一些Style屬性上(如長(zhǎng)度、顏色、大小),對(duì)于這類針對(duì)View Style的的字段可以聲明一個(gè)ViewStyle類包裹起來(lái),這樣整個(gè)代碼邏輯會(huì)更清晰一些,不然ViewModel里面可能字段泛濫,不易管理和閱讀性較差。而對(duì)于其他一些字段,比如說(shuō)title、imageUrl、name這些屬于數(shù)據(jù)源類型的字段,這些字段也叫數(shù)據(jù)字段,是和業(yè)務(wù)數(shù)據(jù)和邏輯息息相關(guān)的,這些字段可以放在一塊。
Command(命令綁定)
Command(命令綁定)簡(jiǎn)言之就是對(duì)事件的處理(下拉刷新、加載更多、點(diǎn)擊、滑動(dòng)等事件處理)。我們之前處理事件是拿到UI控件的引用,然后設(shè)置Listener,這些Listener其實(shí)就是Command。但是考慮到在一個(gè)ViewModel寫各種Listener并不美觀,可能實(shí)現(xiàn)一個(gè)Listener就需要實(shí)現(xiàn)多個(gè)方法,但是我們可能只想要其中一個(gè)有用的方法實(shí)現(xiàn)就好了。更重要一點(diǎn)是實(shí)現(xiàn)一個(gè)Listener可能需要寫一些UI邏輯才能最終獲取我們想要的。簡(jiǎn)單舉個(gè)例子,比如你想要監(jiān)聽ListView滑到最底部然后觸發(fā)加載更多的事件,這時(shí)候就要在ViewModel里面寫一個(gè)OnScrollListener,然后在里面的onScroll方法中做計(jì)算,計(jì)算什么時(shí)候ListView滑動(dòng)底部了。其實(shí)ViewModel的工作并不想去處理這些事件,它專注做的應(yīng)該是業(yè)務(wù)邏輯和數(shù)據(jù)處理,如果有一個(gè)東西不需要你自己去計(jì)算是否滑到底部,而是在滑動(dòng)底部自動(dòng)觸發(fā)一個(gè)Command,同時(shí)把當(dāng)前列表的總共的item數(shù)量返回給你,方便你通過(guò) page=itemCount/LIMIT+1去計(jì)算出應(yīng)該請(qǐng)求服務(wù)器哪一頁(yè)的數(shù)據(jù)那該多好啊。MVVM Light Toolkit 幫你實(shí)現(xiàn)了這一點(diǎn):
public final ReplyCommand<Integer> onLoadMoreCommand = new ReplyCommand<>((itemCount) -> {
int page=itemCount/LIMIT+1;
loadData(page.LIMIT)
);
接著在XML布局文件中通過(guò)bind:onLoadMoreCommand綁定上去就行了。
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent" android:layout_height="match_parent" bind:onLoadMoreCommand="@{viewModel.loadMoreCommand}"/>
具體想了解更多請(qǐng)查看 MVVM Light Toolkit 使用指南,里面有比較詳細(xì)地講解Command的使用。當(dāng)然Command并不是必須的,你完全可以依照自己的習(xí)慣和喜好在ViewModel寫Listener,不過(guò)使用Command可以使ViewModel更簡(jiǎn)潔易讀。你也可以自己定義更多的、其他功能的Command,那么ViewModel的事件處理都是托管ReplyCommand來(lái)處理,這樣的代碼看起來(lái)會(huì)比較美觀和清晰。Command只是對(duì)UI事件的一層隔離UI層的封裝,在事件觸發(fā)時(shí)把ViewModel層可能需要的數(shù)據(jù)傳給ViewModel層,對(duì)事件的處理做了統(tǒng)一化,是否使用的話,還是看你個(gè)人喜好了。
Child ViewModel(子ViewModel)
子ViewModel的概念就是在ViewModel里面嵌套其他的ViewModel,這種場(chǎng)景還是很常見的。比如說(shuō)你一個(gè)Activity里面有兩個(gè)Fragment,ViewModel是以業(yè)務(wù)劃分的,兩個(gè)Fragment做的業(yè)務(wù)不一樣,自然是由兩個(gè)ViewModel來(lái)處理,這時(shí)候Activity對(duì)應(yīng)的ViewModel里面可能包含了兩個(gè)Fragment各自的ViewModel,這就是嵌套的子ViewModel。還有另外一種就是對(duì)于AdapterView,如ListView RecyclerView、ViewPager等。
//Child ViewModelpublic final
ObservableList<ItemViewModel> itemViewModel = new ObservableArrayList<>();
它們的每個(gè)Item其實(shí)就對(duì)應(yīng)于一個(gè)ViewModel,然后在當(dāng)前的ViewModel通過(guò)ObservableList持有引用(如上述代碼),這也是很常見的嵌套的子ViewModel。我們其實(shí)還建議,如果一個(gè)頁(yè)面業(yè)務(wù)非常復(fù)雜,不要把所有邏輯都寫在一個(gè)ViewModel,可以把頁(yè)面做業(yè)務(wù)劃分,把不同的業(yè)務(wù)放到不同的ViewModel,然后整合到一個(gè)總的ViewModel,這樣做起來(lái)可以使我們的代碼業(yè)務(wù)清晰、簡(jiǎn)短意賅,也方便后人的維護(hù)。
總的來(lái)說(shuō),ViewModel和View之前僅僅只有綁定的關(guān)系,View層需要的屬性和事件處理都是在XML里面綁定好了,ViewModel層不會(huì)去操作UI,只是根據(jù)業(yè)務(wù)要求處理數(shù)據(jù),這些數(shù)據(jù)自動(dòng)映射到View層控件的屬性上。
關(guān)于ViewModel類中包含哪些模塊和字段,這個(gè)需要開發(fā)者自己去衡量,我們建議ViewModel不要引入太多的成員變量,成員變量最好只有上面的提到的5種(context、model……),能不引入其他類型的變量就盡量不要引進(jìn)來(lái),太多的成員變量對(duì)于整個(gè)代碼結(jié)構(gòu)破壞很大,后面維護(hù)的人要時(shí)刻關(guān)心成員變量什么時(shí)候被初始化、什么時(shí)候被清掉、什么時(shí)候被賦值或者改變,一個(gè)細(xì)節(jié)不小心可能就出現(xiàn)潛在的Bug。太多不清晰定義的成員變量又沒(méi)有注釋的代碼是很難維護(hù)的。
另外,我們會(huì)把UI控件的屬性和事件都通過(guò)XML(如bind:text=@{…})綁定。如果一個(gè)業(yè)務(wù)邏輯要彈一個(gè)Dialog,但是你又不想在ViewModel里面做彈窗的事(ViewModel不希望做UI相關(guān)的事)或者說(shuō)改變ActionBar上面的圖標(biāo)的顏色,改變ActionBar按鈕是否可點(diǎn)擊,這些都不是寫在XML里面(都是用Java代碼初始化的),如何對(duì)這些控件的屬性做綁定呢?我們先來(lái)看下代碼:
public class MainViewModel implements ViewModel {
....//true的時(shí)候彈出Dialog,false的時(shí)候關(guān)掉dialogpublic final ObservableBoolean isShowDialog = new ObservableBoolean();
....
.....
}// 在View層做一個(gè)對(duì)isShowDialog改變的監(jiān)聽public class MainActivity extends RxBasePmsActivity {private MainViewModel mainViewModel;@Overrideprotected void onCreate(Bundle savedInstanceState) {
.....
mainViewModel.isShowDialog.addOnPropertyChangedCallback(new android.databinding.Observable.OnPropertyChangedCallback() { @Override
public void onPropertyChanged(android.databinding.Observable sender, int propertyId) { if (mainViewModel.isShowDialog.get()) {
dialog.show();
} else {
dialog.dismiss();
}
}
});
}
...
}
簡(jiǎn)單地說(shuō)你可以對(duì)任意的ObservableField做監(jiān)聽,然后根據(jù)數(shù)據(jù)的變化做相應(yīng)UI的改變,業(yè)務(wù)層ViewModel只要根據(jù)業(yè)務(wù)處理數(shù)據(jù)就行,以數(shù)據(jù)來(lái)驅(qū)動(dòng)UI。
ViewModel與Model的協(xié)作
從圖1中,ViewModel通過(guò)傳參數(shù)到Model層獲取網(wǎng)絡(luò)數(shù)據(jù)(數(shù)據(jù)庫(kù)同理),然后把Model的部分?jǐn)?shù)據(jù)映射到ViewModel的一些字段(ObservableField),并在ViewModel保留這個(gè)Model的引用,我們來(lái)看下這一塊的大致代碼(代碼涉及簡(jiǎn)單的RxJava,如看不懂可以查閱入門一下):
//Model
private NewsDetail newsDetail; private void loadData(long id) {
// Observable<Bean> 用來(lái)獲取網(wǎng)絡(luò)數(shù)據(jù)
Observable<Notification<NewsDetailService.NewsDetail>> newsDetailOb =
RetrofitProvider.getInstance()
.create(NewsDetailService.class)
.getNewsDetail(id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) // 將網(wǎng)絡(luò)請(qǐng)求綁定到Activity 的生命周期
.compose(((ActivityLifecycleProvider) context).bindToLifecycle())
//變成 Notification<Bean> 使我們更方便處理數(shù)據(jù)和錯(cuò)誤
.materialize().share(); // 處理返回的數(shù)據(jù)
newsDetailOb.filter(Notification::isOnNext)
.map(n -> n.getValue())
// 給成員變量newsDetail 賦值,之前提到的5種變量類型中的一種(model類型)
.doOnNext(m -> newsDetail = m)
.subscribe(m -> initViewModelField(m)); // 網(wǎng)絡(luò)請(qǐng)求錯(cuò)誤處理
NewsListHelper.dealWithResponseError(
newsDetailOb.filter(Notification::isOnError)
.map(n -> n.getThrowable()));
}//Model -->ViewModelprivate void initViewModelField(NewsDetail newsDetail) {
viewStyle.isRefreshing.set(false);
imageUrl.set(newsDetail.getImage());
Observable.just(newsDetail.getBody())
.map(s -> s + "<style type=\"text/css\">" + newsDetail.getCssStr())
.map(s -> s + "</style>")
.subscribe(s -> html.set(s));
title.set(newsDetail.getTitle());
}
注1:我們推薦MVVM和RxJava一塊兒使用,雖然兩者皆有觀察者模式的概念,但是RxJava不使用在針對(duì)View的監(jiān)聽,更多是業(yè)務(wù)數(shù)據(jù)流的轉(zhuǎn)換和處理。DataBinding框架其實(shí)是專用于View-ViewModel的動(dòng)態(tài)綁定的,它使得我們的ViewModel只需要關(guān)注數(shù)據(jù),而RxJava提供的強(qiáng)大數(shù)據(jù)流轉(zhuǎn)換函數(shù)剛好可以用來(lái)處理ViewModel中的種種數(shù)據(jù),得到很好的用武之地,同時(shí)加上Lambda表達(dá)式結(jié)合的鏈?zhǔn)骄幊?,使ViewModel的代碼非常簡(jiǎn)潔同時(shí)易讀易懂。
注2:因?yàn)楸疚臉永齅odel層只涉及到網(wǎng)絡(luò)數(shù)據(jù)的獲取,并沒(méi)有數(shù)據(jù)庫(kù)、存儲(chǔ)、數(shù)據(jù)狀態(tài)變化等其他業(yè)務(wù),所以本文涉及的源碼并沒(méi)有單獨(dú)把Model層抽出來(lái),我們是建議把Model層單獨(dú)抽出來(lái)放一個(gè)類中,然后以面向接口編程方式提供外界獲取和存儲(chǔ)數(shù)據(jù)的接口。
ViewModel與ViewModel的協(xié)作
在圖1中我們看到兩個(gè)ViewModel之間用一條虛線連接著,中間寫著Messenger。Messenger可以理解是一個(gè)全局消息通道,引入Messenger最主要的目的是實(shí)現(xiàn)ViewModel和ViewModel的通信,雖然也可以用于View和ViewModel的通信,但并不推薦。ViewModel主要是用來(lái)處理業(yè)務(wù)和數(shù)據(jù)的,每個(gè)ViewModel都有相應(yīng)的業(yè)務(wù)職責(zé),但是在業(yè)務(wù)復(fù)雜的情況下,可能存在交叉業(yè)務(wù),這時(shí)候就需要ViewModel和ViewModel交換數(shù)據(jù)和通信,這時(shí)候一個(gè)全局的消息通道就很重要的。
關(guān)于Messenger的詳細(xì)使用方法可以參照 MVVM Light Toolkit 使用指南的 Messenger 部分。這里給出一個(gè)簡(jiǎn)單的例子僅供參考:場(chǎng)景是這樣的,你的MainActivity對(duì)應(yīng)一個(gè)MainViewModel,MainActivity 里面除了自己的內(nèi)容還包含一個(gè)Fragment,這個(gè)Fragment 的業(yè)務(wù)處理對(duì)應(yīng)于一個(gè)FragmentViewModel,F(xiàn)ragmentViewModel請(qǐng)求服務(wù)器并獲取數(shù)據(jù)。剛好這個(gè)數(shù)據(jù)MainViewModel也需要用到,我們不可能在MainViewModel重新請(qǐng)求數(shù)據(jù),這樣不太合理,這時(shí)候就需要把數(shù)據(jù)傳給MainViewModel,那應(yīng)該怎么傳呢,如果彼此沒(méi)有引用或者回調(diào)?那么只能通過(guò)全局的消息通道Messenger。
FragmentViewModel獲取消息后通知MainViewModel并把數(shù)據(jù)傳給它:
combineRequestOb.filter(Notification::isOnNext)
.map(n -> n.getValue())
.map(p -> p.first)
.filter(m -> !m.getTop_stories().isEmpty())
.doOnNext(m ->Observable.just(NewsListHelper.isTomorrow(date)).filter(b -> b).subscribe(b -> itemViewModel.clear()))
// 上面的代碼可以不看,就是獲取網(wǎng)絡(luò)數(shù)據(jù) ,通過(guò)send把數(shù)據(jù)傳過(guò)去.subscribe(m -> Messenger.getDefault().send(m, TOKEN_TOP_NEWS_FINISH));
MainViewModel接收消息并處理:
Messenger.getDefault().register(activity, NewsViewModel.TOKEN_TOP_NEWS_FINISH, TopNewsService.News.class, (news) -> {// to something....}
在MainActivity onDestroy取消注冊(cè)就行了(不然導(dǎo)致內(nèi)存泄露):
@Override
protected void onDestroy() {
super.onDestroy();
Messenger.getDefault().unregister(this);
}
上面的例子只是簡(jiǎn)單地說(shuō)明,Messenger可以用在很多場(chǎng)景,通知、廣播都可以,不一定要傳數(shù)據(jù),在一定條件下也可以用在View層和ViewModel上的通信和廣播,運(yùn)用范圍特別廣,需要開發(fā)者結(jié)合實(shí)際的業(yè)務(wù)中去做更深層次的挖掘。
總結(jié)和源碼
本文主要講解了一些個(gè)人開發(fā)過(guò)程中總結(jié)的Android MVVM構(gòu)建思想,更多是理論上各個(gè)模塊如何分工、代碼如何設(shè)計(jì)。雖然現(xiàn)在業(yè)界使用Android MVVM模式開發(fā)還比較少,但是隨著DataBinding 1.0的發(fā)布,相信在Android MVVM 這一領(lǐng)域會(huì)更多的人來(lái)嘗試。剛好我最近用MVVM開發(fā)了一段時(shí)間,有點(diǎn)心得,寫出來(lái)僅供參考。
本文和源碼都沒(méi)有涉及到單元測(cè)試,如果需要寫單元測(cè)試,可以結(jié)合Google開源的MVP框架添加Contract類實(shí)現(xiàn)面向接口編程,可以幫助你更好地編寫單測(cè)。同時(shí)MVP和MVVM并沒(méi)孰好孰壞,適合業(yè)務(wù)、適合自己的才是最有價(jià)值的,建議結(jié)合Google開源的MVP框架和本文介紹的MVVM相關(guān)的知識(shí)去探索適合自己業(yè)務(wù)發(fā)展的框架。
MVVM Light Toolkit只是一個(gè)工具庫(kù),主要目的是更快捷方便地構(gòu)建Android MVVM應(yīng)用程序,在里面添加了一些控件額外屬性和做了一些事件的封裝,同時(shí)引進(jìn)了全局消息通道Messenger,個(gè)人覺(jué)得用起來(lái)會(huì)比較方便,你也可以嘗試一下。當(dāng)然這個(gè)庫(kù)還有不少地方需要完善和優(yōu)化,后續(xù)也會(huì)持續(xù)做更新和優(yōu)化,如果不能達(dá)到你的業(yè)務(wù)需求時(shí),可以clone下來(lái)自己做一些相關(guān)的擴(kuò)展。如果想更深入了解MVVM Light Toolkit,請(qǐng)看我這篇博文 《MVVM Light Toolkit 使用指南》。
項(xiàng)目的源碼地址 https://github.com/Kelin-Hong/MVVMLight 。其中:
library是MVVM Light Toolkit的源碼,源碼很簡(jiǎn)單,感興趣的同學(xué)可以看看,沒(méi)什么技術(shù)難度,可以根據(jù)自己的需求,添加更多的控件屬性和事件綁定。
sample是一個(gè)實(shí)現(xiàn)知乎日?qǐng)?bào)首頁(yè)樣式的Demo,本文的代碼示例均出自這個(gè)Demo。代碼包含了一大部分MVVM Light Toolkit的使用場(chǎng)景(Data、Command、Messenger均有涉及),同時(shí)sample嚴(yán)格按照本博文闡述的MVVM設(shè)計(jì)思想開發(fā),對(duì)理解本文會(huì)有比較大的幫助。
本文和源碼涉及RxJava+Retrofit+Lambda如有不懂或沒(méi)接觸過(guò),花點(diǎn)時(shí)間入門一下,用到的都是比較簡(jiǎn)單的東西。