如何構(gòu)建Android MVVM 應(yīng)用框架

說到Android MVVM,相信大家都會想到Google 2015年推出的DataBinding框架己莺。然而兩者的概念是不一樣的,不能混為一談戈轿。MVVM是一種架構(gòu)模式凌受,而DataBinding是一個實現(xiàn)數(shù)據(jù)和UI綁定的框架,是構(gòu)建MVVM模式的一個工具思杯。

之前看過很多關(guān)于Android MVVM的博客胜蛉,但大多數(shù)提到的都是DataBinding的基本用法,很少有文章仔細(xì)講解在Android中是如何通過DataBinding去構(gòu)建MVVM的應(yīng)用框架的色乾。View誊册、ViewModel、Model每一層的職責(zé)如何暖璧?它們之間聯(lián)系怎樣案怯、分工如何、代碼應(yīng)該如何設(shè)計澎办?這是我寫這篇文章的初衷嘲碱。

接下來,我們先來看看什么是MVVM局蚀,然后再一步一步來設(shè)計整個MVVM框架麦锯。

首先,我們先大致了解下Android開發(fā)中常見的模式至会。

MVC

View:XML布局文件。 Model:實體模型(數(shù)據(jù)的獲取、存儲梧宫、數(shù)據(jù)狀態(tài)變化)。 Controller:對應(yīng)于Activity摆碉,處理數(shù)據(jù)、業(yè)務(wù)和UI巷帝。

從上面這個結(jié)構(gòu)來看笤闯,Android本身的設(shè)計還是符合MVC架構(gòu)的浦马,但是Android中純粹作為View的XML視圖功能太弱,我們大量處理View的邏輯只能寫在Activity中,這樣Activity就充當(dāng)了View和Controller兩個角色,直接導(dǎo)致Activity中的代碼大爆炸。相信大多數(shù)Android開發(fā)者都遇到過一個Acitivty數(shù)以千行的代碼情況吧屑宠!所以省店,更貼切的說法是,這個MVC結(jié)構(gòu)最終其實只是一個Model-View(Activity:View&Controller)的結(jié)構(gòu)笨触。

MVP

**View: **對應(yīng)于Activity和XML懦傍,負(fù)責(zé)View的繪制以及與用戶的交互。 **Model: **依然是實體模型芦劣。 **Presenter: **負(fù)責(zé)完成View與Model間的交互和業(yè)務(wù)邏輯粗俱。

前面我們說,Activity充當(dāng)了View和Controller兩個角色虚吟,MVP就能很好地解決這個問題寸认,其核心理念是通過一個抽象的View接口(不是真正的View層)將Presenter與真正的View層進(jìn)行解耦。Persenter持有該View接口串慰,對該接口進(jìn)行操作偏塞,而不是直接操作View層。這樣就可以把視圖操作和業(yè)務(wù)邏輯解耦邦鲫,從而讓Activity成為真正的View層灸叼。

但MVP也存在一些弊端:

  • Presenter(以下簡稱P)層與View(以下簡稱V)層是通過接口進(jìn)行交互的,接口粒度不好控制庆捺。粒度太小古今,就會存在大量接口的情況,使代碼太過碎版化疼燥;粒度太大沧卢,解耦效果不好。同時對于UI的輸入和數(shù)據(jù)的變化醉者,需要手動調(diào)用V層或者P層相關(guān)的接口但狭,相對來說缺乏自動性、監(jiān)聽性撬即。如果數(shù)據(jù)的變化能自動響應(yīng)到UI立磁、UI的輸入能自動更新到數(shù)據(jù),那該多好剥槐!
  • MVP是以UI為驅(qū)動的模型唱歧,更新UI都需要保證能獲取到控件的引用,同時更新UI的時候要考慮當(dāng)前是否是UI線程,也要考慮Activity的生命周期(是否已經(jīng)銷毀等)颅崩。
  • MVP是以UI和事件為驅(qū)動的傳統(tǒng)模型几于,數(shù)據(jù)都是被動地通過UI控件做展示,但是由于數(shù)據(jù)的時變性沿后,我們更希望數(shù)據(jù)能轉(zhuǎn)被動為主動沿彭,希望數(shù)據(jù)能更有活性,由數(shù)據(jù)來驅(qū)動UI尖滚。
  • V層與P層還是有一定的耦合度喉刘。一旦V層某個UI元素更改,那么對應(yīng)的接口就必須得改漆弄,數(shù)據(jù)如何映射到UI上睦裳、事件監(jiān)聽接口這些都需要轉(zhuǎn)變,牽一發(fā)而動全身撼唾。如果這一層也能解耦就更好了廉邑。
  • 復(fù)雜的業(yè)務(wù)同時也可能會導(dǎo)致P層太大,代碼臃腫的問題依然不能解決倒谷。

