原文:《REACTIVE APPS WITH MODEL-VIEW-INTENT - PART1 - MODEL》
做者:Hannes Dorfmann
譯者:卻把清梅嗅前端
有朝一日,我忽然發現我對於Model
層的定義 所有是錯誤的,更新了認知後,我發現曾經我在Android
平臺上主題討論中的那些困惑或者頭痛都消失了。android
從結果上來講,最終我選擇使用 RxJava
和 Model-View-Intent(MVI)
構建 響應式的APP,這是我從未有過的嘗試——儘管在這以前我開發的APP也是響應式的,但 響應式編程 的體現與此次實踐相比,徹底沒法相提並論,在接下來我將要講述的一系列文章中,你也會感覺到這些。但做爲系列文章的開始,我想先闡述一個觀點:git
所謂的
Model
層究竟是什麼,我以前對Model
層的定義出現了什麼問題?github
我爲何說 我對Model
層有着錯誤的理解和使用方式 呢?固然,如今有不少架構模式將View
層和Model
層進行了分離,至少在Android
開發的領域,最著名的當屬Model-View-Controller (MVC)
、Model-View-Presenter (MVP)
和Model-View-ViewModel (MVVM)
——你注意到了嗎?這些架構模式中,Model
都是不可或缺的一環,但我意識到 在絕大數狀況下,我根本沒有Model
。數據庫
舉例來講,一個簡單的從後端拉取Person
列表狀況下,傳統的MVP
實現方式應該是這樣的:編程
class PersonsPresenter extends Presenter<PersonsView> {
public void load(){
getView().showLoading(true); // 展現一個 ProgressBar
backend.loadPersons(new Callback(){
public void onSuccess(List<Person> persons){
getView().showPersons(persons); // 展現用戶列表
}
public void onError(Throwable error){
getView().showError(error); // 展現錯誤信息
}
});
}
}
複製代碼
可是,這段代碼中的Model
究竟是指什麼呢?是指後臺的網絡請求嗎?不,那只是業務邏輯。是指請求結果的用戶列表嗎?不,它和ProgressBar、錯誤信息的展現同樣,僅僅只表明了View
層所能展現內容的一小部分而已。後端
那麼,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) ); // 展現一個 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) ); // 展現錯誤信息
}
});
}
}
複製代碼
如今,View
層持有了一個Model
,而且可以藉助它對屏幕上的控件進行rendered
(渲染)。這並不是什麼新鮮的概念,Trygve Reenskaug
在1979年時,其對最第一版本的MVC
定義中具備類似的概念:View
觀察Model
的變化。
然而,MVC
這個術語被用來描述太多種不一樣的模式,這些模式與Reenskaug
在1979年制定的模式並不徹底相同。好比後端開發人員使用MVC
框架,iOS有ViewController
,到了Android
領域MVC
又被如何定義了呢?Activity
是Controller
嗎? 那這樣的話ClickListener
又算什麼呢?現在,MVC
這個術語變成了一個很大的誤區,它錯誤地理解和使用了Reenskaug最初制定的內容——這個話題到此爲止,再繼續下去整個文章就會失控了。
言歸正傳,Model
的持有將會解決許多咱們在Android
開發中常常遇到的問題:
要討論這些關鍵的問題,咱們先來看看「傳統」的MVP
和MVVM
的實現代碼中如何處理它們,而後再談Model
如何跳過這些常見的陷阱。
響應式App,這是最近很是流行的話題,不是嗎?所謂的 響應式App 就是 應用會根據狀態的改變做出UI的響應,這句話裏有一個很是好的單詞:狀態。什麼是狀態呢?大多數時間裏,咱們將 狀態 描述爲咱們在屏幕中看到的東西,例如當界面展現ProgressBar
時的loading state
。
很關鍵的一點是,咱們前端開發人員傾向專一於UI。這不必定是壞事,由於一個好的UI體驗決定了用戶是否會用你的產品,從而決定了產品可否得到成功。可是看看上述的MVP
示例代碼(不是使用了PersonModel
的那個例子),這裏UI的狀態由Presenter
進行協調,Presenter
負責告訴View
層如何進行展現。
MVVM
亦然,我想在本文中對MVVM
的兩種實現方式進行區分:第一種依賴DataBinding
庫,第二種則依賴RxJava
;對於依賴DataBinding
的前者,其狀態被直接定義於ViewModel
中:
class PersonsViewModel {
ObservableBoolean loading;
// 省略...
public void load(){
loading.set(true);
backend.loadPersons(new Callback(){
public void onSuccess(List<Person> persons){
loading.set(false);
// 省略其它代碼,好比對persons進行渲染
}
public void onError(Throwable error){
loading.set(false);
// 省略其它代碼,好比展現錯誤信息
}
});
}
}
複製代碼
使用RxJava
實現MVVM
的方式中,其並不依賴DataBinding
引擎,而是將Observable
和UI的控件進行綁定,例如:
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)
// 實現方式並不唯一
}
// 在View層訂閱它 (好比 Activity / Fragment)
public Observable<Boolean> loading(){
return loading;
}
// 在View層訂閱它 (好比 Activity / Fragment)
public Observable<List<Person>> persons(){
return persons;
}
// 每當觸發此操做 (即調用 onNext()) ,加載Persons數據
public PublishSubject loadPersonsCommand(){
return loadPersonsCommand;
}
}
複製代碼
固然,這些代碼並不是完美,您的實現方式可能大相徑庭;我想說明的是,一般在MVP
或者MVVM
中,狀態 是由ViewModel
或者Presenter
進行驅動的。
這致使下述狀況的發生:
1.業務邏輯自己也擁有了狀態,Presenter
(或者ViewModel
)自己也擁有了狀態(而且,你還須要經過代碼去同步它們的狀態使其保持一致),同時,View
可能也有本身的狀態(比方說,調用View
的setVisibility()
方法設置其可見性,或者Android
系統在從新建立時從bundle
恢復狀態)。
2.Presenter
(或ViewModel
)有任意多個輸入(View
層觸發行爲並交給Presenter
處理),這是ok的,但同時Presenter
也有不少輸出(或MVP
中的輸出通道,如view.showLoading()
或view.showError()
;在MVVM
中,ViewModel
的實現中也提供了多個Observable
,這最終致使了View
層,Presenter
層和業務邏輯中狀態的衝突,在處理多線程的時候,這種狀況更明顯。
在好的狀況下,這隻會致使視覺上的錯誤,例如同時顯示加載指示符(「加載狀態」)和錯誤指示符(「錯誤狀態」),以下所示:
在最糟糕的狀況下,您從崩潰報告工具(如Crashlytics
)接收到了一個嚴重的錯誤報告,但您沒法重現這個錯誤,所以也幾乎無從着手去修復它。
若是從 底層 (業務邏輯層)到 頂層 (UI視圖層),有且僅有一個真實描述狀態的源,會怎麼樣呢?事實上,咱們已經在文章的開頭談論Model
的時候,就已經經過案例,把類似的概念展現了出來:
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映射了狀態,當我想通了這點,許多狀態相關的問題迎刃而解(甚至在編碼以前就已經被避免了);如今Presenter
層變得只有一個輸出了:
getView().render(PersonsModel)
它對應了一個數學上簡單的函數,好比f(x) = y
,對於多個輸入的函數,對應的則是f(a,b,c)
,但也是一個輸出。
並不是對全部人來講數學都是香茗,就好像數學家並不清楚bug是什麼——但軟件工程師須要去品嚐它。
瞭解Model
究竟是什麼以及如何創建對應的Model
很是重要,由於最終Model
能夠解決 狀態問題。
譯者注:針對 屏幕旋轉後的狀態回溯 這個問題,已經能夠經過Google官方發佈的
ViewModel
組件進行處理,開發者再也不須要爲此煩惱,但本章節仍值得一讀。
Android
設備上的 屏幕旋轉 是一個有足夠挑戰性的問題;忽視它是一個最簡單的解決方案,即 每次屏幕旋轉,都對數據從新進行加載 。這確實行之有效,大多數狀況下,您的APP也在離線狀態下工做,其數據來源於數據庫或者其它本地緩存,這意味着屏幕旋轉後的數據加載速度是很快的。
可是,我的而言我不喜歡看到加載框,哪怕加載速度是毫秒級別的,由於我認爲這並不是完美的用戶體驗,所以你們(包括我)開始使用MVP
,這其中包括了 保留性的Presenter——這樣就能夠 在屏幕旋轉時分離和銷燬View層,而Presenter
則會保存在內存中不會被銷燬,而後View
層會再次鏈接到Presenter
。
使用RxJava
的MVVM
也能夠實現相同的概念,但請牢記,一旦View
對ViewModel
取消了訂閱,可觀察的流就會被銷燬,這個問題你能夠用Subject
解決;對於DataBinding
構建的MVVM
來說,ViewModel
由DataBinding
直接綁定到View
層,爲了不內存泄露,須要咱們在屏幕旋轉時及時銷燬ViewModel
。
對於 保留性的Presenter 或者 ViewModel 的問題是: 咱們如何將View
的狀態在屏幕旋轉以後回溯,保證View
和Presenter
再次回到以前相同的狀態?我編寫了一個名爲 Mosby 的MVP庫,其包含一個名爲ViewState
的功能,它基本上將業務邏輯的狀態與View
同步。 Moxy,另外一個MVP庫,提出了一個很是有趣的解決方案——經過使用commands
在屏幕方向更改後重現View的狀態:
針對View
層狀態的問題,我很肯定還有其餘的解決方案。讓咱們退後一步,概括一下這些庫試圖解決的問題:那就是咱們已經討論過的 狀態問題。
再次重申,咱們經過一個 能反映當前狀態的Model 和一個渲染Model的方法 解決了這個問題,就像調用getView().render(PersonsModel)
同樣簡單。
當View
再也不使用時,是否還有保留Presenter
(或ViewModel
)的必要?好比,用戶跳轉到了另一個界面,這致使Fragment
(View
)被另外的Fragment
給replace
了,所以Presenter
已經不在被任何View
持有。
若是沒有View
層和Presenter
進行關聯,Presenter
天然也沒法根據業務邏輯,將最新的數據反映在View
上。但若是用戶又回來了怎麼辦(好比按下後退按鈕),是 從新加載數據 仍是 重用現有的Presenter?——這看起來像是一個哲學問題。
一般用戶一旦回到以前的界面,他會指望回到以前的界面繼續操做。這仍然像是第二小節關於View
層 狀態恢復 的問題,解決方案簡明扼要:當用戶返回時,咱們獲得 表明狀態的Model ,而後只需調用 getView().render(PersonsModel)
對View
層進行渲染。
進程終止是一件壞事,而且咱們須要依賴一些庫以幫助咱們在進程終止後對狀態進行恢復——我認爲這是Android
開發中常見的一種誤解。
首先,進程終止的緣由只有一個,而且有足夠充分的理由——Android
操做系統須要更多資源用於其餘應用程序或節省電池。若是你的APP處於前臺而且正在被用戶主動使用時,這種狀況永遠不會發生,所以,遵紀守法,不要與平臺做鬥爭了(就是不要固執於所謂的進程保活了)。若是你真的須要在後臺進行一些長時間的工做,請使用Service
,這也是向操做系統發出信號,告知您的App仍處於「主動使用狀態」的 惟一方式 。
若是進程終止了,Android
會提供一些回調以供 保存狀態,好比onSaveInstanceState()
——沒錯,又是 狀態 。咱們應該將View
的信息保存在Bundle
中嗎?咱們是否也應該把Presenter
中的狀態保存到Bundle
中?那麼業務邏輯的狀態呢?又是老生常談的問題,就和上面三個小節談到的同樣。
咱們只須要一個表明整個狀態的Model
類,咱們很容易將Model
保存在Bundle
中並在以後對它進行恢復。可是,我我的認爲大部分狀況下最好不保存狀態,而是 從新加載整個界面,就像咱們第一次啓動App同樣。 想一想顯示新聞列表的 NewsReader App
。 當App被殺掉,咱們保存了狀態,6小時後用戶從新打開App並恢復了狀態,咱們的App可能會顯示過期的內容。所以,這種狀況下,也許不存儲Model
和狀態、而對數據從新加載纔是更好的策略。
在這裏我不打算討論不變性(immutabiliy
)的優點,由於有不少資源討論這個問題。咱們想要一個不可變的Model
(表明狀態)。爲何?由於咱們想要惟一的狀態源,在傳遞Model
時,咱們不但願App中的其餘組件能夠改變咱們的Model
或者State
。
讓咱們假設編寫一個簡單的計數器App,它具備遞增和遞減的功能按鈕,並在TextView
中顯示當前計數器值。 若是咱們的Model
(在這種狀況下只是計數器值,即一個整數)是不可變的,那麼咱們如何更改計數器?
我很高興被問到這個問題,按鈕被點擊時,咱們並不是直接操做TextView
。個人建議是:
View
層應該有一個相似view.render(...)
的方法;Model
是不可變的,所以不可直接修改Model
;View
的渲染有且只有一個來源:即業務邏輯。咱們將點擊事件 下沉 到業務邏輯層。業務邏輯知道當前的Model
(例如,持有一個私有的成員Model
,它表明着當前的狀態), 這以後根據舊的Model,建立一個新的帶有增量/減量值的Model
。
這樣咱們創建了一個 單向數據流,業務邏輯做爲單一源用於建立不可變的Model
實例,但對於一個計數器來說未免有點小題大作,不是嗎?誠然,是的,計數器只是一個簡單的應用程序。大多數應用程序都是以簡單的應用程序開始,但複雜性增加很快——從個人角度來看,單向數據流和不可變模型是必要的,這會使簡單的應用程序,在複雜性遞增的同時,依然保持着簡單(對開發者而言)。
此外,單向數據流保證了咱們的應用程序易於調試。下次咱們從Crashlytics得到崩潰報告時,咱們能夠輕鬆地重現並修復此崩潰,由於全部必需的信息都已附加到崩潰報告中了。
什麼叫作必需的信息?那就是當前的Model
和用戶用戶在崩潰發生時想要執行的操做(好比,點擊減量按鈕)。這就是咱們重現此次崩潰所需的所有信息,這些信息很是容易收集並附加在崩潰報告中。
若是沒有單項數據流(好比,對EventBus
的濫用,或者將CounterModels
的私有域暴露出來),或者沒有不變性(這會致使咱們不知道誰實際更改了Model
),那麼bug的復現就沒那麼容易了。
「傳統」的MVP
或MVVM
提升了應用程序的可測試性。MVC
也是可測試的:沒有人說咱們必須將全部業務邏輯放入Activity
中。使用表示狀態的Model
,咱們能夠簡化單元測試的代碼,由於咱們能夠簡單地檢查assertEqual(expectedModel,model)
。這使咱們避免了許多必需要Mock
的對象。
此外,這也減小了不少驗證的測試,即某些方法是否被調用(好比Mockito.verify(view, times(1)).showFoo()
),最終,這使得咱們的單元測試代碼更具可讀性,易於理解而且易於維護,由於咱們沒必要處理不少實際代碼的實現細節。
在這個博客文章系列的第一部分中,咱們談了不少關於理論的東西。咱們真的須要關於專門討論Model
的博客嗎?
我認爲初步地理解Model
的確很重要,這也有助於咱們避免一些會遇到的問題。Model
並不意味着業務邏輯,它是生成Model
的業務邏輯(好比,一次交互,一個用例,一個倉庫或者你在APP中調用的任何東西)。
在接下來的第二部分中,當咱們最終使用Model-View-Intent
構建一個響應式App 時,咱們將看到Model
的實際應用。演示的APP是一個虛構的在線商店的應用程序,敬請關注。
《使用MVI打造響應式APP》原文
《使用MVI打造響應式APP》譯文
《使用MVI打造響應式APP》實戰
Hello,我是卻把清梅嗅,若是您以爲文章對您有價值,歡迎 ❤️,也歡迎關注個人博客或者Github。
若是您以爲文章還差了那麼點東西,也請經過關注督促我寫出更好的文章——萬一哪天我進步了呢?