原文:《REACTIVE APPS WITH MODEL-VIEW-INTENT - PART1 - MODEL》
作者:Hannes Dorfmann
譯者:卻把清梅嗅
有朝一日,我突然發(fā)現(xiàn)我對(duì)于Model層的定義 全部是錯(cuò)誤的,更新了認(rèn)知后,我發(fā)現(xiàn)曾經(jīng)我在Android平臺(tái)上主題討論中的那些困惑或者頭痛都消失了。
從結(jié)果上來說,最終我選擇使用 RxJava 和 Model-View-Intent(MVI) 構(gòu)建 響應(yīng)式的APP,這是我從未有過的嘗試——盡管在這之前我開發(fā)的APP也是響應(yīng)式的,但 響應(yīng)式編程 的體現(xiàn)與這次實(shí)踐相比,完全無法相提并論,在接下來我將要講述的一系列文章中,你也會(huì)感受到這些。但作為系列文章的開始,我想先闡述一個(gè)觀點(diǎn):
所謂的
Model層到底是什么,我之前對(duì)Model層的定義出現(xiàn)了什么問題?
我為什么說 我對(duì)Model層有著錯(cuò)誤的理解和使用方式 呢?當(dāng)然,現(xiàn)在有很多架構(gòu)模式將View層和Model層進(jìn)行了分離,至少在Android開發(fā)的領(lǐng)域,最著名的當(dāng)屬Model-View-Controller (MVC)、Model-View-Presenter (MVP)和Model-View-ViewModel (MVVM)——你注意到了嗎?這些架構(gòu)模式中,Model都是不可或缺的一環(huán),但我意識(shí)到 在絕大數(shù)情況下,我根本沒有Model。
舉例來說,一個(gè)簡(jiǎn)單的從后端拉取Person列表情況下,傳統(tǒng)的MVP實(shí)現(xiàn)方式應(yīng)該是這樣的:
class PersonsPresenter extends Presenter<PersonsView> {
public void load(){
getView().showLoading(true); // 展示一個(gè) ProgressBar
backend.loadPersons(new Callback(){
public void onSuccess(List<Person> persons){
getView().showPersons(persons); // 展示用戶列表
}
public void onError(Throwable error){
getView().showError(error); // 展示錯(cuò)誤信息
}
});
}
}
但是,這段代碼中的Model到底是指什么呢?是指后臺(tái)的網(wǎng)絡(luò)請(qǐng)求嗎?不,那只是業(yè)務(wù)邏輯。是指請(qǐng)求結(jié)果的用戶列表嗎?不,它和ProgressBar、錯(cuò)誤信息的展示一樣,僅僅只代表了View層所能展示內(nèi)容的一小部分而已。
那么,Model層究竟是指什么呢?
從我個(gè)人理解來說,Model類應(yīng)該定義成這樣:
class PersonsModel {
// 在真實(shí)的項(xiàng)目中,需要定義為私有的
// 并且我們需要通過getter和setter來訪問它們
final boolean loading;
final List<Person> persons;
final Throwable error;
public(boolean loading, List<Person> persons, Throwable error){
this.loading = loading;
this.persons = persons;
this.error = error;
}
}
這樣的實(shí)現(xiàn),Presenter層應(yīng)該這樣實(shí)現(xiàn):
class PersonsPresenter extends Presenter<PersonsView> {
public void load(){
getView().render( new PersonsModel(true, null, null) ); // 展示一個(gè) ProgressBar
backend.loadPersons(new Callback(){
public void onSuccess(List<Person> persons){
getView().render( new PersonsModel(false, persons, null) ); // 展示用戶列表
}
public void onError(Throwable error){
getView().render( new PersonsModel(false, null, error) ); // 展示錯(cuò)誤信息
}
});
}
}
現(xiàn)在,View層持有了一個(gè)Model,并且能夠借助它對(duì)屏幕上的控件進(jìn)行rendered(渲染)。這并非什么新鮮的概念,Trygve Reenskaug在1979年時(shí),其對(duì)最初版本的MVC定義中具有相似的概念:View觀察Model的變化。
然而,MVC這個(gè)術(shù)語被用來描述太多種不同的模式,這些模式與Reenskaug在1979年制定的模式并不完全相同。比如后端開發(fā)人員使用MVC框架,iOS有ViewController,到了Android領(lǐng)域MVC又被如何定義了呢?Activity是Controller嗎? 那這樣的話ClickListener又算什么呢?如今,MVC這個(gè)術(shù)語變成了一個(gè)很大的誤區(qū),它錯(cuò)誤地理解和使用了Reenskaug最初制定的內(nèi)容——這個(gè)話題到此為止,再繼續(xù)下去整個(gè)文章就會(huì)失控了。
言歸正傳,Model的持有將會(huì)解決許多我們?cè)?code>Android開發(fā)中經(jīng)常遇到的問題:
- 1.狀態(tài)問題
- 2.屏幕方向的改變
- 3.在頁面堆棧中導(dǎo)航
- 4.進(jìn)程終止
- 5.單向數(shù)據(jù)流的不變性
- 6.可調(diào)試和可重現(xiàn)的狀態(tài)
- 7.可測(cè)試性
要討論這些關(guān)鍵的問題,我們先來看看“傳統(tǒng)”的MVP和MVVM的實(shí)現(xiàn)代碼中如何處理它們,然后再談Model如何跳過這些常見的陷阱。
1.狀態(tài)問題
響應(yīng)式App,這是最近非常流行的話題,不是嗎?所謂的 響應(yīng)式App 就是 應(yīng)用會(huì)根據(jù)狀態(tài)的改變作出UI的響應(yīng),這句話里有一個(gè)非常好的單詞:狀態(tài)。什么是狀態(tài)呢?大多數(shù)時(shí)間里,我們將 狀態(tài) 描述為我們?cè)谄聊恢锌吹降臇|西,例如當(dāng)界面展示ProgressBar時(shí)的loading state。
很關(guān)鍵的一點(diǎn)是,我們前端開發(fā)人員傾向?qū)W⒂赨I。這不一定是壞事,因?yàn)橐粋€(gè)好的UI體驗(yàn)決定了用戶是否會(huì)用你的產(chǎn)品,從而決定了產(chǎn)品能否獲得成功。但是看看上述的MVP示例代碼(不是使用了PersonModel的那個(gè)例子),這里UI的狀態(tài)由Presenter進(jìn)行協(xié)調(diào),Presenter負(fù)責(zé)告訴View層如何進(jìn)行展示。
MVVM亦然,我想在本文中對(duì)MVVM的兩種實(shí)現(xiàn)方式進(jìn)行區(qū)分:第一種依賴DataBinding庫,第二種則依賴RxJava;對(duì)于依賴DataBinding的前者,其狀態(tài)被直接定義于ViewModel中:
class PersonsViewModel {
ObservableBoolean loading;
// 省略...
public void load(){
loading.set(true);
backend.loadPersons(new Callback(){
public void onSuccess(List<Person> persons){
loading.set(false);
// 省略其它代碼,比如對(duì)persons進(jìn)行渲染
}
public void onError(Throwable error){
loading.set(false);
// 省略其它代碼,比如展示錯(cuò)誤信息
}
});
}
}
使用RxJava實(shí)現(xiàn)MVVM的方式中,其并不依賴DataBinding引擎,而是將Observable和UI的控件進(jìn)行綁定,例如:
class RxPersonsViewModel {
private PublishSubject<Boolean> loading;
private PublishSubject<List<Person> persons;
private PublishSubject loadPersonsCommand;
public RxPersonsViewModel(){
loadPersonsCommand.flatMap(ignored -> backend.loadPersons())
.doOnSubscribe(ignored -> loading.onNext(true))
.doOnTerminate(ignored -> loading.onNext(false))
.subscribe(persons)
// 實(shí)現(xiàn)方式并不惟一
}
// 在View層訂閱它 (比如 Activity / Fragment)
public Observable<Boolean> loading(){
return loading;
}
// 在View層訂閱它 (比如 Activity / Fragment)
public Observable<List<Person>> persons(){
return persons;
}
// 每當(dāng)觸發(fā)此操作 (即調(diào)用 onNext()) ,加載Persons數(shù)據(jù)
public PublishSubject loadPersonsCommand(){
return loadPersonsCommand;
}
}
當(dāng)然,這些代碼并非完美,您的實(shí)現(xiàn)方式可能截然不同;我想說明的是,通常在MVP或者MVVM中,狀態(tài) 是由ViewModel或者Presenter進(jìn)行驅(qū)動(dòng)的。
這導(dǎo)致下述情況的發(fā)生:
1.業(yè)務(wù)邏輯本身也擁有了狀態(tài),
Presenter(或者ViewModel)本身也擁有了狀態(tài)(并且,你還需要通過代碼去同步它們的狀態(tài)使其保持一致),同時(shí),View可能也有自己的狀態(tài)(比方說,調(diào)用View的setVisibility()方法設(shè)置其可見性,或者Android系統(tǒng)在重新創(chuàng)建時(shí)從bundle恢復(fù)狀態(tài))。2.
Presenter(或ViewModel)有任意多個(gè)輸入(View層觸發(fā)行為并交給Presenter處理),這是ok的,但同時(shí)Presenter也有很多輸出(或MVP中的輸出通道,如view.showLoading()或view.showError();在MVVM中,ViewModel的實(shí)現(xiàn)中也提供了多個(gè)Observable,這最終導(dǎo)致了View層,Presenter層和業(yè)務(wù)邏輯中狀態(tài)的沖突,在處理多線程的時(shí)候,這種情況更明顯。
在好的情況下,這只會(huì)導(dǎo)致視覺上的錯(cuò)誤,例如同時(shí)顯示加載指示符(“加載狀態(tài)”)和錯(cuò)誤指示符(“錯(cuò)誤狀態(tài)”),如下所示:

在最糟糕的情況下,您從崩潰報(bào)告工具(如Crashlytics)接收到了一個(gè)嚴(yán)重的錯(cuò)誤報(bào)告,但您無法重現(xiàn)這個(gè)錯(cuò)誤,因此也幾乎無從著手去修復(fù)它。
如果從 底層 (業(yè)務(wù)邏輯層)到 頂層 (UI視圖層),有且僅有一個(gè)真實(shí)描述狀態(tài)的源,會(huì)怎么樣呢?事實(shí)上,我們已經(jīng)在文章的開頭談?wù)?code>Model的時(shí)候,就已經(jīng)通過案例,把相似的概念展示了出來:
class PersonsModel {
final boolean loading;
final List<Person> persons;
final Throwable error;
public(boolean loading, List<Person> persons, Throwable error){
this.loading = loading;
this.persons = persons;
this.error = error;
}
}
你猜怎么了? Model映射了狀態(tài),當(dāng)我想通了這點(diǎn),許多狀態(tài)相關(guān)的問題迎刃而解(甚至在編碼之前就已經(jīng)被避免了);現(xiàn)在Presenter層變得只有一個(gè)輸出了:
getView().render(PersonsModel)
它對(duì)應(yīng)了一個(gè)數(shù)學(xué)上簡(jiǎn)單的函數(shù),比如f(x) = y,對(duì)于多個(gè)輸入的函數(shù),對(duì)應(yīng)的則是f(a,b,c),但也是一個(gè)輸出。
并非對(duì)所有人來說數(shù)學(xué)都是香茗,就好像數(shù)學(xué)家并不清楚bug是什么——但軟件工程師需要去品嘗它。
了解Model到底是什么以及如何建立對(duì)應(yīng)的Model非常重要,因?yàn)樽罱KModel可以解決 狀態(tài)問題。
2.屏幕方向的改變
譯者注:針對(duì) 屏幕旋轉(zhuǎn)后的狀態(tài)回溯 這個(gè)問題,已經(jīng)可以通過Google官方發(fā)布的
ViewModel組件進(jìn)行處理,開發(fā)者不再需要為此煩惱,但本章節(jié)仍值得一讀。
Android設(shè)備上的 屏幕旋轉(zhuǎn) 是一個(gè)有足夠挑戰(zhàn)性的問題;忽視它是一個(gè)最簡(jiǎn)單的解決方案,即 每次屏幕旋轉(zhuǎn),都對(duì)數(shù)據(jù)重新進(jìn)行加載 。這確實(shí)行之有效,大多數(shù)情況下,您的APP也在離線狀態(tài)下工作,其數(shù)據(jù)來源于數(shù)據(jù)庫或者其它本地緩存,這意味著屏幕旋轉(zhuǎn)后的數(shù)據(jù)加載速度是很快的。
但是,個(gè)人而言我不喜歡看到加載框,哪怕加載速度是毫秒級(jí)別的,因?yàn)槲艺J(rèn)為這并非完美的用戶體驗(yàn),因此大家(包括我)開始使用MVP,這其中包括了 保留性的Presenter——這樣就可以 在屏幕旋轉(zhuǎn)時(shí)分離和銷毀View層,而Presenter則會(huì)保存在內(nèi)存中不會(huì)被銷毀,然后View層會(huì)再次連接到Presenter。
使用RxJava的MVVM也可以實(shí)現(xiàn)相同的概念,但請(qǐng)牢記,一旦View對(duì)ViewModel取消了訂閱,可觀察的流就會(huì)被銷毀,這個(gè)問題你可以用Subject解決;對(duì)于DataBinding構(gòu)建的MVVM來講,ViewModel由DataBinding直接綁定到View層,為了避免內(nèi)存泄露,需要我們?cè)谄聊恍D(zhuǎn)時(shí)及時(shí)銷毀ViewModel。
對(duì)于 保留性的Presenter 或者 ViewModel 的問題是: 我們?nèi)绾螌?code>View的狀態(tài)在屏幕旋轉(zhuǎn)之后回溯,保證View和Presenter再次回到之前相同的狀態(tài)?我編寫了一個(gè)名為 Mosby 的MVP庫,其包含一個(gè)名為ViewState的功能,它基本上將業(yè)務(wù)邏輯的狀態(tài)與View同步。 Moxy,另一個(gè)MVP庫,提出了一個(gè)非常有趣的解決方案——通過使用commands在屏幕方向更改后重現(xiàn)View的狀態(tài):