MVVM

**View: **對應(yīng)于Activity和XML鬓催,負(fù)責(zé)View的繪制以及與用戶交互。 **Model: **實體模型恨锚。 **ViewModel: **負(fù)責(zé)完成View與Model間的交互,負(fù)責(zé)業(yè)務(wù)邏輯倍靡。

MVVM的目標(biāo)和思想與MVP類似猴伶,利用數(shù)據(jù)綁定(Data Binding)、依賴屬性(Dependency Property)塌西、命令(Command)他挎、路由事件(Routed Event)等新特性,打造了一個更加靈活高效的架構(gòu)捡需。

數(shù)據(jù)驅(qū)動

在常規(guī)的開發(fā)模式中办桨,數(shù)據(jù)變化需要更新UI的時候,需要先獲取UI控件的引用站辉,然后再更新UI呢撞。獲取用戶的輸入和操作也需要通過UI控件的引用。在MVVM中饰剥,這些都是通過數(shù)據(jù)驅(qū)動來自動完成的殊霞,數(shù)據(jù)變化后會自動更新UI,UI的改變也能自動反饋到數(shù)據(jù)層汰蓉,數(shù)據(jù)成為主導(dǎo)因素绷蹲。這樣MVVM層在業(yè)務(wù)邏輯處理中只要關(guān)心數(shù)據(jù),不需要直接和UI打交道,在業(yè)務(wù)處理過程中簡單方便很多祝钢。

低耦合度

MVVM模式中比规,數(shù)據(jù)是獨立于UI的。

數(shù)據(jù)和業(yè)務(wù)邏輯處于一個獨立的ViewModel中拦英,ViewModel只需要關(guān)注數(shù)據(jù)和業(yè)務(wù)邏輯蜒什,不需要和UI或者控件打交道。UI想怎么處理數(shù)據(jù)都由UI自己決定龄章,ViewModel不涉及任何和UI相關(guān)的事吃谣,也不持有UI控件的引用。即便是控件改變了(比如:TextView換成EditText)做裙,ViewModel也幾乎不需要更改任何代碼岗憋。它非常完美的解耦了View層和ViewModel,解決了上面我們所說的MVP的痛點锚贱。

更新UI

在MVVM中仔戈,數(shù)據(jù)發(fā)生變化后,我們在工作線程直接修改(在數(shù)據(jù)是線程安全的情況下)ViewModel的數(shù)據(jù)即可拧廊,不用再考慮要切到主線程更新UI了监徘,這些事情相關(guān)框架都幫我們做了。

團(tuán)隊協(xié)作

MVVM的分工是非常明顯的吧碾,由于View和ViewModel之間是松散耦合的:一個是處理業(yè)務(wù)和數(shù)據(jù)凰盔、一個是專門的UI處理。所以倦春,完全由兩個人分工來做户敬,一個做UI(XML和Activity)一個寫ViewModel,效率更高睁本。

可復(fù)用性

一個ViewModel可以復(fù)用到多個View中尿庐。同樣的一份數(shù)據(jù),可以提供給不同的UI去做展示呢堰。對于版本迭代中頻繁的UI改動抄瑟,更新或新增一套View即可。如果想在UI上做A/B Testing枉疼,那MVVM是你不二選擇皮假。

單元測試

有些同學(xué)一看到單元測試,可能腦袋都大骂维。是啊钞翔,寫成一團(tuán)漿糊的代碼怎么可能做單元測試?如果你們以代碼太爛無法寫單元測試而逃避席舍,那可真是不好的消息了布轿。這時候,你需要MVVM來拯救。

我們前面說過了汰扭,ViewModel層做的事是數(shù)據(jù)處理和業(yè)務(wù)邏輯稠肘,View層中關(guān)注的是UI,兩者完全沒有依賴萝毛。不管是UI的單元測試還是業(yè)務(wù)邏輯的單元測試项阴,都是低耦合的。在MVVM中數(shù)據(jù)是直接綁定到UI控件上的(部分?jǐn)?shù)據(jù)是可以直接反映出UI上的內(nèi)容)笆包,那么我們就可以直接通過修改綁定的數(shù)據(jù)源來間接做一些Android UI上的測試环揽。

