我曾經(jīng)有一個瞬間覺的我的Model定義全都是錯的。經(jīng)過在各種安卓開發(fā)論壇也好主題也罷的討論和頭疼的研究啡浊。無論如何岂座,最終我選擇使用rxjava和Model-View-Intent(MVI)的方式構(gòu)建響應式的安卓應用程序肪凛,就像這種組合我以前是沒有嘗試過一樣,我創(chuàng)建是十分被動的辽社。當然伟墙,你也會,但是滴铅,你會比我好很多戳葵,因為,我將寫一系列文章來介紹這個模式和用法汉匙。在第一節(jié)拱烁,也就是這篇文章生蚁,我們來說說我們的Model出現(xiàn)了什么問題?
我為什么說我以前定義的Model全都是錯的咧?誠然戏自,有很多模式將"View"和"Model"分離邦投。在安卓開發(fā)領(lǐng)域,最出名的當屬Model-View-Controller(MVC),Model-View_Presenter(MVP)和Model-View-ViewModel(MVVM)擅笔。你可以從名字看出什么東西么志衣?他們都有Model。但是猛们,我發(fā)現(xiàn)大多數(shù)時間念脯,我根本沒有用Model。
例子:僅僅是在后臺加載一個persons的列表弯淘,一個傳統(tǒng)的MVP模式的代碼是這樣的:
class PersonsPresenter extends Presenter<PersonsView> {
public void load(){
getView().showLoading(true); // 顯示一個加載進度條
backend.loadPersons(new Callback(){
public void onSuccess(List<Person> persons){
getView().showPersons(persons); // 顯示人列表
}
public void onError(Throwable error){
getView().showError(error); // 顯示錯誤信息
}
});
}
}
但是到底什么是"Model"?后臺運行是Model?不是绿店,Model應當是業(yè)務邏輯。它是作為結(jié)果的列表庐橙?不是假勿,它僅僅只做一件事情,就是我們View顯示所需要的東西怕午,像加載指示器或錯誤信息废登。因此,真正的Model“長”什么樣的郁惜?
如果按照我對Model的理解堡距,那么,Model類應當是這樣的:
class PersonsModel {
// 在真實的項目中兆蕉,需要定義為私有的
// 并且我們需要通過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;
}
}
那么Presenter應該“長”這樣的:
class PersonsPresenter extends Presenter<PersonsView> {
public void load(){
getView().render( new PersonsModel(true, null, null) ); // 顯示一個加載進度條
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) ); // 顯示錯誤信息
}
});
}
}
現(xiàn)在在屏幕上的View有了一個將被渲染上去的Model羽戒。這個概念其實不是什么新概念。最開始的被Trygve Reenskaug在1979年定義的MVC模式的Model的定義幾乎一致:View觀察Model的變化虎韵。不幸的是易稠,MVC這個術(shù)語被濫用來描述太多不同的模式,它們與最原始的MVC定義有了出入包蓝。例如驶社,后端工程師使用MVC框架,iOS工程師有ViewController测萎,在安卓開發(fā)中MVC的真正含義是什么亡电?Activities是Controller?那么ClickListener意味著什么?現(xiàn)在MVC與最初被Reenskaug定義的MVC來講,這個術(shù)語被誤解硅瞧,濫用和錯誤使用份乒。關(guān)于MVC的討論就此打住,在討論下去文章就要失控翻車了。
讓我們回到我剛開始說的地方或辖。Model需要解決我們在安卓開發(fā)中經(jīng)常遇到的問題:
- 狀態(tài)問題
- 屏幕方向問題
- 在頁面堆棧中導航
- 進程死亡
- 單向數(shù)據(jù)流的不變性
- 可調(diào)試和可重現(xiàn)的狀態(tài)
- 測試
讓我們討論的上面這些點瘾英,并研究傳統(tǒng)的MVP和MVVM如何處理這些內(nèi)容,最后颂暇,在探究到底什么樣的Model可以幫助避免共性的陷阱缺谴。
1.狀態(tài)問題
響應式App,可以說最近非常流行蟀架。難道不是么瓣赂?所謂的響應式App應該就是會根據(jù)應用的狀態(tài)改變,來改變UI片拍。這里還有一個單詞:"State(上文譯為狀態(tài))"煌集。什么是"State(上文譯為狀態(tài))"?大多數(shù)時間我們描述“State(上文譯為狀態(tài))”,就是我們從屏幕上看到的東西捌省,比如說在屏幕上顯示一個ProgressBar
就是“加載狀態(tài)”苫纤。最關(guān)鍵的地方:我們的前端開發(fā)者趨向于關(guān)注UI。這明顯不是一件壞事纲缓,因為一個好的UI決定了用戶會不會用你們家的產(chǎn)品卷拘,從而決定了產(chǎn)品能不能成功。但是祝高,我們看一下上面最基本的MVP示例代碼(不是用PersonsModel的例子栗弟,是最上面的例子)。Ui的狀態(tài)被Presenter協(xié)調(diào)工闺,Presenter決定了View應該顯示什么內(nèi)容乍赫。MVVM也是同樣的。在這篇博客中我簡單區(qū)分兩種MVVM實現(xiàn):第一種是用到了Android的data binding陆蟆,第二種是用到RxJava雷厂。在用data binding實現(xiàn)的MVVM這種方式下,狀態(tài)直接被定義到了ViewModel里面:
class PersonsViewModel {
ObservableBoolean loading;
// ... Other fields left out for better readability
public void load(){
loading.set(true);
backend.loadPersons(new Callback(){
public void onSuccess(List<Person> persons){
loading.set(false);
// ... other stuff like set list of persons
}
public void onError(Throwable error){
loading.set(false);
// ... other stuff like set error message
}
});
}
}
在使用RxJava實現(xiàn)的MVVM中叠殷,我們不需要使用data binding引擎改鲫,而是將Observable綁定到View中的UI Widget,例如:
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)
// Could also be implemented entirely different
}
// Subscribed to in View (i.e. Activity / Fragment)
public Observable<Boolean> loading(){
return loading;
}
// Subscribed to in View (i.e. Activity / Fragment)
public Observable<List<Person>> persons(){
return persons;
}
// Whenever this action is triggered (calling onNext() ) we load persons
public PublishSubject loadPersonsCommand(){
return loadPersonsCommand;
}
}
當然林束,這只是一個代碼片段不是一個完整的代碼像棘,你實現(xiàn)的可能看起來完全不一樣。重點是通常在MVP和MVVM中壶冒,狀態(tài)由Presenter或ViewModel驅(qū)動缕题。
這導致了下面幾個問題:
- 業(yè)務邏輯有了自己的狀態(tài),Presenter(或ViewModel)有了自己的狀態(tài)(你需要同步你的業(yè)務邏輯狀態(tài)依痊,和你的Presenter的狀態(tài)避除,兩者需要保持一致)并且View可能也有自己的狀態(tài)(舉個栗子,您直接在視圖中設置可見性胸嘁,或者Android本身在從bundle中恢復狀態(tài))
- Presenter(或者ViewModel)有任意多的輸入(View的觸發(fā)瓶摆,被Presenter處理),這是可以理解的,但是Presenter也有很多的輸出(或輸出一些像
view.showLoading()
或view.showError()
在MVP或ViewModel都提供觀察)那么這種情況會導致View,Presenter和業(yè)務邏輯的狀態(tài)沖突性宏,這種現(xiàn)象在多線程下尤為突出群井。
在最好的情況下,這只會導致可見的錯誤毫胜,例如像這樣同時顯示加載指示符(“加載狀態(tài)”)和錯誤指示符(“錯誤狀態(tài)”)书斜。
在最壞的情況下,你有一個像Crashlytics(理解成bugly)這樣的崩潰報告工具報告給你的嚴重的錯誤酵使,你無法重現(xiàn)荐吉,因此幾乎不可能修復。
如果口渔,我們從底層(業(yè)務邏輯)到頂層(VIew)有且僅有一個狀態(tài)源样屠。其實,我們最開始展示的第二個例子就是一個很接近這個概念的例子缺脉。
class PersonsModel {
// 在真實的項目中痪欲,需要定義為私有的
// 并且我們需要通過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;
}
}
你猜怎么了? 模型反應了狀態(tài) 。當我理解了這個攻礼,那么多個狀態(tài)依賴的問題就被解決了(從一開始就阻止了)业踢,并且我的Presenter也就只有一個明確的輸出:getView().render(PersonsModel) .這反應了一個簡單的數(shù)學函數(shù)像f(x)=y (也可以有多個輸入,例如f(a,b,c),但只有一個輸出)礁扮。數(shù)學并不是所有的人都擅長知举,但是,數(shù)學家不知道什么是Bug深员。軟件工程師咧负蠕。
理解什么是"Model",并且知道m(xù)odel如何正確的定義倦畅,是十分重要的遮糖,因為到最后Model將解決"狀態(tài)問題"。
2.屏幕方向改變
安卓屏幕方向改變是一個有挑戰(zhàn)性性的問題叠赐。最簡單的方法是直接忽略這個問題欲账。當屏幕方向改變的時候,重新加載所有的東西芭概。這是完全有效的解決方法赛不。大多數(shù)時間,你的App在離線狀態(tài)下工作罢洲,數(shù)據(jù)是存儲在你的本地數(shù)據(jù)庫或者其他的本地緩存踢故。因此文黎,當屏幕的方向發(fā)生改變,加載數(shù)據(jù)是很快的殿较、然而耸峭,我個人不喜歡看到loading指示器(大神都是有點各種小脾氣的),盡管它可能只出現(xiàn)幾微秒的時間(這里應該用了夸張的修辭手法)淋纲,因為在我看來這不是一個無縫的用戶體驗劳闹。因此,很多人(包括我)開始使用帶有“固定的presenter”的MVP洽瞬。因此View可以在屏幕方向旋轉(zhuǎn)的時候被分離(被銷毀)本涕,而presenter將被保留在內(nèi)存中,隨后伙窃,我們的View和Presenter將會被重新連接菩颖。在用RxJava實現(xiàn)的MVVM中有相同的概念,但是为障,我們需要記在心中的是一旦View被它的ViewModel退訂那么觀察流就被破壞位他。例如,你可以用Subjects來解決這個問題产场。在使用data binding實現(xiàn)的MVVM中ViewModel是通過data binding 引擎直接綁定在View上的鹅髓。去避免當我們改變屏幕方向而導致的內(nèi)存泄露。
但是固定的Presenter(或者ViewModel)有一個問題是:當屏幕旋轉(zhuǎn)的時候京景,我們?nèi)绾螌iew的狀態(tài)退回旋轉(zhuǎn)前的狀態(tài)窿冯,也就是說,我們的View和Presenter是否處在相同的狀態(tài)确徙?我寫了一個MVP庫叫做Mosby
帶有一個功能叫做ViewState ,用來同步業(yè)務邏輯和View的狀態(tài)醒串。Moxy
,另一個MVP庫,用了一種有趣的方式解決了這個問題鄙皇,解決的方法就是用到了"命令(原文為commands)"去在屏幕旋轉(zhuǎn)以后芜赌,重建View的狀態(tài):
我可以十分確定的是,肯定有其他方法來解決這個問題伴逸。讓我們退一步來說缠沈,我們總結(jié)一下上面說到的庫的解決方法:他們試圖解決我們一直在討論的狀態(tài)問題。
所以错蝴,再次強調(diào)洲愤,當有一個能夠反應確切的"狀態(tài)"的"Model",肯定只有一個方法去"渲染(原文為render)"這個"Model"解決這個問題顷锰,并且是通過一種簡單的如調(diào)用 getView().render(PersonsModel) 一樣柬赐。
3.在頁面堆棧中導航
Presenter(或者ViewModel)需要去維護什么時候View不使用么?舉個栗子,如果官紫,F(xiàn)ragment(View)將被另一個Fragment替換掉肛宋,因為用戶導航到另外的頁面州藕,那么這個將沒有View附屬到Presenter里。如果沒有View沒有Presenter顯然不可能用最新的從業(yè)務邏輯里出來的數(shù)據(jù)去更新View酝陈。如果用戶返回(例如慎框,用戶按了返回按鈕)?去重新加載數(shù)據(jù)或復用已經(jīng)存在的Presenter?這是一道哲學題后添。通常的一旦用戶返回先前的頁面,他期望回到他原來閱讀的地方薪丁。這是個最基本的“重置View狀態(tài)的問題”遇西,我們剛剛在2中也討論了這個問題。所以富有策略的解決方案:當"Model"代表一種狀態(tài)严嗜,我們僅僅需要當用戶返回時粱檀,調(diào)用getView().render(PersonModel) 去渲染視圖就可以了。
4.進程死亡
我認為這是個安卓開發(fā)普遍錯誤的理解漫玄,就是進程死亡是意見壞的事情茄蚯,并且在進程死亡以后,我們需要庫去幫助我們重啟狀態(tài)(例如Presenters或者ViewModels)睦优。第一渗常,一個進程死亡發(fā)生的原因是:安卓操作系統(tǒng)需要更多的資源去給其他的App或者為了省電。但是汗盘,如果你的應用程序處于前臺皱碘,正在被你的用戶使用是決定不可能出現(xiàn)進程死亡的。因此隐孽,做個好市民癌椿,不要在和平臺對戰(zhàn)了(這里的意思是不要再瞎折騰進程包活了)。如果你真的需要在后臺長時間運行的一些工作,請用 Service 菱阵,在安卓操作系統(tǒng)中踢俄,這是唯一的一種方式向系統(tǒng)發(fā)出你的應用程序仍然被使用的信號。如果一個進程死亡發(fā)生晴及,安卓提供了一些回調(diào)像onSaveInstanceState() 去保存狀態(tài)都办。State又出現(xiàn)了。我們應該保存我們的View信息到Bundle里么?我們的Presenter的狀態(tài)是不是也要存儲到Bundle里?那么業(yè)務邏輯的狀態(tài)要不要存?我們先前也一直討論這個問題:剛才1.2.3點都在討論這個問題虑稼。我們僅僅需要一個Model類脆丁,這個Model類代表了整個狀態(tài)。那么存儲到Bundle里动雹,就變得很簡單了槽卫。然而,我個人意見認為大多數(shù)時間我們不存儲狀態(tài)數(shù)據(jù)胰蝠,而選擇像我們啟動App時候重新加載整個屏幕歼培,似乎更好震蒋。考慮一下新聞閱讀軟件顯示新聞列表躲庄,當我們App六小時以前被殺死查剖,我們存儲了的新聞狀態(tài),當用戶重新打開我們的App的時候噪窘,我們六小時前存儲的狀態(tài)被重新顯示出來笋庄,很顯然新聞已經(jīng)過期了。也許在這種場景下倔监,不去存儲狀態(tài)(Model/State)直砂,而去重新加載數(shù)據(jù)是更好的選擇。
5. 單向不可變的數(shù)據(jù)流
我這里不去討論不變性(immutabiliy)的先進性浩习,因為有很多資源討論這個問題静暂。我們需要一個不變的“Model”(代表狀態(tài))。為啥谱秽?因為我們想要唯一的來源洽蛀。當我們傳遞Model對象的時候,我們不想要在我們應用中其他組件去改變我們的Model/狀態(tài)(State)疟赊。讓我想象一下我們正在寫一個簡單的“計數(shù)器”的安卓應用程序郊供,這個有一個增量和一個減量按鈕,并且在一個TextView中顯示當前技術(shù)的值近哟。如果我們的Model(就是計數(shù)的值颂碘,一個Integer)是不可變的,我們?nèi)绾稳ジ挠嫈?shù)器椅挣?我要告訴你头岔,我們不直接通過按鈕點擊來控制TextView。一些建議:第一鼠证,我們的View應該有一個view.render(...).第二峡竣,我們的Model是不可變的,因此不可能直接修改Model量九。第三适掰,有且只有一個來源:業(yè)務邏輯。我們讓點擊事件“下沉”到業(yè)務邏輯層荠列。業(yè)務邏輯知道了當前的Model(例如类浪,當前Model有一個私有域)并且將根據(jù)舊的Model,創(chuàng)建一個新的帶有增量/減量值的Model肌似。
通過這樣做费就,我們確信有單向的數(shù)據(jù)流,并將業(yè)務邏輯作為創(chuàng)建不可變模型實例的單一的來源川队。對于一個計數(shù)器來講有點太小題大做力细。難道不是么睬澡?是的,一個計數(shù)器是一個非常簡單的程序眠蚂。大多數(shù)的App都是從一個簡單的程序變的復雜起來煞聪。我認為,一個單向的數(shù)據(jù)流和一個不變的Model是十分必要的逝慧,當我們工程變復雜的時候昔脯,開發(fā)將依然是簡單的。
6. 可調(diào)試和可重現(xiàn)的狀態(tài)
此外笛臣,單向數(shù)據(jù)流保證了我們的APP的調(diào)試非常簡單云稚。下次如果有新的crash報告從Crashlytics(感覺類似與Bugly)傳過來,我們可以很快速的修復Crash捐祠。因為所有需要的信息都會在crash報告里面。什么是“需要的信息”桑李?就是我們需要的當前Model和用戶執(zhí)行了什么樣的操作而導致的八阿哥(例子:點擊減量按鈕)踱蛀。這就是我們需要的信息,而且這些信息很顯然贵白,是十分容易附加到Crash報告中的率拒。如果,數(shù)據(jù)流不是單向的禁荒,那么實現(xiàn)起來就有點困難猬膨。(例如:一些人亂用EventBus,并且將CounterModels暴露出來。譯者:EventBus沒用過呛伴,所以勃痴,這里可能看起來怪怪的原話是someone misuses an EventBus and fires CounterModels out into the wild )或不具有不變性(這樣會導致我們不能確定誰改變了Model)。
7. 可測試
"傳統(tǒng)"MVP或者MVVM改善了應用程序的可測試性热康。MVC也是可測試的:沒人告訴我們業(yè)務邏輯一定要放在activity里沛申。當Model代表狀態(tài),我們可以簡化我們的集成測試代碼姐军,例如铁材,我們可以簡單的檢查assertEquals(expectedModel, model) 。這讓我們除了Model以外的所有對象都不用mock奕锌。另外著觉,這可以消除了方法的許多驗證測試,例如 Mockito.verify(view, times(1)).showFoo() 惊暴。最后饼丘,它可以讓我們的測試代碼可讀性更好,更容易理解辽话,更好的可維護性葬毫,我們不需要糾結(jié)于如何實現(xiàn)在代碼中實現(xiàn)一些細節(jié)镇辉。
總結(jié)
作為這個系列的第一篇博客,我們討論了很多關(guān)于理論的東西贴捡。我們真的需要花4千多字介紹Model么?我認為理解Model的實現(xiàn)是十分重要的基礎(chǔ)忽肛,有助于防止一些問題,否則容易翻車烂斋。Model不意味著業(yè)務邏輯屹逛,它是生成Model的業(yè)務邏輯(例如,一個交互汛骂,一個用例罕模,一個倉庫或者你在APP中調(diào)用的任何東西(原文:Model doesn’t mean business logic. It’s the business logic (i.e. an Interactor, a Usecase, a Repositor or whatever you call it in your app) that produces a Model.)。在第二部分帘瞭,我們將要將我們學到的Model理論淑掌,用到Model-View-Intent上,來構(gòu)建響應式應用程序蝶念。下面展示的抛腕,簡單的在線商城軟件將是我們以后去實現(xiàn)的一個例子。你可以期望在第二部分了媒殉。 敬請關(guān)注担敌。