Model-View-Intent 构建的响应式应用(四)构建 UI 组件

JerryXia 发表于 , 阅读 (26)

标签
不明,不名

Model-View-Intent 构建的响应式应用(四)构建 UI 组件

翻译自 REACTIVE APPS WITH MODEL-VIEW-INTENT - PART3 - BUILDING UI COMPONETS

这篇文章,我们会讨论如何去构建一个独立的 UI 组件,并且阐述一下,我为什么反对父子关系,为什么说,这种关系其实没有必要。

伴随着诸如 MVI 、 MVP 或者 MVVM 之类的设计模式,时常存在着一个问题,Presenter (或者 ViewModel )之间是如何通信的?说得更具体一点,“子级 Presenter ”是如何与“父级 Presenter ”通信的?

我认为,这样的父子关系是一种糟糕的实现,因为这种做法使得父级与子级互相耦合,导致代码的可读性和可维护性都大大降低,需求更改的时候,就会影响许多的组件(也因此,在大型项目中,这几乎就是不可能完成的任务),最重要的是,共享状态的引进,让程序变得难以预测,甚至难以重用、难以测试。

然后呢?不管怎样,信息总要从 Presenter A 流向 Presenter BPresenter 之间到底应该怎样通信?他们不需要! 为什么一个 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>

这些组件相互之间怎样通信?很显然,每个组件都有自己的 PresenterSelectedCountPresenterShoppingBasketPresenter 。这是父子关系吗?不,他们都观察着同样的 Model (从同样的业务逻辑而来):

ShoppingCart-Businesslogic

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;
}
@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(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;
}
@Override 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/FragmentfindViewById() ,也不用 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);

乍一看,这似乎是一个有效的处理方式,但这不就是父子关系的一个变体吗?当然,这并不是严格意义上的父子关系,它更像是洋葱结构(内层给外层提供状态),然而,这不也是一种强耦合的关系吗?我还是不太确定,但现在还是避免这种洋葱结构一样的关系比较好。如果你有不同的观点,请在下方(原文下方啊)留言,我想听一听大家的意见。

Model-View-Intent 构建的响应式应用(三)状态转换机
使用 Kotlin 优化 Intent 数据传递
Please enable JavaScript to view the comments powered by Disqus.