原文:REACTIVE APPS WITH MODEL-VIEW-INTENT - PART4 - INDEPENDENT UI COMPONENTS
作者:Hannes Dorfmann
譯者:卻把清梅嗅
這篇部落格中,我們將針對如何 如何構建獨立元件 進行探討,我將闡述為什麼在我看來 父子關係會導致壞味道的程式碼,以及為何這種關係是沒有意義的。
有這樣一個問題時不時湧現在我的腦海中—— MVI
、MVP
、MVVM
這些架構設計模式中,多個Presenter
(或者ViewModel
)彼此之間是如何進行通訊的?更直白點說吧,Child-Presenter
是如何與Parent-Presenter
通訊的?
對我來說,這種 父子關係 會產生壞味道的程式碼,因為這直接 導致了父子層級之間的耦合,使得程式碼難以閱讀和維護。
這種情況下,需求的更改會影響很多的元件(對於大型系統來說,這種情況下實現需求的變動簡直難如登天);並非僅此而已,同時,這也 引入了難以預測的共享的狀態,其導致的問題甚至難以重現和除錯。
其實這也沒那麼不堪,但我實在不理解為何資訊必須從Presenter A
流向Presenter B
呢?或者Presenter
如何與另一個Presenter
進行通訊?
根本沒必要! 什麼情況下Presenter
才會需要和Presenter
進行直接的通訊,是什麼事件發生了嗎?Presenter
根本不需要和其它的Presenter
直接通訊,它們都觀察了同一個Model
(或者說是業務邏輯的相同部分),這就是它們如何獲得變化的通知:通過底層。
當一些事件發生時(比如使用者點選了View1
按鈕),Presenter
將資訊下沉到業務邏輯。因為其它的Presenter
觀察了相同的業務邏輯,因此它們從業務邏輯中接收到了同樣變化的通知(Model
被更新了)。
關於這一點,我們已經在 第一章節 討論了 單向資料流 的原理的重要性。
讓我們通過一個真實的案例實現它:在我們的購物App
中,我們能夠將商品加入購物車,此外,有這樣一個頁面,我們可以看到購物車商品的內容,並且能夠一次選擇或者刪除多個商品條目:
我們如果能夠將這樣一個複雜的介面分割成更多 精巧、獨立且可複用的UI元件 的話就太棒了。以Toolbar
為例,它展示了被選中條目的數量,以及RecyclerView
展示了購物車裡條目的列表。
<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>
複製程式碼
但是這些元件之間如何保持相互的通訊呢?很明顯每個元件都有它自己的Presenter
:SelectedCountPresenter
和 ShoppingBasketPresenter
。這屬於父子關係嗎?不,它們僅僅是觀察了同一個Model
,該Model
根據在的邏輯程式碼中進行更新:
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 的程式碼和上述程式碼的實現非常類似,因此本文不對其進行展示。然而,如果我們認真去觀察這段程式碼,你會發現SelectedCountPresenter
和ShoppingCart
有一定的耦合。
我們完全有可能會在其它的頁面去複用這個UI元件,因此我們需要移除這個依賴的關係以達到複用該元件的目的。重構其實很簡單:presenter
持有一個 Observable<Integer>
作為Model
代替之前構造器中所需要的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 (原文為法語,大概意思是“就是這樣”),每當我們需要顯示當前選擇的條目數量時,我們就可以使用SelectedCountToolbar
元件——這可以代表ShoppingCart
中的條目數,也可以表示在App
中的完全不同的上下文環境和頁面中。
此外,此UI元件可以放入獨立的庫中,並在另一個App
(如相簿應用程式)中使用,以顯示所選照片的數量:
Observable<Integer> selectedCount = photoManager.getPhotos()
.map(photos -> {
int selected = 0;
for (Photo item : photos) {
if (item.isSelected()) selected++;
}
return selected;
});
return new SelectedCountToolbarPresnter(selectedCount);
複製程式碼
結語
本文的目的是證明通常情況下,程式碼的設計中根本不需要 父子關係 ,它們僅需要通過簡單的對相同業務邏輯進行觀察就能實現。
不需要EventBus
,不需要從上層的Activity
或者Fragment
中呼叫findViewById()
,不需要presenter.getParentPresenter()
或者其它的解決方案。僅使用 觀察者模式 就夠了。藉助於RxJava
——它本身也是基於觀察者模式思想的體現,我們就能夠輕而易舉構建這樣響應式的UI元件。
額外的思考
與MVP
或MVVM
相比,MVI
的實現過程中,我們被迫(通過積極的方式)使用業務邏輯驅動某個元件的狀態。因此,具有更多MVI
經驗的開發人員可以得出以下結論:
如果
View
的狀態是另一個元件的Model
怎麼辦?如果一個元件的ViewState
的變更是另一個元件的Intent
怎麼辦?
舉個例子:
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);
複製程式碼
乍一看,這似乎是一種可行的方案,但它不是父子關係的變體嗎?當然不是,這並非傳統分層的父子關係,也許將其比喻為洋蔥更為恰當(洋蔥的內層為外層提供了一種狀態)。
但是,這依然是一種耦合的關係,不是嗎?我還沒有下定決心,但現在我認為避免這種洋蔥般的關係更好。如果您有不同意見,請在下面留言,我很期待您的觀點。
系列目錄
《使用MVI打造響應式APP》原文
《使用MVI打造響應式APP》譯文
《使用MVI打造響應式APP》實戰
關於我
Hello,我是卻把清梅嗅,如果您覺得文章對您有價值,歡迎 ❤️,也歡迎關注我的部落格或者Github。
如果您覺得文章還差了那麼點東西,也請通過關注督促我寫出更好的文章——萬一哪天我進步了呢?