原文:REACTIVE APPS WITH MODEL-VIEW-INTENT - PART2 - VIEW AND INTENT
作者:Hannes Dorfmann
譯者:卻把清梅嗅
在 上文 中蛋逾,我們探討了對Model
的定義遇汞、與 狀態(tài) 的關(guān)系以及如何在通過良好地定義Model
來解決一些Android
開發(fā)中常見的問題辱姨。本文將通過 Model-View-Intent
憔鬼,即MVI
模式,繼續(xù)我們的 響應(yīng)式App 構(gòu)建之旅洋只。
如果您尚未閱讀上一小節(jié)样眠,則應(yīng)在繼續(xù)閱讀本文之前閱讀該部分∪ㄉ眨總結(jié)一下:以“傳統(tǒng)的”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); // 展示錯誤信息
}
});
}
}
我們應(yīng)該創(chuàng)建能夠反映 狀態(tài) 的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
層也應(yīng)該像這樣進行定義:
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) ); // 展示錯誤信息
}
});
}
}
現(xiàn)在,僅需簡單調(diào)用View
層的render(personsModel)
方法板祝,Model
就會被成功的渲染在屏幕上宫静。在第一小節(jié)中我們同樣探討了 單項數(shù)據(jù)流 的重要性,同時您的業(yè)務(wù)邏輯應(yīng)該驅(qū)動該Model
券时。在正式將所有內(nèi)容環(huán)環(huán)相扣連接之前孤里,我們先簡單了解一下MVI
的核心思想。
Model-View-Intent (MVI)
該模式最初被 andrestaltz 在他寫的JavaScript
框架 cycle.js 中所提出; 從理論(還有數(shù)學(xué))上講橘洞,我們這樣對Model-View-Intent
的定義進行描述:
[站外圖片上傳中...(image-fc6580-1552228859520)]
1.intent()
此函數(shù)接受來自用戶的輸入(即UI事件捌袜,比如點擊事件)并將其轉(zhuǎn)換為可傳遞給Model()
函數(shù)的參數(shù),該參數(shù)可能是一個簡單的String
對Model
進行賦值炸枣,也可能像是Object
這樣復(fù)雜的數(shù)據(jù)結(jié)構(gòu)虏等。intent
作為意圖,標志著 我們試圖對Model
進行改變抛虏。
2.model()
model()
函數(shù)將intent()
函數(shù)的輸出作為輸入來操作Model
博其,其函數(shù)輸出是一個新的Model
(狀態(tài)發(fā)生了改變)。
不要對已存在的Model
對象進行修改迂猴,我們需要的是不可變慕淡!對此,在上文中我們已經(jīng)展示了一個計數(shù)器的具體案例沸毁,再次重申峰髓,不要修改已存在的Model
傻寂!
根據(jù)intent
所描述的變化,我們創(chuàng)建一個新的Model
,請注意携兵,Model()
函數(shù)是唯一允許對Model
進行創(chuàng)建的途徑疾掰。然后這個新的Model
作為該函數(shù)的輸出——基本上model()
函數(shù)調(diào)用我們App
的業(yè)務(wù)邏輯(可以是交互、用例徐紧、Repository
......您在App
中使用的任何模式/術(shù)語)并作為結(jié)果提供新的Model
對象静檬。
3.view()
該方法獲取model()
函數(shù)返回的Model
,并將其作為view()
函數(shù)的輸入并级,這之后通過某種方式將Model
展示出來拂檩,view()
和view.render(model)
大體上是一致的。
4.本質(zhì)
但是我們希望構(gòu)建的是 響應(yīng)式的App嘲碧,不是嗎稻励?那么MVI
是如何響應(yīng)式的呢?響應(yīng)式實際上意味著什么愈涩?
這意味著App
的 UI反映了狀態(tài)的變更望抽。
因為Model
反映了狀態(tài),因此履婉,本質(zhì)上我們希望 業(yè)務(wù)邏輯能夠?qū)斎氲氖录?code>intents)進行響應(yīng)煤篙,并創(chuàng)建對應(yīng)的Model
作為輸出,這之后再通過調(diào)用View
層的render(model)
方法毁腿,對UI進行渲染舰蟆。
5.通過RxJava串聯(lián)
我們希望我們的數(shù)據(jù)流的單向性,因此RxJava
閃亮登場狸棍。我們的App必須通過RxJava
保持 數(shù)據(jù)的單向性 和 響應(yīng)式 來構(gòu)建嗎?或者必須用MVI
模式才能構(gòu)建嗎味悄?當(dāng)然不草戈,我們也可以寫 命令式 和 程序性 的代碼。但是侍瑟,基于事件編程 的RxJava
實在太優(yōu)秀了唐片,既然UI
是基于事件的,因此使用RxJava
也是非常有意義的涨颜。
本文我們將會構(gòu)建一個簡單的虛擬在線商店App
费韭,其UI界面中展示的商品數(shù)據(jù),都來源于我們向后臺進行的網(wǎng)絡(luò)請求庭瑰。
我們可以精確的搜索特定的商品星持,并將其添加到我們的購物車中,最終App
的效果如下所示:
這個項目的源碼你可以在Github上找到弹灭,我們從實現(xiàn)一個簡單的搜索界面開始做起:
首先督暂,就像上文我們描述的那樣揪垄,我們定義一個Model
用于描述View
層是如何被展示的—— 這個系列中,我們將用帶有 ViewState
后綴的類來替代 Model
逻翁;舉個例子饥努,我們將會為搜索頁的Model
類命名為SearchViewState
。
這很好理解八回,因為Model
反應(yīng)的就是狀態(tài)(State)酷愧,至于為什么不用聽起來有些奇怪的名稱比如SearchModel
,是因為擔(dān)心和MVVM
中的SearchViewModel
類在一起會導(dǎo)致歧義——命名真的很難缠诅。
public interface SearchViewState {
// 搜索尚未開始
final class SearchNotStartedYet implements SearchViewState {
}
// 搜索中
final class Loading implements SearchViewState {
}
// 返回結(jié)果為空
final class EmptyResult implements SearchViewState {
private final String searchQueryText;
public EmptyResult(String searchQueryText) {
this.searchQueryText = searchQueryText;
}
public String getSearchQueryText() {
return searchQueryText;
}
}
// 有效的搜索結(jié)果溶浴,包含和搜索條件匹配的商品列表
final class SearchResult implements SearchViewState {
private final String searchQueryText;
private final List<Product> result;
public SearchResult(String searchQueryText, List<Product> result) {
this.searchQueryText = searchQueryText;
this.result = result;
}
public String getSearchQueryText() {
return searchQueryText;
}
public List<Product> getResult() {
return result;
}
}
// 表示搜索過程中發(fā)生了錯誤
final class Error implements SearchViewState {
private final String searchQueryText;
private final Throwable error;
public Error(String searchQueryText, Throwable error) {
this.searchQueryText = searchQueryText;
this.error = error;
}
public String getSearchQueryText() {
return searchQueryText;
}
public Throwable getError() {
return error;
}
}
}
因為Java
是一種強類型的語言,因此我們可以選擇一種安全的方式為我們的Model
類拆分出多個不同的 子狀態(tài)滴铅。
我們的業(yè)務(wù)邏輯返回的是一個 SearchViewState
類型的對象戳葵,它可能是SearchViewState.Error
或者其它的一個實例。這只是我個人的偏好汉匙,們也可以通過不同的方式定義拱烁,例如:
class SearchViewState {
Throwable error; // 非空則意味著,出現(xiàn)了一個錯誤
boolean loading; // 值為true意味著正在加載中
List<Product> result; // 非空意味著商品列表的結(jié)果
boolean SearchNotStartedYet; // true意味著還未開始搜索
}
再次重申噩翠,如何定義Model
純屬個人喜好戏自,如果你用Kotlin
作為編程語言,那么sealed classes
是一個不錯的選擇伤锚。
將目光聚集回到業(yè)務(wù)代碼擅笔,讓我們通過 SearchInteractor
去執(zhí)行搜索的功能,其輸出就是我們之前說過的SearchViewState
對象:
public class SearchInteractor {
final SearchEngine searchEngine; // 執(zhí)行網(wǎng)絡(luò)請求
public Observable<SearchViewState> search(String searchString) {
// 如果是空的字符串屯援,不進行搜索
if (searchString.isEmpty()) {
return Observable.just(new SearchViewState.SearchNotStartedYet());
}
// 搜索商品列表
// 返回 Observable<List<Product>>
return searchEngine.searchFor(searchString)
.map(products -> {
if (products.isEmpty()) {
return new SearchViewState.EmptyResult(searchString);
} else {
return new SearchViewState.SearchResult(searchString, products);
}
})
.startWith(new SearchViewState.Loading())
.onErrorReturn(error -> new SearchViewState.Error(searchString, error));
}
}
來看下SearchInteractor.search()
的方法簽名:我們將String
類型的searchString
作為 輸入 的參數(shù)猛们,以及Observable<SearchViewState>
類型的 輸出,這意味著我們期望隨著時間的推移狞洋,可以在可觀察的流上會有任意多個SearchViewState
的實例被發(fā)射弯淘。
在我們正式開始查詢搜索之前(即SearchEngine
執(zhí)行網(wǎng)絡(luò)請求),我們通過startWith()
操作符發(fā)射一個SearchViewState.Loading
,這將會使得View
在執(zhí)行搜索時展示ProgressBar
吉懊。
onErrorReturn()
會捕獲在執(zhí)行搜索時拋出的所有異常庐橙,并且發(fā)射出一個SearchViewState.Error
——在訂閱這個Observable
時,我們?yōu)槭裁床蝗ナ褂?code>onError()回調(diào)呢借嗽?
這是一個對RxJava
認知的普遍誤解态鳖,實際上,onError()
的回調(diào)意味著 整個可觀察的流進入了不可恢復(fù)的狀態(tài)恶导,因此可觀察的流結(jié)束了浆竭,而在我們的案例中,類似“沒有網(wǎng)絡(luò)連接”的error
并非不可恢復(fù)的error
:這只是我們的Model
所代表的另外一個狀態(tài)。
此外兆蕉,我們還有另外一個可以轉(zhuǎn)換到的狀態(tài)羽戒,即一旦網(wǎng)絡(luò)連接可用,我們可以通過 SearchViewState.Loading
跳轉(zhuǎn)到的 加載狀態(tài)虎韵。
因此易稠,我們建立了一個可觀察的流,這是一個每當(dāng)狀態(tài)發(fā)生了改變包蓝,從業(yè)務(wù)邏輯層就會發(fā)射一個發(fā)生了改變的Model
到View
層的流驶社。
我們不想在網(wǎng)絡(luò)連接錯誤時終止這個可觀察的流,因此测萎,在error
發(fā)生時亡电,類似這種可以被處理為 狀態(tài) 的error
(而不是終止流的那種致命的錯誤),可以反應(yīng)為Model
硅瞧,被可觀察的流發(fā)射份乒。
通常,在MVI
中腕唧,Model
的Observable
永遠不會被終止(即永遠不會執(zhí)行onComplete()
或者onError()
回調(diào))或辖。
總結(jié)一下,SearchInteractor
(即業(yè)務(wù)邏輯)提供了一個可觀察的流Observable<SearchViewState>
枣接,每當(dāng)狀態(tài)發(fā)生了變化颂暇,就會發(fā)射一個新的SearchViewState
。
6.View層的職責(zé)
接下來我們來討論一下View
應(yīng)該是什么樣的但惶,View
層的職責(zé)是什么耳鸯?顯然View
層應(yīng)該對Model
進行展示,我們已經(jīng)認可View
層應(yīng)該有類似 render(model)
這樣的函數(shù)膀曾。此外县爬,View
應(yīng)該提供一個給其他層響應(yīng)用戶輸入的方法,在MVI
中這個方法被稱為 intents
添谊。
在這個案例中捌省,我們只有一個intent
:用戶可以在輸入框中輸入一個用于檢索商品的字符串進行搜索。MVP
中的好習(xí)慣是為View
層定義一個接口碉钠,所以在MVI
中我們也可以這樣做。
public interface SearchView {
// 搜索的intent
Observable<String> searchIntent();
// 對View層進行渲染
void render(SearchViewState viewState);
}
我們的案例中View
層只提供了一個intent
,但通常View
擁有更多的intent
;在 第一小節(jié) 中我們討論了為什么一個單獨的render()
函數(shù)是一個不錯的實踐卷拘,如果你對此還不是很清楚的話喊废,請閱讀該小節(jié)并通過留言進行探討。
在我們開始對View
層進行具體的實現(xiàn)之前栗弟,我們先看看最終界面的展示效果:
public class SearchFragment extends Fragment implements SearchView {
@BindView(R.id.searchView) android.widget.SearchView searchView;
@BindView(R.id.container) ViewGroup container;
@BindView(R.id.loadingView) View loadingView;
@BindView(R.id.errorView) TextView errorView;
@BindView(R.id.recyclerView) RecyclerView recyclerView;
@BindView(R.id.emptyView) View emptyView;
private SearchAdapter adapter;
@Override public Observable<String> searchIntent() {
return RxSearchView.queryTextChanges(searchView) // 感謝 Jake Wharton :)
.filter(queryString -> queryString.length() > 3 || queryString.length() == 0)
.debounce(500, TimeUnit.MILLISECONDS);
}
@Override public void render(SearchViewState viewState) {
if (viewState instanceof SearchViewState.SearchNotStartedYet) {
renderSearchNotStarted();
} else if (viewState instanceof SearchViewState.Loading) {
renderLoading();
} else if (viewState instanceof SearchViewState.SearchResult) {
renderResult(((SearchViewState.SearchResult) viewState).getResult());
} else if (viewState instanceof SearchViewState.EmptyResult) {
renderEmptyResult();
} else if (viewState instanceof SearchViewState.Error) {
renderError();
} else {
throw new IllegalArgumentException("Don't know how to render viewState " + viewState);
}
}
private void renderResult(List<Product> result) {
TransitionManager.beginDelayedTransition(container);
recyclerView.setVisibility(View.VISIBLE);
loadingView.setVisibility(View.GONE);
emptyView.setVisibility(View.GONE);
errorView.setVisibility(View.GONE);
adapter.setProducts(result);
adapter.notifyDataSetChanged();
}
private void renderSearchNotStarted() {
TransitionManager.beginDelayedTransition(container);
recyclerView.setVisibility(View.GONE);
loadingView.setVisibility(View.GONE);
errorView.setVisibility(View.GONE);
emptyView.setVisibility(View.GONE);
}
private void renderLoading() {
TransitionManager.beginDelayedTransition(container);
recyclerView.setVisibility(View.GONE);
loadingView.setVisibility(View.VISIBLE);
errorView.setVisibility(View.GONE);
emptyView.setVisibility(View.GONE);
}
private void renderError() {
TransitionManager.beginDelayedTransition(container);
recyclerView.setVisibility(View.GONE);
loadingView.setVisibility(View.GONE);
errorView.setVisibility(View.VISIBLE);
emptyView.setVisibility(View.GONE);
}
private void renderEmptyResult() {
TransitionManager.beginDelayedTransition(container);
recyclerView.setVisibility(View.GONE);
loadingView.setVisibility(View.GONE);
errorView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
}
}
render(SearchViewState)
方法的作用顯而易見污筷,searchIntent()
方法中,我們使用了Jake Wharton
的 RxBinding ,這是一個對Android UI
組件提供了RxJava
響應(yīng)式支持的庫瓣蛀。
RxSearchView.queryText()
創(chuàng)建了一個Observable<String>
,每當(dāng)用戶在EditText
上輸入了一些文字陆蟆,它就會發(fā)射一個對應(yīng)的字符串;我們通過filter()
去保證只有當(dāng)用戶輸入的字符數(shù)達到三個以上時才進行搜索惋增;同時叠殷,我們不希望每當(dāng)用戶輸入一個字符,就去請求網(wǎng)絡(luò)诈皿,而是當(dāng)用戶輸入結(jié)束后再去請求網(wǎng)絡(luò)(debounce()
操作符會停留500毫秒以決定用戶是否輸入完成)林束。
現(xiàn)在我們知道了屏幕中的searchIntent()
方法就是 輸入 ,而render()
方法則是 輸出稽亏。我們?nèi)绾螐?輸入 獲得 輸出 呢壶冒,如下所示:
7.連接View和Intent
剩下的問題就是:我們?nèi)绾螌?code>View的intent
和業(yè)務(wù)邏輯進行連接呢?如果你認真觀看了上面的流程圖截歉,你應(yīng)該注意到了中間的 flatMap()
操作符胖腾,這暗示了我們還有一個尚未談及的組件: Presenter
;Presenter
負責(zé)連接這些點,就和我們在MVP
中使用的方式一樣瘪松。
public class SearchPresenter extends MviBasePresenter<SearchView, SearchViewState> {
private final SearchInteractor searchInteractor;
@Override protected void bindIntents() {
Observable<SearchViewState> search =
intent(SearchView::searchIntent)
// 上文中我們談到了flatMap,但在這里switchMap更為適用
.switchMap(searchInteractor::search)
.observeOn(AndroidSchedulers.mainThread());
subscribeViewState(search, SearchView::render);
}
}
什么是 MviBasePresenter
, intent()
和 subscribeViewState()
又是什么咸作?這個類是我寫的 Mosby 庫的一部分(3.0版本后,Mosby
已經(jīng)支持了MVI
)凉逛。本文并非為了講述Mosby
性宏,但我向簡單介紹一下MviBasePresenter
是如何的便利——這其中沒有什么黑魔法,雖然確實看起來像是那樣状飞。
讓我們從生命周期開始:MviBasePresenter
并未持有任何生命周期毫胜,它暴露出一個 bindIntent()
方法以供View
層和業(yè)務(wù)邏輯進行綁定。通常诬辈,你通過flatMap()
酵使、switchMap()
或者concatMap()
操作符將intent
“轉(zhuǎn)移”到業(yè)務(wù)邏輯中,這個方法僅僅在View
層第一次被附加到Presenter
中時調(diào)用焙糟,而當(dāng)View
再次被附加在Presenter
中時(比如口渔,屏幕方向發(fā)生了改變),將不再被調(diào)用穿撮。
這聽起來有些怪缺脉,也許有人會說:
MviBasePresenter
在屏幕方向發(fā)生了改變后依然能夠存活?如果是這樣悦穿,Mosby
如何保證Observable
的流不會發(fā)生內(nèi)存的泄漏攻礼?
這就是 intent()
和 subscribeViewState()
的作用所在了,intent()
在內(nèi)部創(chuàng)建一個PublishSubject
栗柒,就像是業(yè)務(wù)邏輯的“網(wǎng)關(guān)”一樣礁扮;實際上,PublishSubject
訂閱了View
層傳過來的intent
的Observable
,調(diào)用intent(o1)
實際返回了一個訂閱了o1
的PublishSubject
。
屏幕發(fā)生旋轉(zhuǎn)時,Mosby
將View
從Presenter
中分離太伊,但是雇锡,內(nèi)部的PublishSubject
只是暫時和View
解除了訂閱;而當(dāng)View
重新附著在Presenter
上時僚焦,PublishSubject
將會對View
層的intent
進行重新訂閱锰提。
subscribeViewState()
方法做的是同樣的事情,只不過將順序調(diào)換了過來(Presenter
向View
層的通信)叠赐。它在內(nèi)部創(chuàng)建一個BehaviorSubject
作為從業(yè)務(wù)邏輯到View層的“網(wǎng)關(guān)”欲账。
由于它是一個BehaviorSubject
,因此芭概,即使此時Presenter
沒有持有View
赛不,我們依然可以從業(yè)務(wù)邏輯中接收到Model
的更新(比如View
并未處于棧頂);BehaviorSubjects
始終持有它最后的值罢洲,并在View
重新依附后將其重新發(fā)射踢故。
規(guī)則很簡單:使用intent()
來“包裝”View
層的所有intent
,使用subscribeViewState()
替代Observable.subscribe()
.
[站外圖片上傳中...(image-1db2b2-1552228859520)]
8.UnbindIntents
與bindIntent()
相對應(yīng)的是 unbindIntents()
,該方法只會執(zhí)行一次,即View
被永久銷毀時才會被調(diào)用惹苗。舉個例子殿较,將一個Fragment
放在棧中,直到Activity
被銷毀之前桩蓉,該View
一直不會被銷毀淋纲。
由于intent()
和subscribeViewState()
已經(jīng)對訂閱進行了管理,因此您只需要實現(xiàn)unbindIntents()
院究。
9.其它生命周期的事件
那么其它生命周期的事件洽瞬,比如onPause()
和onResume()
又該如何處理?我依然認為Presenter
不需要生命周期的事件业汰,然而伙窃,如果你堅持認為你需要將這些生命周期的事件視為另一種形式的intent
,您的View
可以提供一個pauseIntent()
样漆,它是由android
生命周期觸發(fā)为障,而又不是按鈕點擊事件這樣的由用戶交互觸發(fā)的intent
——但兩者都是有效的意圖。
結(jié)語
第二小節(jié)中放祟,我們探討了Model-View-Intent
的基礎(chǔ)鳍怨,并通過MVI
淺嘗輒止實現(xiàn)了一個簡單的頁面。也許這個例子太簡單了跪妥,所以你尚未感受到MVI
模式的優(yōu)點:代表 狀態(tài) 的Model
和與傳統(tǒng)MVP
或者MVVM
相比的 單項數(shù)據(jù)流京景。
MVP
和MVVM
并沒有什么問題,我也并非是在說MVI
比其它架構(gòu)模式更優(yōu)秀骗奖,但是,我認為MVI
可以幫助我們 為復(fù)雜的問題編寫優(yōu)雅的代碼 ,這也正如我們將在本系列博客的 下一小節(jié)(第3小節(jié))中探討的那樣——屆時我們將針對 狀態(tài)合并 (state reducers)的問題進行探討执桌,歡迎關(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[四]:獨立性UI組件
- [譯]使用MVI打造響應(yīng)式APP[五]:輕而易舉地Debug
- [譯]使用MVI打造響應(yīng)式APP[六]:恢復(fù)狀態(tài)
- [譯]使用MVI打造響應(yīng)式APP[七]:掌握時機(SingleLiveEvent問題)
- [譯]使用MVI打造響應(yīng)式APP[八]:導(dǎo)航
《使用MVI打造響應(yīng)式APP》實戰(zhàn)
關(guān)于我
Hello,我是卻把清梅嗅仰挣,如果您覺得文章對您有價值伴逸,歡迎 ??,也歡迎關(guān)注我的博客或者Github膘壶。
如果您覺得文章還差了那么點東西错蝴,也請通過關(guān)注督促我寫出更好的文章——萬一哪天我進步了呢?