[译]使用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(或者说是业务逻辑的相同部分),这就是它们如何获得变化的通知:通过底层。

image

当一些事件发生时(比如用户点击了View1按钮),Presenter将信息下沉到业务逻辑。因为其它的Presenter观察了相同的业务逻辑,因此它们从业务逻辑中接收到了同样变化的通知(Model被更新了)。

image

关于这一点,我们已经在 第一章节 讨论了 单向数据流 的原理的重要性。

让我们通过一个真实的案例实现它:在我们的购物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根据在的逻辑代码中进行更新:

image
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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352