通過上面的簡述以及模式的對比,我們可以發(fā)現(xiàn)MVVM的優(yōu)勢還是非常明顯的庵佣。雖然目前Android開發(fā)中可能真正在使用MVVM的很少歉胶,但是值得我們?nèi)プ鲆恍┨接懞驼{(diào)研。

如何分工

構(gòu)建MVVM框架首先要具體了解各個模塊的分工巴粪。接下來我們來講解View通今、ViewModel、Model它們各自的職責(zé)所在肛根。

View

View層做的就是和UI相關(guān)的工作辫塌,我們只在XML、Activity和Fragment寫View層的代碼派哲,View層不做和業(yè)務(wù)相關(guān)的事臼氨,也就是我們在Activity不寫業(yè)務(wù)邏輯和業(yè)務(wù)數(shù)據(jù)相關(guān)的代碼,更新UI通過數(shù)據(jù)綁定實現(xiàn)芭届,盡量在ViewModel里面做(更新綁定的數(shù)據(jù)源即可)一也,Activity要做的事就是初始化一些控件(如控件的顏色,添加RecyclerView的分割線)喉脖,View層可以提供更新UI的接口(但是我們更傾向所有的UI元素都是通過數(shù)據(jù)來驅(qū)動更改UI),View層可以處理事件(但是我們更希望UI事件通過Command來綁定)抑月。簡單地說: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 層不會持有任何控件的引用,更不會在ViewModel中通過UI控件的引用去做更新UI的事情叫胖。ViewModel就是專注于業(yè)務(wù)的邏輯處理草冈,做的事情也都只是對數(shù)據(jù)的操作(這些數(shù)據(jù)綁定在相應(yīng)的控件上會自動去更改UI)。同時DataBinding框架已經(jīng)支持雙向綁定,讓我們可以通過雙向綁定獲取View層反饋給ViewModel層的數(shù)據(jù)怎棱,并對這些數(shù)據(jù)上進(jìn)行操作哩俭。關(guān)于對UI控件事件的處理,我們也希望能把這些事件處理綁定到控件上拳恋,并把這些事件的處理統(tǒng)一化凡资,為此我們通過BindingAdapter對一些常用的事件做了封裝,把一個個事件封裝成一個個Command谬运,對于每個事件我們用一個ReplyCommand<t style="box-sizing: border-box;">去處理就行了隙赁,ReplyCommand<t style="box-sizing: border-box;">會把你可能需要的數(shù)據(jù)帶給你,這使得我們在ViewModel層處理事件的時候只需要關(guān)心處理數(shù)據(jù)就行了梆暖,具體見MVVM Light Toolkit 使用指南的Command部分伞访。再強(qiáng)調(diào)一遍:ViewModel 不做和UI相關(guān)的事。</t></t>

Model

Model層最大的特點是被賦予了數(shù)據(jù)獲取的職責(zé)式廷,與我們平常Model層只定義實體對象的行為截然不同咐扭。實例中,數(shù)據(jù)的獲取滑废、存儲蝗肪、數(shù)據(jù)狀態(tài)變化都是Model層的任務(wù)。Model包括實體模型(Bean)蠕趁、Retrofit的Service 薛闪,獲取網(wǎng)絡(luò)數(shù)據(jù)接口,本地存儲(增刪改查)接口俺陋,數(shù)據(jù)變化監(jiān)聽等豁延。Model提供數(shù)據(jù)獲取接口供ViewModel調(diào)用,經(jīng)數(shù)據(jù)轉(zhuǎn)換和操作并最終映射綁定到View層某個UI元素的屬性上腊状。

如何協(xié)作

關(guān)于協(xié)作诱咏,我們先來看下面的一張圖:

[圖片上傳失敗...(image-cfc71b-1606550227256)]

圖 1

上圖反映了MVVM框架中各個模塊的聯(lián)系和數(shù)據(jù)流的走向,我們從每個模塊一一拆分來看缴挖。那么我們重點就是下面的三個協(xié)作袋狞。

  • ViewModel與View的協(xié)作
  • ViewModel與Model的協(xié)作映屋。
  • ViewModel與ViewModel的協(xié)作苟鸯。

ViewModel與View的協(xié)作

[圖片上傳失敗...(image-1be9c4-1606550227256)]

圖 2

圖2中ViewModel和View是通過綁定的方式連接在一起的,綁定分成兩種:一種是數(shù)據(jù)綁定棚点,一種是命令綁定早处。數(shù)據(jù)的綁定DataBinding已經(jīng)提供好了,簡單地定義一些ObservableField就能把數(shù)據(jù)和控件綁定在一起了(如TextView的text屬性)瘫析,但是DataBinding框架提供的不夠全面砌梆,比如說如何讓一個URL綁定到一個ImageView默责,讓這個ImageView能自動去加載url指定的圖片,如何把數(shù)據(jù)源和布局模板綁定到一個ListView么库,讓ListView可以不需要去寫Adapter和ViewHolder相關(guān)的東西傻丝?這些就需要我們做一些工作和簡單的封裝。MVVM Light Toolkit 已經(jīng)幫我們做了一部分的工作诉儒,詳情可以查看MVVM Light Toolkit 使用指南葡缰。關(guān)于事件綁定也是一樣,MVVM Light Toolkit 做了簡單的封裝忱反,對于每個事件我們用一個ReplyCommand去處理就行了泛释,ReplyCommand<t style="box-sizing: border-box;">會把可能需要的數(shù)據(jù)帶給你,這樣我們處理事件的時候也只關(guān)心處理數(shù)據(jù)就行了温算。</t>

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

  • Context (上下文) - Model (數(shù)據(jù)源 Java Bean) - Data Field (數(shù)據(jù)綁定) - Command (命令綁定) - Child ViewModel (子ViewModel)

我們先來看下示例代碼怜校,然后再一一講解5個部分是干嘛用的:

//context
private 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<>();
// 一個變量包含了所有關(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 ViewModel
public final ObservableList<NewItemViewModel> itemViewModel = new ObservableArrayList<>();

/** * ViewStyle 關(guān)于控件的一些屬性和業(yè)務(wù)數(shù)據(jù)無關(guān)的Style 可以做一個包裹注竿,這樣代碼比較美觀茄茁,
ViewModel 頁面也不會有太多太雜的字段。 **/
public static class ViewStyle {    
   public final ObservableBoolean isRefreshing = new ObservableBoolean(true);    
   public final ObservableBoolean progressRefreshing = new ObservableBoolean(true);
}

Context

Context是干嘛用的呢巩割,為什么每個ViewModel都最好需要持了一個Context的引用呢裙顽?ViewModel不處理和UI相關(guān)的事也不操作控件,更不更新UI宣谈,那為什么要有Context呢愈犹?原因主要有以下兩點:

  1. 通過圖1中,然后得到一個Observable<bean style="box-sizing: border-box;">闻丑,其實這就是網(wǎng)絡(luò)請求部分漩怎。其實這就是網(wǎng)絡(luò)請求部分,做網(wǎng)絡(luò)請求我們必須把Retrofit Service返回的Observable<bean style="box-sizing: border-box;">綁定到Context的生命周期上嗦嗡,防止在請求回來時Activity已經(jīng)銷毀等異常勋锤,其實這個Context的目的就是把網(wǎng)絡(luò)請求綁定到當(dāng)前頁面的生命周期中。</bean></bean>

  2. 在圖1中侥祭,我們可以看到兩個ViewModel之間的聯(lián)系是通過Messenger來做叁执,這個Messenger是需要用到Context,這個我們后續(xù)會講解卑硫。

當(dāng)然,除此以外蚕断,調(diào)用工具類欢伏、幫助類有時候需要Context做為參數(shù)等也是原因之一。

Model (數(shù)據(jù)源)

Model是什么呢亿乳?其實就是數(shù)據(jù)源硝拧,可以簡單理解是我們用JSON轉(zhuǎn)過來的Bean径筏。ViewModel要把數(shù)據(jù)映射到UI中可能需要大量對Model的數(shù)據(jù)拷貝和操作,拿Model的字段去生成對應(yīng)的ObservableField然后綁定到UI(我們不會直接拿Model的數(shù)據(jù)去做綁定展示)障陶,這里是有必要在一個ViewModel保留原始的Model引用滋恬,這對于我們是非常有用的,因為可能用戶的某些操作和輸入需要我們?nèi)ジ淖償?shù)據(jù)源抱究,可能我們需要把一個Bean在列表頁點擊后傳給詳情頁恢氯,可能我們需要把這個Model當(dāng)做表單提交到服務(wù)器。這些都需要我們的ViewModel持有相應(yīng)的Model(數(shù)據(jù)源)鼓寺。