針對(duì)View層狀態(tài)的問題,我很確定還有其他的解決方案。讓我們退后一步,歸納一下這些庫試圖解決的問題:那就是我們已經(jīng)討論過的 狀態(tài)問題。
再次重申,我們通過一個(gè) 能反映當(dāng)前狀態(tài)的Model 和一個(gè)渲染Model的方法 解決了這個(gè)問題,就像調(diào)用getView().render(PersonsModel)一樣簡(jiǎn)單。
3.在頁面堆棧中導(dǎo)航
當(dāng)View不再使用時(shí),是否還有保留Presenter(或ViewModel)的必要?比如,用戶跳轉(zhuǎn)到了另外一個(gè)界面,這導(dǎo)致Fragment(View)被另外的Fragment給replace了,因此Presenter已經(jīng)不在被任何View持有。
如果沒有View層和Presenter進(jìn)行關(guān)聯(lián),Presenter自然也無法根據(jù)業(yè)務(wù)邏輯,將最新的數(shù)據(jù)反映在View上。但如果用戶又回來了怎么辦(比如按下后退按鈕),是 重新加載數(shù)據(jù) 還是 重用現(xiàn)有的Presenter?——這看起來像是一個(gè)哲學(xué)問題。
通常用戶一旦回到之前的界面,他會(huì)期望回到之前的界面繼續(xù)操作。這仍然像是第二小節(jié)關(guān)于View層 狀態(tài)恢復(fù) 的問題,解決方案簡(jiǎn)明扼要:當(dāng)用戶返回時(shí),我們得到 代表狀態(tài)的Model ,然后只需調(diào)用 getView().render(PersonsModel) 對(duì)View層進(jìn)行渲染。
4.進(jìn)程終止
進(jìn)程終止是一件壞事,并且我們需要依賴一些庫以幫助我們?cè)谶M(jìn)程終止后對(duì)狀態(tài)進(jìn)行恢復(fù)——我認(rèn)為這是Android開發(fā)中常見的一種誤解。
首先,進(jìn)程終止的原因只有一個(gè),并且有足夠充分的理由——Android操作系統(tǒng)需要更多資源用于其他應(yīng)用程序或節(jié)省電池。如果你的APP處于前臺(tái)并且正在被用戶主動(dòng)使用時(shí),這種情況永遠(yuǎn)不會(huì)發(fā)生,因此,遵紀(jì)守法,不要與平臺(tái)作斗爭(zhēng)了(就是不要執(zhí)拗于所謂的進(jìn)程?;盍耍?。如果你真的需要在后臺(tái)進(jìn)行一些長(zhǎng)時(shí)間的工作,請(qǐng)使用Service,這也是向操作系統(tǒng)發(fā)出信號(hào),告知您的App仍處于“主動(dòng)使用狀態(tài)”的 唯一方式 。
如果進(jìn)程終止了,Android會(huì)提供一些回調(diào)以供 保存狀態(tài),比如onSaveInstanceState()——沒錯(cuò),又是 狀態(tài) 。我們應(yīng)該將View的信息保存在Bundle中嗎?我們是否也應(yīng)該把Presenter中的狀態(tài)保存到Bundle中?那么業(yè)務(wù)邏輯的狀態(tài)呢?又是老生常談的問題,就和上面三個(gè)小節(jié)談到的一樣。
我們只需要一個(gè)代表整個(gè)狀態(tài)的Model類,我們很容易將Model保存在Bundle中并在之后對(duì)它進(jìn)行恢復(fù)。但是,我個(gè)人認(rèn)為大部分情況下最好不保存狀態(tài),而是 重新加載整個(gè)界面,就像我們第一次啟動(dòng)App一樣。 想想顯示新聞列表的 NewsReader App。 當(dāng)App被殺掉,我們保存了狀態(tài),6小時(shí)后用戶重新打開App并恢復(fù)了狀態(tài),我們的App可能會(huì)顯示過時(shí)的內(nèi)容。因此,這種情況下,也許不存儲(chǔ)Model和狀態(tài)、而對(duì)數(shù)據(jù)重新加載才是更好的策略。
5.單向數(shù)據(jù)流的不變性
在這里我不打算討論不變性(immutabiliy)的優(yōu)勢(shì),因?yàn)橛泻芏噘Y源討論這個(gè)問題。我們想要一個(gè)不可變的Model(代表狀態(tài))。為什么?因?yàn)槲覀兿胍ㄒ坏臓顟B(tài)源,在傳遞Model時(shí),我們不希望App中的其他組件可以改變我們的Model或者State。
讓我們假設(shè)編寫一個(gè)簡(jiǎn)單的計(jì)數(shù)器App,它具有遞增和遞減的功能按鈕,并在TextView中顯示當(dāng)前計(jì)數(shù)器值。 如果我們的Model(在這種情況下只是計(jì)數(shù)器值,即一個(gè)整數(shù))是不可變的,那么我們?nèi)绾胃挠?jì)數(shù)器?
我很高興被問到這個(gè)問題,按鈕被點(diǎn)擊時(shí),我們并非直接操作TextView。我的建議是:
- 1.我們的
View層應(yīng)該有一個(gè)類似view.render(...)的方法; - 2.我們的
Model是不可變的,因此不可直接修改Model; - 3.
View的渲染有且只有一個(gè)來源:即業(yè)務(wù)邏輯。
我們將點(diǎn)擊事件 下沉 到業(yè)務(wù)邏輯層。業(yè)務(wù)邏輯知道當(dāng)前的Model(例如,持有一個(gè)私有的成員Model,它代表著當(dāng)前的狀態(tài)), 這之后根據(jù)舊的Model,創(chuàng)建一個(gè)新的帶有增量/減量值的Model。

