[译]使用MVI打造响应式APP(四):独立性UI组件

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

这篇博客中,我们将针对如何 如何构建独立组件 进行探讨,我将阐述为什么在我看来 父子关系会导致坏味道的代码,以及为何这种关系是没有意义的。

有这样一个问题时不时涌现在我的脑海中—— MVIMVPMVVM这些架构设计模式中,多个Presenter(或者ViewModel)彼此之间是如何进行通讯的?更直白点说吧,Child-Presenter是如何与Parent-Presenter通讯的?

对我来说,这种 父子关系 会产生坏味道的代码,因为这直接 导致了父子层级之间的耦合,使得代码难以阅读和维护

这种情况下,需求的更改会影响很多的组件(对于大型系统来说,这种情况下实现需求的变动简直难如登天);并非仅此而已,同时,这也 引入了难以预测的共享的状态,其导致的问题甚至难以重现和调试。

其实这也没那么不堪,但我实在不理解为何信息必须从Presenter A流向Presenter B呢?或者Presenter如何与另一个Presenter进行通信?

根本没必要! 什么情况下Presenter才会需要和Presenter进行直接的通讯,是什么事件发生了吗?Presenter根本不需要和其它的Presenter直接通讯,它们都观察了同一个Model(或者说是业务逻辑的相同部分),这就是它们如何获得变化的通知:通过底层。

扫描二维码关注公众号,回复: 5544904 查看本文章

当一些事件发生时(比如用户点击了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:SelectedCountPresenterShoppingBasketPresenter。这属于父子关系吗?不,它们仅仅是观察了同一个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 的代码和上述代码的实现非常类似,因此本文不对其进行展示。然而,如果我们认真去观察这段代码,你会发现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》实战系列

关于我

Hello,我是却把清梅嗅,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的博客或者Github

如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?

猜你喜欢

转载自juejin.im/post/5c8b38476fb9a049b222c365