Data Field(數(shù)據(jù)綁定)

Data Field就是需要綁定到控件上的ObservableField字段勋拟,這是ViewModel的必需品,這個沒有什么好說妈候。但是這邊有一個建議: 這些字段是可以稍微做一下分類和包裹的敢靡。比如說可能一些字段是綁定到控件的一些Style屬性上(如長度、顏色苦银、大行ル省),對于這類針對View Style的的字段可以聲明一個ViewStyle類包裹起來幔虏,這樣整個代碼邏輯會更清晰一些含思,不然ViewModel里面可能字段泛濫,不易管理和閱讀性較差兔毙。而對于其他一些字段尘颓,比如說title、imageUrl主胧、name這些屬于數(shù)據(jù)源類型的字段叭首,這些字段也叫數(shù)據(jù)字段,是和業(yè)務(wù)數(shù)據(jù)和邏輯息息相關(guān)的踪栋,這些字段可以放在一塊焙格。

Command(命令綁定)

Command(命令綁定)簡言之就是對事件的處理(下拉刷新、加載更多夷都、點擊眷唉、滑動等事件處理)。我們之前處理事件是拿到UI控件的引用囤官,然后設(shè)置Listener冬阳,這些Listener其實就是Command。但是考慮到在一個ViewModel寫各種Listener并不美觀党饮,可能實現(xiàn)一個Listener就需要實現(xiàn)多個方法肝陪,但是我們可能只想要其中一個有用的方法實現(xiàn)就好了。更重要一點是實現(xiàn)一個Listener可能需要寫一些UI邏輯才能最終獲取我們想要的刑顺。簡單舉個例子氯窍,比如你想要監(jiān)聽ListView滑到最底部然后觸發(fā)加載更多的事件饲常,這時候就要在ViewModel里面寫一個OnScrollListener,然后在里面的onScroll方法中做計算狼讨,計算什么時候ListView滑動底部了贝淤。其實ViewModel的工作并不想去處理這些事件,它專注做的應(yīng)該是業(yè)務(wù)邏輯和數(shù)據(jù)處理政供,如果有一個東西不需要你自己去計算是否滑到底部播聪,而是在滑動底部自動觸發(fā)一個Command,同時把當(dāng)前列表的總共的item數(shù)量返回給你鲫骗,方便你通過 page=itemCount/LIMIT+1去計算出應(yīng)該請求服務(wù)器哪一頁的數(shù)據(jù)那該多好啊犬耻。MVVM Light Toolkit 幫你實現(xiàn)了這一點:

 public final ReplyCommand<Integer> onLoadMoreCommand =  new ReplyCommand<>((itemCount) -> { 
   int page=itemCount/LIMIT+1; 
   loadData(page.LIMIT)
});

