前言
去年5月左右的時候,筆者在逛GitHub的時候,看到了一個MVP的專案,叫做mosby,仔細看了原始碼和作者介紹的文章後,發現確實有點意思,雖然會需要多寫幾個類和方法,但是解決了activity/fragment過重的問題,通過V/P分離能夠幫助提高可維護性。時至去年年底,今年年初,MVP才逐漸被大家所知,也不時看到些文章介紹其概念和實踐。
再說說MVVM (Model-View-ViewModel),在Android上對應data binding。即ViewModel到View的對映,不需要再去自己找到view,然後更新欄位,而是在對映建立後直接更新ViewModel然後反映到View上。
值得一提的是,MVP和MVVM都是微軟提出的理念,最早都是在WPF裡面被應用的,只是時至今日才在Android上被真正用起來。本文不是來介紹這兩個的,所以不再贅述,講講正題。
在本系列首篇中我曾經提到過在我設計的新應用中,採用了MVP+MVVM的混合(當初也考慮過Flux,但感覺並不合適Android),後來有一次CJJ同學和我探討這個架構的時候,問到了我有沒有什麼正式的名字,我就有楞,因為這個組合和應用是我自己設計的,所以還真沒想過這個問題,Google一搜,還真有這麼個東西(見參考資料,文章寫得很棒,建議英文不錯的同學讀一讀)!
這就是本文我要介紹的東西,MVPVM (Model-View-Presenter-ViewModel)。
Quick glance
以下所有Model,並不單單指的是Bean,而是Model層,更像是repository或者business logic。
MVC: View持有Controller,傳遞事件給Controller,Controller由此去觸發Model層事件,Model更新完資料(如從網路或者資料庫獲得資料後)觸發View的更新事件。
乍一看,MVP似乎是MVC的變種,即C的位置被P取代了,但如果我們再看一看下圖:
其實MVP是MVC的一個wrap,C層仍然可以在那裡,代替View處理點選事件、資料繫結、扮演ListView的觀察者,從而View可以專注於處理純視覺的一些東西。而Presenter則避免了Model直接去觸發View的更新,View徹底成為了一個被動的東西,只有Presenter告知其更新視覺,它才會去更新,比如showLoading(), showEmpty()。
MVVM通過View和ViewModel的雙向繫結,讓我們可以
- 直接更新ViewModel,View會進行對應重新整理
- View的事件直接傳遞到ViewModel,ViewModel去對Model進行操作並接受更新。
Why MVPVM
如果你仔細讀過Clean architecture的原始碼,會發現其中已經有了ViewModel這一層。如果你熟悉DO(Domain Object),PO(Persistent Object),VO(View Object),或許瞭解visibility這個概念,各層只需要知道其應該知道的。這些Object代表了完全獨立不同的概念。
ViewModel層的必要性,簡單舉個例子,伺服器傳來一個Date String,但我們需要顯示的是該Date到現在的相對時間描述,比如1分鐘前,2天前,為了避免在view中繫結資料時去做這個邏輯,ViewModel會代替來進行這個的轉換。
MVVM儘管確實省去了繫結資料到View的boilerplate,但
- ViewModel引用了View,從而導致ViewModel無法重用於其他View。
- 並沒有解決View層過重的問題,僅僅去掉了資料繫結,尤其對一些複雜業務邏輯的頁面。
模式的引入都是為了通過可拔插化以提高可複用性,鬆耦合和儘量小的介面可以給予最大的可複用性,使得元件能重組使用。
所以有了MVPVM:
在我的個人實踐中:
- Model: data和domain模組組成,包含了Interactor(UseCase)、Repository、Datastore、Retrofit、Realm、DO、部分PO等。
- View: Activity/Fragment。
- Presenter:Presenter,包含了Subscriber,並通過Dagger2注入UseCase從而減輕耦合。
- ViewModel:由Model轉換而成,繼承BaseObservable或SortedList,大部分直接wrap了model,從而去掉了mapper的boilerplate。通過Data Binding繫結到xml。
從Presenter的Subscriber往下都是RxJava的流世界,stream in stream out。如果你原來就應用了MVP或者Clean Architecture,那會發現再加上ViewModel簡直太簡單了,同時讓程式碼庫更小,邏輯更清晰。
接著看看各個元件在MVPVM中的standing。
MVPVM: Model
實際對應的是Repository層,即第一篇文章中提到的data/domain module。具體的Model理論上應該是PO,但我們大部分場景並不需要PO,所以也可以是domain層的DO。
MVPVM: View
View對ViewModel不需要了解太多,這樣才能保持兩者的解耦,兩者之間的協議只需要:
- ViewModel支援View需要展示的properties。
- View實現了ViewModel的觀察者模式介面(如Listener)。
所以這裡ViewModel到View是一條虛線,而不是MVVM中的雙向實線。
MVPVM: Presenter
和在MVP一樣,Presenter站在View和Model層之間。這裡值得一提的是Presenter到ViewModel是有耦合的,因為Presenter需要把model更新到ViewModel中,也就是map行為,然後呼叫View的對應介面進行binding。
Presenter是MVPVM中唯一不需要解耦的,它緊緊地與View、ViewModel、Model層耦合。如果你的Presenter被多個View重用了,那你可能需要考慮它是不是更應該作為一個module,比如(第三方)登陸。
MVPVM: ViewModel
MVPVM讓ViewModel可以重用,因為它再也不是直接和特定View繫結,而僅僅作為資料到View的一個繫結用展示。ViewModel因為使用者操作而觸發的事件不再直接對Model進行操作,而由View去負責任務流。ViewModel本身基本沒有field,而是通過暴露get方法來讓data binding找到對應要顯示的property,get方法中直接呼叫持有的model的對應屬性get方法。
理想化的架構是通過一個mapper類進行轉換,但我想大部分的程式設計師面對這個工作都會抓狂,畢竟很多欄位其實就是一個複製,而且對效能也有一些影響(遍歷list,new物件,一個個欄位轉換,新增到新的list)。所以折中地,讓ViewModel持有Model,在get方法中直接返回對應model的具體欄位,在一些特殊的field如相對時間、新增一些描述性字元的地方再去進行拼接和特殊處理。
啊,對了,說到ViewModel,Data Binding現在支援雙向繫結了哦,見 https://halfthought.wordpress.com/2016/03/23/2-way-data-binding-on-android/,語法如:
1 |
<EditText android:text="@={user.firstName}" .../> |
不同於單向繫結的@{},使用了@={},畢竟雙向繫結這個東西還是慎用,一方面早成資料流混亂不好理解,另一方面容易出現死迴圈。
NO Presenter
在MVP中,我們有時候碰到的問題是,Presenter真的有必要存在嗎,尤其是一些較為靜態,沒什麼業務邏輯,只需要純展示的頁面,結果就是為了MVP而特意去建立一個Presenter。
所以Presenter不應該被強求,正如MVP中,V和C其實被並在了一起,在某些情況下(確實就是個純展示,或者很少的業務邏輯),應該允許去Presenter,並讓View承擔其任務。比如註冊頁面,我真的就只是想把使用者的輸入發到伺服器驗證一下,何必非得去搞一個presenter套著呢?
我們不能永遠理想化地去選擇所謂最好的設計,在現實的必要情況下,我們要敢於捨棄,最合適的設計才是最好的設計。為此,Presenter不是強制的;為此,ViewModel並不一定通過mapper生成,而可以返回持有的DO物件對應欄位。
總結
本篇講了講MVPVM及其在Android的實踐,因為時間原因來不及寫個demo來說說具體實現,歡迎大家提出意見和建議。有空的話我最近會在GitHub上寫一下demo,你如果有興趣可以follow一下等等更新: markzhai。
下集預告
Dagger匕首,比ButterKnife黃油刀鋒利得多。Square為什麼這麼有自信地給它取了這個名字,Google又為什麼會拿去做了Dagger2呢?筆者看了很多國內Dagger2的文章,但發現它們都保留在介紹API和官網翻譯的層面,無法讓讀者能明白究竟為什麼用Dagger2,又如何用好Dagger2。希望能在下一次為大家講清楚。
參考資料
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式