翻译自 REACTIVE APPS WITH MODEL-VIEW-INTENT - PART3 - BUILDING UI COMPONETS 。
这篇文章,我们会讨论如何去构建一个独立的 UI 组件,并且阐述一下,我为什么反对父子关系,为什么说,这种关系其实没有必要。
伴随着诸如 MVI 、 MVP 或者 MVVM 之类的设计模式,时常存在着一个问题,Presenter (或者 ViewModel )之间是如何通信的?说得更具体一点,“子级 Presenter ”是如何与“父级 Presenter ”通信的?

我认为,这样的父子关系是一种糟糕的实现,因为这种做法使得父级与子级互相耦合,导致代码的可读性和可维护性都大大降低,需求更改的时候,就会影响许多的组件(也因此,在大型项目中,这几乎就是不可能完成的任务),最重要的是,共享状态的引进,让程序变得难以预测,甚至难以重用、难以测试。
然后呢?不管怎样,信息总要从 Presenter A 流向 Presenter B ,Presenter 之间到底应该怎样通信?他们不需要! 为什么一个 Presenter 必须向另一个 Presenter 传递数据?发生了什么必然事件吗?Presenter 之间没有必要交流,他们只需要观测同样的 Model (更准确地说是业务逻辑)就行。这就是他们获取变化消息的方式:从最底层来。

当事件X(比如,用户点击了 View 1 的一个按钮)发生的时候,Presenter 1 就会让信息向下传递到业务逻辑层,而另一个 Presenter 正关注着同一个业务逻辑,他们就可以通过业务逻辑得知发生改变的地方(Model 已经更新)。

我们已经在第一部分已经讨论过单向数据流的重要性了。
现在就在实际项目里实现这个例子吧:在我们的购物应用中,我们要向购物车内添加商品。除此之外,还要有一个可以用来查看购物车内容的页面,在这里,我们也能方便地移除商品。
想象一下,如果我们能够把屏幕里的东西抽取出来,变成独立的可重用的 UI 组件该是多么棒啊!看看那个 Toolbar ,它展示了选中商品的个数,还有那个 RecyclerView ,它展示了购物车里的每一个商品。
1 2 3 4 5 6 7 8 9 10 11 | <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 :SelectedCountPresenter 和 ShoppingBasketPresenter 。这是父子关系吗?不,他们都观察着同样的 Model (从同样的业务逻辑而来):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class SelectedCountPresenter extends MviBasePresenter<SelectedCountView, Integer> { private ShoppingCart shoppingCart; public SelectedCountPresenter(ShoppingCart shoppingCart) { this.shoppingCart = shoppingCart; } protected void bindIntents() { subscribeViewState(shoppingCart.getSelectedItemsObservable(), SelectedCountView::render); } } class SelectedCountToolbar extends Toolbar implements SelectedCountView { // ... public void render(int selectedCount) { if (selectedCount == 0) { setVisibility(VISIBLE); } else { setVisibility(INVISIBLE); } } } |
ShoppingBasketRecyclerView 的代码看起来差不多,在此略过。然而,如果我们仔细看 SelectedCountPresenter ,就会注意到他和 ShoppingCart 耦合了。我们想要这个控件也能在其他地方使用。所以为了组件的可重用性,我们必须移除这层依赖。完成重构很容易:Presenter 可以获取 Observable<Integer> 作为 Model :
1 2 3 4 5 6 7 8 9 | public class SelectedCountPresenter extends MviBasePresenter<SelectedCountView, Ingeter> { private Observable<Integer> selectedCountObervable; public SelectedCountPresenter(Observable<Integer> selectedCountObservable) { this.selectedCountObservable = selectedCountObservable; } protected void bindIntents() { subscribeViewState(selectedCountObservable, SelectedCountToolbarView::render); } } |
到这里,我们在任何想要显示选中数目的时候,都可以重用这个组件了。可以是 ShoppingCart 中的商品数目,也可以是完全不相干的内容。甚至,这个 UI 组件还可以放到一个独立的库中,以便其他应用使用,比如在一个相册应用中,用来展示选中的图片数量。
1 2 3 4 5 6 7 8 9 | Observable<Integer> selectedCount = photoManager.getPhotos() .map(photos -> { int selected = 0; for(Photo item: photos) { if (item.isSelected()) selected++; } return selected; }) return new SelectedCountTollbarPresenter(selectedCount); |
结论
这篇文章旨在展示一下,父子关系通常是没有必要的,可以通过简单地观察同一个业务逻辑代码来避免。不用 EventBus ,不用从 Activity/Fragment 中 findViewById() ,也不用 presenter.getParentPresenter() 或者其他相关的工作。
更多思考
与 MVP 和 MVVM 相比,MVI 中强制要求业务逻辑驱动组件的状态,因此,熟练使用 MVI 的开发者会有这样的困惑:
如果一个视图状态是另一个组件的
Model怎么办?如果一个组件的视图状态的变化是另一个组件的intent怎么办?
举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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); |
乍一看,这似乎是一个有效的处理方式,但这不就是父子关系的一个变体吗?当然,这并不是严格意义上的父子关系,它更像是洋葱结构(内层给外层提供状态),然而,这不也是一种强耦合的关系吗?我还是不太确定,但现在还是避免这种洋葱结构一样的关系比较好。如果你有不同的观点,请在下方(原文下方啊)留言,我想听一听大家的意见。