這樣我們建立了一個(gè) 單向數(shù)據(jù)流,業(yè)務(wù)邏輯作為單一源用于創(chuàng)建不可變的Model實(shí)例,但對(duì)于一個(gè)計(jì)數(shù)器來講未免有點(diǎn)小題大做,不是嗎?誠(chéng)然,是的,計(jì)數(shù)器只是一個(gè)簡(jiǎn)單的應(yīng)用程序。大多數(shù)應(yīng)用程序都是以簡(jiǎn)單的應(yīng)用程序開始,但復(fù)雜性增長(zhǎng)很快——從我的角度來看,單向數(shù)據(jù)流和不可變模型是必要的,這會(huì)使簡(jiǎn)單的應(yīng)用程序,在復(fù)雜性遞增的同時(shí),依然保持著簡(jiǎn)單(對(duì)開發(fā)者而言)。
6.可調(diào)試和可重現(xiàn)的狀態(tài)
此外,單向數(shù)據(jù)流保證了我們的應(yīng)用程序易于調(diào)試。下次我們從Crashlytics獲得崩潰報(bào)告時(shí),我們可以輕松地重現(xiàn)并修復(fù)此崩潰,因?yàn)樗斜匦璧男畔⒍家迅郊拥奖罎?bào)告中了。
什么叫做必需的信息?那就是當(dāng)前的Model和用戶用戶在崩潰發(fā)生時(shí)想要執(zhí)行的操作(比如,點(diǎn)擊減量按鈕)。這就是我們重現(xiàn)這次崩潰所需的全部信息,這些信息非常容易收集并附加在崩潰報(bào)告中。
如果沒有單項(xiàng)數(shù)據(jù)流(比如,對(duì)EventBus的濫用,或者將CounterModels的私有域暴露出來),或者沒有不變性(這會(huì)導(dǎo)致我們不知道誰實(shí)際更改了Model),那么bug的復(fù)現(xiàn)就沒那么容易了。
7.可測(cè)試性
“傳統(tǒng)”的MVP或MVVM提高了應(yīng)用程序的可測(cè)試性。MVC也是可測(cè)試的:沒有人說我們必須將所有業(yè)務(wù)邏輯放入Activity中。使用表示狀態(tài)的Model,我們可以簡(jiǎn)化單元測(cè)試的代碼,因?yàn)槲覀兛梢院?jiǎn)單地檢查assertEqual(expectedModel,model)。這使我們避免了許多必須要Mock的對(duì)象。
此外,這也減少了很多驗(yàn)證的測(cè)試,即某些方法是否被調(diào)用(比如Mockito.verify(view, times(1)).showFoo()),最終,這使得我們的單元測(cè)試代碼更具可讀性,易于理解并且易于維護(hù),因?yàn)槲覀儾槐靥幚砗芏鄬?shí)際代碼的實(shí)現(xiàn)細(xì)節(jié)。
總結(jié)
在這個(gè)博客文章系列的第一部分中,我們談了很多關(guān)于理論的東西。我們真的需要關(guān)于專門討論Model的博客嗎?
我認(rèn)為初步地理解Model的確很重要,這也有助于我們避免一些會(huì)遇到的問題。Model并不意味著業(yè)務(wù)邏輯,它是生成Model的業(yè)務(wù)邏輯(比如,一次交互,一個(gè)用例,一個(gè)倉庫或者你在APP中調(diào)用的任何東西)。
在接下來的第二部分中,當(dāng)我們最終使用Model-View-Intent構(gòu)建一個(gè)響應(yīng)式App 時(shí),我們將看到Model的實(shí)際應(yīng)用。演示的APP是一個(gè)虛構(gòu)的在線商店的應(yīng)用程序,敬請(qǐng)關(guān)注。

