[譯]使用 MODEL-VIEW-INTENT 第四部分 — 獨立 UI 元件

pcdack發表於2019-02-21

在這篇部落格我們將討論如何構建獨立UI元件,並且要弄清楚為什麼在我看來子類和父類關係充滿著壞程式碼的味道。此外,我們將討論為什麼我認為這種關係是不必要的。

不時的出現諸如 Model-View-Intent,Model-View-Presenter 或 Model-View-ViewModel 之類的架構設計模式的一個問題是,Presenter(或ViewModels) 之間是如何通訊的?甚至更具體一點,"子-Presenter"如何與它的"父-Presenter"進行溝通?

wtf

父子關係的元件充滿著程式碼異味,因為它們表示了一種父類與子類的直接耦合,這就導致了程式碼很難閱讀,很難維護,當需求發生變化會影響很多元件(尤其是在大型系統中幾乎是不可能完成的任務)最後,同樣重要的是,引入了很多很難預測甚至更難去復刻和除錯的共享狀態。

到現在為止還挺好的,但是我們假設資訊必須從 Presenter A 流向 Presenter B:如何讓不同的 Presenter 相互間通訊? 它們不通訊!什麼樣的場景才需要一個 Presenter 不得不與另一個 Presenter 通訊?事件 X 發生了?Presenters 完全不用相互間通訊,他們僅僅觀察相同的 Model(或者精確到相同的業務邏輯)。這是它們如何得到關於變化的通知:從底層。

Presenter-Businesslogic

無論何時一個事件X發生了(例如:一個使用者點選了在View1上的按鈕), 這個 Presenter 會讓資訊下沉到業務邏輯。既然其他的 Presenter 觀察相同的業務邏輯, 他們從已經變化的業務邏輯(model 已經發生變化)裡得到通知。

Presenter-Businesslogic

我們已經在第一部分強調了一個很重要的原則(單向資料流)。

讓我們用真實案例來實現上面的內容:在我們的電商 app 我們可以將任意一項商品放到購物車裡。另外,這裡還有一個頁面,我們可以看到我們購物車的所有商品,並且我們一次性可以選擇或者移除多個商品項。

[譯]使用 MODEL-VIEW-INTENT 第四部分 — 獨立 UI 元件

如果我們可以把這個大的頁面分離成很多小的,獨立的並且可複用的UI元件,那豈不是很酷?比如說一個 Toolbar,它顯示被選擇的 item 的數量,和一個用來顯示購物車裡的商品項列表的 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(從相同的業務邏輯裡獲取更新):

ShoppingCart-Businesslogic

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 我們會注意到這個 Presenter 與 shoppingcart 耦合。我們想要使用這個 UI 元件可以在我們 App 的其他的頁面使用,讓這個元件變的可複用,我們需要移除這個依賴,這事實上是一個簡單的重構:這個 Presenter 得到一個 Observable 作為 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);
  }
}
複製程式碼

就是這樣,任何時候,當我們想要顯示當前 item 選擇數量的時候,我們可以用這個 SelectedCountToolbar 元件。這個元件在購物車,可以記物品項的數量。但是,這個 UI 控制元件也可以用在你 App 裡完全不同的情景下。此外,這個 UI 控制元件可以放在一個獨立庫中,並且在其他的 app 中使用,比如一個能顯示選擇多少張照片的 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 的幫助,RxJava 是實現觀察者模式的基礎,我們可以很輕鬆的構建這樣的響應式 UI 元件。

另外的思考

通過與 MVP 或者 MVVM 的對比,在 MVI 我們強制(用一種激進的方法)讓業務邏輯驅動一定的元件狀態。故在使用 MVI 上有經驗的開發者總結出下面結論:

如果一個 view 狀態是另一個元件的 model?如果 view 的狀態在一個元件中發生了變化,這個變化是另一個元件的意圖,那麼如何處理?

例子:

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”的一部分。 下面是內容表:

這是這個系列部落格的中譯版:


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章