接著在XML布局文件中通過bind:onLoadMoreCommand綁定上去就行了。

 <android.support.v7.widget.RecyclerView 
 android:layout_width="match_parent"  
 android:layout_height="match_parent"  
 bind:onLoadMoreCommand="@{viewModel.loadMoreCommand}"/>
 x```

 具體想了解更多請查看 **[MVVM Light Toolkit 使用指南](http://www.reibang.com/p/43ea7a531700)**执泰,里面有比較詳細(xì)地講解Command的使用枕磁。當(dāng)然Command并不是必須的,你完全可以依照自己的習(xí)慣和喜好在ViewModel寫Listener术吝,不過使用Command可以使ViewModel更簡潔易讀计济。你也可以自己定義更多的、其他功能的Command排苍,那么ViewModel的事件處理都是托管ReplyCommand<T>來處理沦寂,這樣的代碼看起來會比較美觀和清晰。Command只是對UI事件的一層隔離UI層的封裝淘衙,在事件觸發(fā)時把ViewModel層可能需要的數(shù)據(jù)傳給ViewModel層传藏,對事件的處理做了統(tǒng)一化,是否使用的話彤守,還是看你個人喜好了毯侦。

 ### Child ViewModel(子ViewModel)

 子ViewModel的概念就是在ViewModel里面嵌套其他的ViewModel,這種場景還是很常見的具垫。比如說你一個Activity里面有兩個Fragment侈离,ViewModel是以業(yè)務(wù)劃分的,兩個Fragment做的業(yè)務(wù)不一樣筝蚕,自然是由兩個ViewModel來處理卦碾,這時候Activity對應(yīng)的ViewModel里面可能包含了兩個Fragment各自的ViewModel,這就是嵌套的子ViewModel起宽。還有另外一種就是對于AdapterView洲胖,如ListView RecyclerView、ViewPager等坯沪。

 ```java
  //Child ViewModelpublic final 
   ObservableList<ItemViewModel> itemViewModel = new ObservableArrayList<>();

