我曾經有一個瞬間覺的個人Model定義全都是錯的。通過在各類安卓開發論壇也好主題也罷的討論和頭疼的研究。不管如何,最終我選擇使用rxjava和Model-View-Intent(MVI)的方式構建響應式的安卓應用程序,就像這種組合我之前是沒有嘗試過同樣,我建立是十分被動的。固然,你也會,可是,你會比我好不少,由於,我將寫一系列文章來介紹這個模式和用法。在第一節,也就是這篇文章,咱們來講說咱們的Model出現了什麼問題?前端
我爲何說我之前定義的Model全都是錯的咧?誠然,有不少模式將"View"和"Model"分離。在安卓開發領域,最出名的當屬Model-View-Controller(MVC),Model-View_Presenter(MVP)和Model-View-ViewModel(MVVM)。你能夠從名字看出什麼東西麼?他們都有Model。可是,我發現大多數時間,我根本沒有用Model。java
例子:僅僅是在後臺加載一個persons的列表,一個傳統的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應當是業務邏輯。它是做爲結果的列表?不是,它僅僅只作一件事情,就是咱們View顯示所須要的東西,像加載指示器或錯誤信息。所以,真正的Model「長」什麼樣的?後端
若是按照我對View的理解,那麼,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) ); // 顯示錯誤信息
}
});
}
}
複製代碼
如今在屏幕上的View有了一個將被渲染上去的Model。這個概念其實不是什麼新概念。最開始的被Trygve Reenskaug在1979年定義的MVC模式的Model的定義幾乎一致:View觀察Model的變化。不幸的是,MVC這個術語被濫用來描述太多不一樣的模式,它們與最原始的MVC定義有了出入。例如,後端工程師使用MVC框架,iOS工程師有ViewController,在安卓開發中MVC的真正含義是什麼?Activities是Controller?那麼ClickListener意味着什麼?如今MVC與最初被Reenskaug定義的MVC來說,這個術語被誤解,濫用和錯誤使用。關於MVC的討論就此打住,在討論下去文章就要失控翻車了。app
讓咱們回到我剛開始說的地方。Model須要解決咱們在安卓開發中常常遇到的問題:框架
讓咱們討論的上面這些點,並研究傳統的MVP和MVVM如何處理這些內容,最後,在探究到底什麼樣的Model能夠幫助避免共性的陷阱。dom
響應式App,能夠說最近很是流行。難道不是麼?所謂的響應式App應該就是會根據應用的狀態改變,來改變UI。這裏還有一個單詞:"State(上文譯爲狀態)"。什麼是"State(上文譯爲狀態)"?大多數時間咱們描述「State(上文譯爲狀態)」,就是咱們從屏幕上看到的東西,好比說在屏幕上顯示一個ProgressBar
就是「加載狀態」。最關鍵的地方:咱們的前端開發者趨向於關注UI。這明顯不是一件壞事,由於一個好的UI決定了用戶會不會用大家家的產品,從而決定了產品能不能成功。可是,咱們看一下上面最基本的MVP示例代碼(不是用PersonsModel的例子,是最上面的例子)。Ui的狀態被Presenter協調,Presenter決定了View應該顯示什麼內容。MVVM也是一樣的。在這篇博客中我簡單區分兩種MVVM實現:第一種是用到了Android的data binding,第二種是用到RxJava。在用data binding實現的MVVM這種方式下,狀態直接被定義到了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實現的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;
}
}
複製代碼
固然,這只是一個代碼片斷不是一個完整的代碼,你實現的可能看起來徹底不同。重點是一般在MVP和MVVM中,狀態由Presenter或ViewModel驅動。
這致使了下面幾個問題:
view.showLoading()
或view.showError()
在MVP或ViewModel都提供觀察)那麼這種狀況會致使View,Presenter和業務邏輯的狀態衝突,這種現象在多線程下尤其突出。在最好的狀況下,這隻會致使可見的錯誤,例如像這樣同時顯示加載指示符(「加載狀態」)和錯誤指示符(「錯誤狀態」)
在最壞的狀況下,你有一個像Crashlytics(理解成bugly)這樣的崩潰報告工具報告給你的嚴重的錯誤,你沒法重現,所以幾乎不可能修復。
若是,咱們從底層(業務邏輯)到頂層(VIew)有且僅有一個狀態源。其實,咱們最開始展現的第二個例子就是一個很接近這個概念的例子。
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也就只有一個明確的輸出:getView().render(PersonsModel) .這反應了一個簡單的數學函數像f(x)=y (也能夠有多個輸入,例如f(a,b,c),但只有一個輸出)。數學並非全部的人都擅長,可是,數學家不知道什麼是Bug。軟件工程師咧。
理解什麼是"Model",而且知道model如何正確的定義,是十分重要的,由於到最後Model將解決"狀態問題"。
安卓屏幕方向改變是一個有挑戰性性的問題。最簡單的方法是直接忽略這個問題。當屏幕方向改變的時候,從新加載全部的東西。這是徹底有效的解決方法。大多數時間,你的App在離線狀態下工做,數據是存儲在你的本地數據庫或者其餘的本地緩存。所以,當屏幕的方向發生改變,加載數據是很快的、然而,我我的不喜歡看到loading指示器(大神都是有點各類小脾氣的),儘管它可能只出現幾微秒的時間(這裏應該用了誇張的修辭手法),由於在我看來這不是一個無縫的用戶體驗。所以,不少人(包括我)開始使用帶有「固定的presenter」的MVP。所以View能夠在屏幕方向旋轉的時候被分離(被銷燬),而presenter將被保留在內存中,隨後,咱們的View和Presenter將會被從新鏈接。在用RxJava實現的MVVM中有相同的概念,可是,咱們須要記在心中的是一旦View被它的ViewModel退訂那麼觀察流就被破壞。例如,你能夠用Subjects來解決這個問題。在使用data binding實現的MVVM中ViewModel是經過data binding 引擎直接綁定在View上的。去避免當咱們改變屏幕方向而致使的內存泄露。
可是固定的Presenter(或者ViewModel)有一個問題是:當屏幕旋轉的時候,咱們如何將View的狀態退回旋轉前的狀態,也就是說,咱們的View和Presenter是否處在相同的狀態?我寫了一個MVP庫叫作Mosby
帶有一個功能叫作ViewState ,用來同步業務邏輯和View的狀態。Moxy
,另外一個MVP庫,用了一種有趣的方式解決了這個問題,解決的方法就是用到了"命令(原文爲commands)"去在屏幕旋轉之後,重建View的狀態:
我能夠十分肯定的是,確定有其餘方法來解決這個問題。讓咱們退一步來講,咱們總結一下上面說到的庫的解決方法:他們試圖解決咱們一直在討論的狀態問題。
因此,再次強調,當有一個可以反應確切的"狀態"的"Model",確定只有一個方法去"渲染(原文爲render)"這個"Model"解決這個問題,而且是經過一種簡單的如調用 getView().render(PersonsModel) 同樣。
Presenter(或者ViewModel)須要去維護何時View不使用麼?舉個栗子,若是,Fragment(View)將被另外一個Fragment替換掉,由於用戶導航到另外的頁面,那麼這個將沒有View附屬到Presenter裏。若是沒有View沒有Presenter顯然不可能用最新的從業務邏輯裏出來的數據去更新View。若是用戶返回(例如,用戶按了返回按鈕)?去從新加載數據或複用已經存在的Presenter?這是一道哲學題。一般的一旦用戶返回先前的頁面,他指望回到他原來閱讀的地方。這是個最基本的「重置View狀態的問題」,咱們剛剛在2中也討論了這個問題。因此富有策略的解決方案:當"Model"表明一種狀態,咱們僅僅須要當用戶返回時,調用getView().render(PersonModel) 去渲染視圖就能夠了。
我認爲這是個安卓開發廣泛錯誤的理解,就是進程死亡是意見壞的事情,而且在進程死亡之後,咱們須要庫去幫助咱們重啓狀態(例如Presenters或者ViewModels)。第一,一個進程死亡發生的緣由是:安卓操做系統須要更多的資源去給其餘的App或者爲了省電。可是,若是你的應用程序處於前臺,正在被你的用戶使用是決定不可能出現進程死亡的。所以,作個好市民,不要在和平臺對戰了(這裏的意思是不要再瞎折騰進程包活了)。若是你真的須要在後臺長時間運行的一些工做,請用 Service ,在安卓操做系統中,這是惟一的一種方式向系統發出你的應用程序仍然被使用的信號。若是一個進程死亡發生,安卓提供了一些回調像onSaveInstanceState() 去保存狀態。State又出現了。咱們應該保存咱們的View信息到Bundle裏麼?咱們的Presenter的狀態是否是也要存儲到Bundle裏?那麼業務邏輯的狀態要不要存?咱們先前也一直討論這個問題:剛纔1.2.3點都在討論這個問題。咱們僅僅須要一個Model類,這個Model類表明了整個狀態。那麼存儲到Bundle裏,就變得很簡單了。然而,我我的意見認爲大多數時間咱們不存儲狀態數據,而選擇像咱們啓動App時候從新加載整個屏幕,彷佛更好。考慮一下新聞閱讀軟件顯示新聞列表,當咱們App六小時之前被殺死,咱們存儲了的新聞狀態,當用戶從新打開咱們的App的時候,咱們六小時前存儲的狀態被從新顯示出來,很顯然新聞已通過期了。也許在這種場景下,不去存儲狀態(Model/State),而去從新加載數據是更好的選擇。
我這裏不去討論不變性(immutabiliy)的先進性,由於有不少資源討論這個問題。咱們須要一個不變的「Model」(表明狀態)。爲啥?由於咱們想要惟一的來源。當咱們傳遞Model對象的時候,咱們不想要在咱們應用中其餘組件去改變咱們的Model/狀態(State)。讓我想象一下咱們正在寫一個簡單的「計數器」的安卓應用程序,這個有一個增量和一個減量按鈕,而且在一個TextView中顯示當前技術的值。若是咱們的Model(就是計數的值,一個Integer)是不可變的,咱們如何去更改計數器?我要告訴你,咱們不直接經過按鈕點擊來控制TextView。一些建議:第一,咱們的View應該有一個view.render(...).第二,咱們的Model是不可變的,所以不可能直接修改Model。第三,有且只有一個來源:業務邏輯。咱們讓點擊事件「下沉」到業務邏輯層。業務邏輯知道了當前的Model(例如,當前Model有一個私有域)而且將根據舊的Model,建立一個新的帶有增量/減量值的Model。
經過這樣作,咱們確信有單向的數據流,並將業務邏輯做爲建立不可變模型實例的單一的來源。對於一個計數器來說有點過小題大作。難道不是麼?是的,一個計數器是一個很是簡單的程序。大多數的App都是從一個簡單的程序變的複雜起來。我認爲,一個單向的數據流和一個不變的Model是十分必要的,當咱們工程變複雜的時候,開發將依然是簡單的。
此外,單向數據流保證了咱們的APP的調試很是簡單。下次若是有新的crash報告從Crashlytics(感受相似與Bugly)傳過來,咱們能夠很快速的修復Crash。由於全部須要的信息都會在crash報告裏面。什麼是「須要的信息」?就是咱們須要的當前Model和用戶執行了什麼樣的操做而致使的八阿哥(例子:點擊減量按鈕)。這就是咱們須要的信息,並且這些信息很顯然,是十分容易附加到Crash報告中的。若是,數據流不是單向的,那麼實現起來就有點困難。(例如:一些人亂用EventBus,而且將CounterModels暴露出來。譯者:EventBus沒用過,因此,這裏可能看起來怪怪的原話是someone misuses an EventBus and fires CounterModels out into the wild )或不具備不變性(這樣會致使咱們不能肯定誰改變了Model)。
"傳統"MVP或者MVVM改善了應用程序的可測試性。MVC也是可測試的:沒人告訴咱們業務邏輯必定要放在activity裏。當Model表明狀態,咱們能夠簡化咱們的集成測試代碼,例如,咱們能夠簡單的檢查assertEquals(expectedModel, model) 。這讓咱們除了Model之外的全部對象都不用mock。另外,這能夠消除了方法的許多驗證測試,例如 Mockito.verify(view, times(1)).showFoo() 。最後,它可讓咱們的測試代碼可讀性更好,更容易理解,更好的可維護性,咱們不須要糾結於如何實如今代碼中實現一些細節。
##總結
做爲這個系列的第一篇博客,咱們討論了不少關於理論的東西。咱們真的須要花4千多字介紹Model麼?我認爲理解Model的實現是十分重要的基礎,有助於防止一些問題,不然容易翻車。Model不意味着業務邏輯,它是生成Model的業務邏輯(例如,一個交互,一個用例,一個倉庫或者你在APP中調用的任何東西(原文: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上,來構建響應式應用程序。下面展現的,簡單的在線商城軟件將是咱們之後去實現的一個例子。你能夠指望在第二部分了。 敬請關注。