[譯]使用MVI打造響應式APP(四):獨立性UI元件

卻把清梅嗅發表於2019-03-15

原文:REACTIVE APPS WITH MODEL-VIEW-INTENT - PART4 - INDEPENDENT UI COMPONENTS
作者:Hannes Dorfmann
譯者:卻把清梅嗅

這篇部落格中,我們將針對如何 如何構建獨立元件 進行探討,我將闡述為什麼在我看來 父子關係會導致壞味道的程式碼,以及為何這種關係是沒有意義的。

有這樣一個問題時不時湧現在我的腦海中—— MVIMVPMVVM這些架構設計模式中,多個Presenter(或者ViewModel)彼此之間是如何進行通訊的?更直白點說吧,Child-Presenter是如何與Parent-Presenter通訊的?

[譯]使用MVI打造響應式APP(四):獨立性UI元件

對我來說,這種 父子關係 會產生壞味道的程式碼,因為這直接 導致了父子層級之間的耦合,使得程式碼難以閱讀和維護

這種情況下,需求的更改會影響很多的元件(對於大型系統來說,這種情況下實現需求的變動簡直難如登天);並非僅此而已,同時,這也 引入了難以預測的共享的狀態,其導致的問題甚至難以重現和除錯。

其實這也沒那麼不堪,但我實在不理解為何資訊必須從Presenter A流向Presenter B呢?或者Presenter如何與另一個Presenter進行通訊?

根本沒必要! 什麼情況下Presenter才會需要和Presenter進行直接的通訊,是什麼事件發生了嗎?Presenter根本不需要和其它的Presenter直接通訊,它們都觀察了同一個Model(或者說是業務邏輯的相同部分),這就是它們如何獲得變化的通知:通過底層。

[譯]使用MVI打造響應式APP(四):獨立性UI元件

當一些事件發生時(比如使用者點選了View1按鈕),Presenter將資訊下沉到業務邏輯。因為其它的Presenter觀察了相同的業務邏輯,因此它們從業務邏輯中接收到了同樣變化的通知(Model被更新了)。

[譯]使用MVI打造響應式APP(四):獨立性UI元件

關於這一點,我們已經在 第一章節 討論了 單向資料流 的原理的重要性。

讓我們通過一個真實的案例實現它:在我們的購物App中,我們能夠將商品加入購物車,此外,有這樣一個頁面,我們可以看到購物車商品的內容,並且能夠一次選擇或者刪除多個商品條目:

[譯]使用MVI打造響應式APP(四):獨立性UI元件

我們如果能夠將這樣一個複雜的介面分割成更多 精巧、獨立且可複用的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:SelectedCountPresenterShoppingBasketPresenter。這屬於父子關係嗎?不,它們僅僅是觀察了同一個Model,該Model根據在的邏輯程式碼中進行更新:

[譯]使用MVI打造響應式APP(四):獨立性UI元件

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 的程式碼和上述程式碼的實現非常類似,因此本文不對其進行展示。然而,如果我們認真去觀察這段程式碼,你會發現SelectedCountPresenterShoppingCart有一定的耦合。

我們完全有可能會在其它的頁面去複用這個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元件。

額外的思考

MVPMVVM相比,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

如果您覺得文章還差了那麼點東西,也請通過關注督促我寫出更好的文章——萬一哪天我進步了呢?

相關文章