它們的每個Item其實就對應(yīng)于一個ViewModel绿映,然后在當(dāng)前的ViewModel通過ObservableList<itemviewmodel style="box-sizing: border-box;">持有引用(如上述代碼),這也是很常見的嵌套的子ViewModel屏箍。我們其實還建議绘梦,如果一個頁面業(yè)務(wù)非常復(fù)雜,不要把所有邏輯都寫在一個ViewModel赴魁,可以把頁面做業(yè)務(wù)劃分卸奉,把不同的業(yè)務(wù)放到不同的ViewModel,然后整合到一個總的ViewModel颖御,這樣做起來可以使我們的代碼業(yè)務(wù)清晰榄棵、簡短意賅,也方便后人的維護(hù)潘拱。</itemviewmodel>

總的來說疹鳄,ViewModel和View之前僅僅只有綁定的關(guān)系,View層需要的屬性和事件處理都是在XML里面綁定好了芦岂,ViewModel層不會去操作UI瘪弓,只是根據(jù)業(yè)務(wù)要求處理數(shù)據(jù),這些數(shù)據(jù)自動映射到View層控件的屬性上禽最。關(guān)于ViewModel類中包含哪些模塊和字段腺怯,這個需要開發(fā)者自己去衡量,我們建議ViewModel不要引入太多的成員變量川无,成員變量最好只有上面的提到的5種(context呛占、model……),能不引入其他類型的變量就盡量不要引進(jìn)來懦趋,太多的成員變量對于整個代碼結(jié)構(gòu)破壞很大晾虑,后面維護(hù)的人要時刻關(guān)心成員變量什么時候被初始化、什么時候被清掉仅叫、什么時候被賦值或者改變帜篇,一個細(xì)節(jié)不小心可能就出現(xiàn)潛在的Bug。太多不清晰定義的成員變量又沒有注釋的代碼是很難維護(hù)的惑芭。

另外坠狡,我們會把UI控件的屬性和事件都通過XML(如bind:text=@{…})綁定。如果一個業(yè)務(wù)邏輯要彈一個Dialog遂跟,但是你又不想在ViewModel里面做彈窗的事(ViewModel不希望做UI相關(guān)的事)或者說改變ActionBar上面的圖標(biāo)的顏色逃沿,改變ActionBar按鈕是否可點擊,這些都不是寫在XML里面(都是用Java代碼初始化的)幻锁,如何對這些控件的屬性做綁定呢凯亮?我們先來看下代碼:

public class MainViewModel implements ViewModel {
....
//true的時候彈出Dialog,false的時候關(guān)掉dialog
public final ObservableBoolean isShowDialog = new ObservableBoolean();
....
.....
}
// 在View層做一個對isShowDialog改變的監(jiān)聽
public class MainActivity extends RxBasePmsActivity {

private MainViewModel mainViewModel;

@Override
protected 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();
          }
       }
    });
 }
 ...
}

簡單地說你可以對任意的ObservableField做監(jiān)聽哄尔,然后根據(jù)數(shù)據(jù)的變化做相應(yīng)UI的改變假消,業(yè)務(wù)層ViewModel只要根據(jù)業(yè)務(wù)處理數(shù)據(jù)就行,以數(shù)據(jù)來驅(qū)動UI岭接。

ViewModel與Model的協(xié)作

從圖1中富拗,ViewModel通過傳參數(shù)到Model層獲取網(wǎng)絡(luò)數(shù)據(jù)(數(shù)據(jù)庫同理)臼予,然后把Model的部分?jǐn)?shù)據(jù)映射到ViewModel的一些字段(ObservableField),并在ViewModel保留這個Model的引用啃沪,我們來看下這一塊的大致代碼(代碼涉及簡單的RxJava粘拾,如看不懂可以查閱入門一下):

 //Model
 private NewsDetail newsDetail;

 private void loadData(long id) {  
   //  Observable<Bean> 用來獲取網(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ò)請求綁定到Activity 的生命周期
                  .compose(((ActivityLifecycleProvider) context).bindToLifecycle()) 
                 //變成 Notification<Bean> 使我們更方便處理數(shù)據(jù)和錯誤
                  .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ò)請求錯誤處理
    NewsListHelper.dealWithResponseError(
      newsDetailOb.filter(Notification::isOnError)        
                  .map(n -> n.getThrowable()));
}
//Model -->ViewModel
private 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不使用在針對View的監(jiān)聽,更多是業(yè)務(wù)數(shù)據(jù)流的轉(zhuǎn)換和處理追驴。DataBinding框架其實是專用于View-ViewModel的動態(tài)綁定的械哟,它使得我們的ViewModel只需要關(guān)注數(shù)據(jù),而RxJava提供的強(qiáng)大數(shù)據(jù)流轉(zhuǎn)換函數(shù)剛好可以用來處理ViewModel中的種種數(shù)據(jù)殿雪,得到很好的用武之地暇咆,同時加上Lambda表達(dá)式結(jié)合的鏈?zhǔn)骄幊蹋筕iewModel的代碼非常簡潔同時易讀易懂丙曙。

