[译]使用 MODEL-VIEW-INTENT 第四部分 — 独立 UI 组件

在这篇博客咱们将讨论如何构建独立UI组件,而且要弄清楚为何在我看来子类和父类关系充满着坏代码的味道。此外,咱们将讨论为何我认为这种关系是没必要要的。前端

不时的出现诸如 Model-View-Intent,Model-View-Presenter 或 Model-View-ViewModel 之类的架构设计模式的一个问题是,Presenter(或ViewModels) 之间是如何通讯的?甚至更具体一点,"子-Presenter"如何与它的"父-Presenter"进行沟通?java

wtf

父子关系的组件充满着代码异味,由于它们表示了一种父类与子类的直接耦合,这就致使了代码很难阅读,很难维护,当需求发生变化会影响不少组件(尤为是在大型系统中几乎是不可能完成的任务)最后,一样重要的是,引入了不少很难预测甚至更难去复刻和调试的共享状态。android

到如今为止还挺好的,可是咱们假设信息必须从 Presenter A 流向 Presenter B:如何让不一样的 Presenter 相互间通讯? 它们不通讯!什么样的场景才须要一个 Presenter 不得不与另外一个 Presenter 通讯?事件 X 发生了?Presenters 彻底不用相互间通讯,他们仅仅观察相同的 Model(或者精确到相同的业务逻辑)。这是它们如何获得关于变化的通知:从底层。ios

Presenter-Businesslogic

不管什么时候一个事件X发生了(例如:一个用户点击了在View1上的按钮), 这个 Presenter 会让信息下沉到业务逻辑。既然其余的 Presenter 观察相同的业务逻辑, 他们从已经变化的业务逻辑(model 已经发生变化)里获得通知。git

Presenter-Businesslogic

咱们已经在第一部分强调了一个很重要的原则(单向数据流)。github

让咱们用真实案例来实现上面的内容:在咱们的电商 app 咱们能够将任意一项商品放到购物车里。另外,这里还有一个页面,咱们能够看到咱们购物车的全部商品,而且咱们一次性能够选择或者移除多个商品项。后端

若是咱们能够把这个大的页面分离成不少小的,独立的而且可复用的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:app

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前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索