原文:REACTIVE APPS WITH MODEL-VIEW-INTENT - PART4 - INDEPENDENT UI COMPONENTS
作者:Hannes Dorfmann
譯者:卻把清梅嗅
這篇博客中,我們將針對(duì)如何 如何構(gòu)建獨(dú)立組件 進(jìn)行探討,我將闡述為什么在我看來 父子關(guān)系會(huì)導(dǎo)致壞味道的代碼,以及為何這種關(guān)系是沒有意義的唉锌。
有這樣一個(gè)問題時(shí)不時(shí)涌現(xiàn)在我的腦海中—— MVI
闯两、MVP
秽浇、MVVM
這些架構(gòu)設(shè)計(jì)模式中召廷,多個(gè)Presenter
(或者ViewModel
)彼此之間是如何進(jìn)行通訊的揖闸?更直白點(diǎn)說吧句灌,Child-Presenter
是如何與Parent-Presenter
通訊的夷陋?
對(duì)我來說,這種 父子關(guān)系 會(huì)產(chǎn)生壞味道的代碼胰锌,因?yàn)檫@直接 導(dǎo)致了父子層級(jí)之間的耦合骗绕,使得代碼難以閱讀和維護(hù)。
這種情況下资昧,需求的更改會(huì)影響很多的組件(對(duì)于大型系統(tǒng)來說爹谭,這種情況下實(shí)現(xiàn)需求的變動(dòng)簡直難如登天);并非僅此而已榛搔,同時(shí)诺凡,這也 引入了難以預(yù)測的共享的狀態(tài),其導(dǎo)致的問題甚至難以重現(xiàn)和調(diào)試践惑。
其實(shí)這也沒那么不堪腹泌,但我實(shí)在不理解為何信息必須從Presenter A
流向Presenter B
呢?或者Presenter
如何與另一個(gè)Presenter
進(jìn)行通信尔觉?
根本沒必要凉袱! 什么情況下Presenter
才會(huì)需要和Presenter
進(jìn)行直接的通訊,是什么事件發(fā)生了嗎侦铜?Presenter
根本不需要和其它的Presenter
直接通訊专甩,它們都觀察了同一個(gè)Model
(或者說是業(yè)務(wù)邏輯的相同部分),這就是它們?nèi)绾潍@得變化的通知:通過底層。
當(dāng)一些事件發(fā)生時(shí)(比如用戶點(diǎn)擊了View1
按鈕)钉稍,Presenter
將信息下沉到業(yè)務(wù)邏輯涤躲。因?yàn)槠渌?code>Presenter觀察了相同的業(yè)務(wù)邏輯,因此它們從業(yè)務(wù)邏輯中接收到了同樣變化的通知(Model
被更新了)贡未。
關(guān)于這一點(diǎn)种樱,我們已經(jīng)在 第一章節(jié) 討論了 單向數(shù)據(jù)流 的原理的重要性。
讓我們通過一個(gè)真實(shí)的案例實(shí)現(xiàn)它:在我們的購物App
中俊卤,我們能夠?qū)⑸唐芳尤胭徫镘嚹奂罚送猓羞@樣一個(gè)頁面消恍,我們可以看到購物車商品的內(nèi)容岂昭,并且能夠一次選擇或者刪除多個(gè)商品條目:
我們?nèi)绻軌驅(qū)⑦@樣一個(gè)復(fù)雜的界面分割成更多 精巧、獨(dú)立且可復(fù)用的UI組件 的話就太棒了狠怨。以Toolbar
為例约啊,它展示了被選中條目的數(shù)量邑遏,以及RecyclerView
展示了購物車?yán)飾l目的列表。
<LinearLayout>
<com.hannesdorfmann.SelectedCountToolbar
android:id="@+id/selectedCountToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<com.hannesdorfmann.ShoppingBasketRecyclerView
android:id="@+id/shoppingBasketRecyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
/>
</LinearLayout>
但是這些組件之間如何保持相互的通訊呢棍苹?很明顯每個(gè)組件都有它自己的Presenter
:SelectedCountPresenter
和 ShoppingBasketPresenter
。這屬于父子關(guān)系嗎茵汰?不枢里,它們僅僅是觀察了同一個(gè)Model
,該Model
根據(jù)在的邏輯代碼中進(jìn)行更新:
public class SelectedCountPresenter
extends MviBasePresenter<SelectedCountView, Integer> {
private ShoppingCart shoppingCart;
public SelectedCountPresenter(ShoppingCart shoppingCart) {
this.shoppingCart = shoppingCart;
}
@Override protected void bindIntents() {
subscribeViewState(shoppingCart.getSelectedItemsObservable(), SelectedCountView::render);
}
}
class SelectedCountToolbar extends Toolbar implements SelectedCountView {
...
@Override public void render(int selectedCount) {
if (selectedCount == 0) {
setVisibility(View.VISIBLE);
} else {
setVisibility(View.INVISIBLE);
}
}
}
ShoppingBasketRecyclerView 的代碼和上述代碼的實(shí)現(xiàn)非常類似蹂午,因此本文不對(duì)其進(jìn)行展示栏豺。然而,如果我們認(rèn)真去觀察這段代碼豆胸,你會(huì)發(fā)現(xiàn)SelectedCountPresenter
和ShoppingCart
有一定的耦合奥洼。
我們完全有可能會(huì)在其它的頁面去復(fù)用這個(gè)UI組件,因此我們需要移除這個(gè)依賴的關(guān)系以達(dá)到復(fù)用該組件的目的晚胡。重構(gòu)其實(shí)很簡單:presenter
持有一個(gè) Observable<Integer>
作為Model
代替之前構(gòu)造器中所需要的ShoppingCart
:
public class SelectedCountPresenter
extends MviBasePresenter<SelectedCountView, Integer> {
private Observable<Integer> selectedCountObservable;
public SelectedCountPresenter(Observable<Integer> selectedCountObservable) {
this.selectedCountObservable = selectedCountObservable;
}
@Override protected void bindIntents() {
subscribeViewState(selectedCountObservable, SelectedCountToolbarView::render);
}
}
There you go (原文為法語灵奖,大概意思是“就是這樣”),每當(dāng)我們需要顯示當(dāng)前選擇的條目數(shù)量時(shí)估盘,我們就可以使用SelectedCountToolbar
組件——這可以代表ShoppingCart
中的條目數(shù)瓷患,也可以表示在App
中的完全不同的上下文環(huán)境和頁面中。
此外遣妥,此UI組件可以放入獨(dú)立的庫中擅编,并在另一個(gè)App
(如相冊(cè)應(yīng)用程序)中使用,以顯示所選照片的??數(shù)量:
Observable<Integer> selectedCount = photoManager.getPhotos()
.map(photos -> {
int selected = 0;
for (Photo item : photos) {
if (item.isSelected()) selected++;
}
return selected;
});
return new SelectedCountToolbarPresnter(selectedCount);
結(jié)語
本文的目的是證明通常情況下箫踩,代碼的設(shè)計(jì)中根本不需要 父子關(guān)系 爱态,它們僅需要通過簡單的對(duì)相同業(yè)務(wù)邏輯進(jìn)行觀察就能實(shí)現(xiàn)。
不需要EventBus
境钟,不需要從上層的Activity
或者Fragment
中調(diào)用findViewById()
锦担,不需要presenter.getParentPresenter()
或者其它的解決方案。僅使用 觀察者模式 就夠了慨削。借助于RxJava
——它本身也是基于觀察者模式思想的體現(xiàn)吆豹,我們就能夠輕而易舉構(gòu)建這樣響應(yīng)式的UI組件。
額外的思考
與MVP
或MVVM
相比理盆,MVI
的實(shí)現(xiàn)過程中痘煤,我們被迫(通過積極的方式)使用業(yè)務(wù)邏輯驅(qū)動(dòng)某個(gè)組件的狀態(tài)。因此猿规,具有更多MVI
經(jīng)驗(yàn)的開發(fā)人員可以得出以下結(jié)論:
如果
View
的狀態(tài)是另一個(gè)組件的Model
怎么辦衷快?如果一個(gè)組件的ViewState
的變更是另一個(gè)組件的Intent
怎么辦?
舉個(gè)例子:
Observable<Integer> selectedItemCountObservable =
shoppingBasketPresenter
.getViewStateObservable()
.map(items -> {
int selected = 0;
for (ShoppingCartItem item : items) {
if (item.isSelected()) selected++;
}
return selected;
});
Observable<Boolean> doSomethingBecauseOtherComponentReadyIntent =
shoppingBasketPresenter
.getViewStateObservable()
.filter(state -> state.isShowingData())
.map(state -> true);
return new SelectedCountToolbarPresenter(
selectedItemCountObservable,
doSomethingBecauseOtherComponentReadyIntent);
乍一看,這似乎是一種可行的方案姨俩,但它不是父子關(guān)系的變體嗎蘸拔?當(dāng)然不是师郑,這并非傳統(tǒng)分層的父子關(guān)系,也許將其比喻為洋蔥更為恰當(dāng)(洋蔥的內(nèi)層為外層提供了一種狀態(tài))调窍。
但是宝冕,這依然是一種耦合的關(guān)系,不是嗎邓萨?我還沒有下定決心地梨,但現(xiàn)在我認(rèn)為避免這種洋蔥般的關(guān)系更好。如果您有不同意見缔恳,請(qǐng)?jiān)谙旅媪粞员ζ剩液芷诖挠^點(diǎ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)步了呢聘裁?