注2:因為本文樣例Model層只涉及到網(wǎng)絡(luò)數(shù)據(jù)的獲取糯崎,并沒有數(shù)據(jù)庫、存儲河泳、數(shù)據(jù)狀態(tài)變化等其他業(yè)務(wù)沃呢,所以本文涉及的源碼并沒有單獨把Model層抽出來,我們是建議把Model層單獨抽出來放一個類中拆挥,然后以面向接口編程方式提供外界獲取和存儲數(shù)據(jù)的接口薄霜。

ViewModel與ViewModel的協(xié)作

在圖1中我們看到兩個ViewModel之間用一條虛線連接著,中間寫著Messenger纸兔。Messenger可以理解是一個全局消息通道惰瓜,引入Messenger最主要的目的是實現(xiàn)ViewModel和ViewModel的通信,雖然也可以用于View和ViewModel的通信汉矿,但并不推薦崎坊。ViewModel主要是用來處理業(yè)務(wù)和數(shù)據(jù)的,每個ViewModel都有相應(yīng)的業(yè)務(wù)職責(zé)洲拇,但是在業(yè)務(wù)復(fù)雜的情況下奈揍,可能存在交叉業(yè)務(wù),這時候就需要ViewModel和ViewModel交換數(shù)據(jù)和通信赋续,這時候一個全局的消息通道就很重要男翰。

關(guān)于Messenger的詳細(xì)使用方法可以參照 MVVM Light Toolkit 使用指南的 Messenger 部分。這里給出一個簡單的例子僅供參考:場景是這樣的纽乱,你的MainActivity對應(yīng)一個MainViewModel蛾绎,MainActivity 里面除了自己的內(nèi)容還包含一個Fragment,這個Fragment 的業(yè)務(wù)處理對應(yīng)于一個FragmentViewModel,F(xiàn)ragmentViewModel請求服務(wù)器并獲取數(shù)據(jù)租冠。剛好這個數(shù)據(jù)MainViewModel也需要用到鹏倘,我們不可能在MainViewModel重新請求數(shù)據(jù),這樣不太合理顽爹,這時候就需要把數(shù)據(jù)傳給MainViewModel第股,那應(yīng)該怎么傳呢,如果彼此沒有引用或者回調(diào)话原?那么只能通過全局的消息通道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ù) 繁仁,通過send把數(shù)據(jù)傳過去
.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取消注冊就行了(不然導(dǎo)致內(nèi)存泄露):

 @Override
 protected void onDestroy() {    
      super.onDestroy();      
      Messenger.getDefault().unregister(this);
 }

上面的例子只是簡單地說明,Messenger可以用在很多場景归园,通知黄虱、廣播都可以,不一定要傳數(shù)據(jù)庸诱,在一定條件下也可以用在View層和ViewModel上的通信和廣播捻浦,運用范圍特別廣,需要開發(fā)者結(jié)合實際的業(yè)務(wù)中去做更深層次的挖掘桥爽。

本文主要講解了一些個人開發(fā)過程中總結(jié)的Android MVVM構(gòu)建思想朱灿,更多是理論上各個模塊如何分工、代碼如何設(shè)計钠四。雖然現(xiàn)在業(yè)界使用Android MVVM模式開發(fā)還比較少盗扒,但是隨著DataBinding 1.0的發(fā)布,相信在Android MVVM 這一領(lǐng)域會更多的人來嘗試缀去。剛好我最近用MVVM開發(fā)了一段時間侣灶,有點心得,寫出來僅供參考缕碎。