系列目錄
《使用MVI打造響應(yīng)式APP》原文
《使用MVI打造響應(yīng)式APP》譯文
- [譯]使用MVI打造響應(yīng)式APP(一):Model到底是什么
- [譯]使用MVI打造響應(yīng)式APP[二]:View層和Intent層
- [譯]使用MVI打造響應(yīng)式APP[三]:狀態(tài)折疊器
- [譯]使用MVI打造響應(yīng)式APP[四]:獨(dú)立性UI組件
- [譯]使用MVI打造響應(yīng)式APP[五]:輕而易舉地Debug
- [譯]使用MVI打造響應(yīng)式APP[六]:恢復(fù)狀態(tài)
- [譯]使用MVI打造響應(yīng)式APP[七]:掌握時(shí)機(jī)(SingleLiveEvent問題)
- [譯]使用MVI打造響應(yīng)式APP[八]:導(dǎo)航
《使用MVI打造響應(yīng)式APP》實(shí)戰(zhàn)
關(guān)于我
Hello,我是卻把清梅嗅,如果您覺得文章對(duì)您有價(jià)值,歡迎 ??,也歡迎關(guān)注我的博客或者Github。
如果您覺得文章還差了那么點(diǎn)東西,也請(qǐng)通過關(guān)注督促我寫出更好的文章——萬一哪天我進(jìn)步了呢?