本文和源碼都沒有涉及到單元測試褥影,如果需要寫單元測試,可以結(jié)合Google開源的MVP框架添加Contract類實現(xiàn)面向接口編程咏雌,可以幫助你更好地編寫單測凡怎。同時MVP和MVVM并沒孰好孰壞,適合業(yè)務(wù)赊抖、適合自己的才是最有價值的栅贴,建議結(jié)合Google開源的MVP框架和本文介紹的MVVM相關(guān)的知識去探索適合自己業(yè)務(wù)發(fā)展的框架。

MVVM Light Toolkit只是一個工具庫熏迹,主要目的是更快捷方便地構(gòu)建Android MVVM應(yīng)用程序檐薯,在里面添加了一些控件額外屬性和做了一些事件的封裝,同時引進(jìn)了全局消息通道Messenger,個人覺得用起來會比較方便坛缕,你也可以嘗試一下墓猎。當(dāng)然這個庫還有不少地方需要完善和優(yōu)化(后續(xù)會持續(xù)改進(jìn)),如果不能達(dá)到你的業(yè)務(wù)需求赚楚,可以clone下來自己做一些相關(guān)的擴(kuò)展毙沾。如果想更深入了解MVVM Light Toolkit,請看我這篇博文 MVVM Light Toolkit 使用指南宠页。

項目的源碼地址 https://github.com/Kelin-Hong/MVVMLight 左胞。 其中:

library是MVVM Light Toolkit的源碼,源碼很簡單举户,感興趣的同學(xué)可以看看烤宙,沒什么技術(shù)難度,可以根據(jù)自己的需求俭嘁,添加更多的控件屬性和事件綁定躺枕。

sample是一個實現(xiàn)知乎日報首頁樣式的Demo,本文的代碼實例均出自這個Demo供填,代碼包含了一大部分MVVM Light Toolkit的使用場景(Data拐云、Command、Messenger均有涉及)近她,同時sample嚴(yán)格按照本文闡述的MVVM的設(shè)計思想開發(fā)叉瘩,對理解本文會有比較大的幫助。

本文和源碼涉及RxJava+Retrofit+Lambda如有不懂或沒接觸過粘捎,花點時間入門一下房揭,用到的都是比較簡單的東西。

本文在開源項目:https://github.com/Android-Alvin/Android-LearningNotes 中已收錄晌端,里面包含不同方向的自學(xué)編程路線捅暴、面試題集合/面經(jīng)、及系列技術(shù)文章等咧纠,資源持續(xù)更新中...

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蓬痒,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子漆羔,更是在濱河造成了極大的恐慌梧奢,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件演痒,死亡現(xiàn)場離奇詭異亲轨,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)鸟顺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進(jìn)店門惦蚊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來器虾,“玉大人,你說我怎么就攤上這事蹦锋≌咨常” “怎么了?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵莉掂,是天一觀的道長葛圃。 經(jīng)常有香客問我,道長憎妙,這世上最難降的妖魔是什么库正? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮厘唾,結(jié)果婚禮上褥符,老公的妹妹穿的比我還像新娘。我一直安慰自己阅嘶,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布载迄。 她就那樣靜靜地躺著讯柔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪护昧。 梳的紋絲不亂的頭發(fā)上魂迄,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天,我揣著相機(jī)與錄音惋耙,去河邊找鬼捣炬。 笑死,一個胖子當(dāng)著我的面吹牛绽榛,可吹牛的內(nèi)容都是我干的湿酸。 我是一名探鬼主播,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼灭美,長吁一口氣:“原來是場噩夢啊……” “哼推溃!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起届腐,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤铁坎,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后犁苏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體硬萍,經(jīng)...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年围详,在試婚紗的時候發(fā)現(xiàn)自己被綠了朴乖。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖寒砖,靈堂內(nèi)的尸體忽然破棺而出赐劣,到底是詐尸還是另有隱情,我是刑警寧澤哩都,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布魁兼,位于F島的核電站,受9級特大地震影響漠嵌,放射性物質(zhì)發(fā)生泄漏咐汞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一儒鹿、第九天 我趴在偏房一處隱蔽的房頂上張望化撕。 院中可真熱鬧,春花似錦约炎、人聲如沸植阴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽掠手。三九已至,卻和暖如春狸捕,著一層夾襖步出監(jiān)牢的瞬間喷鸽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工灸拍, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留做祝,地道東北人。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓鸡岗,卻偏偏與公主長得像混槐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子轩性,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,901評論 2 355

推薦閱讀更多精彩內(nèi)容