我对MVP以及RxJava的浅薄认识

MVP+RxJava是最近一两年讨论最热的技术,也许你最近还在讨论他们?


via @Hanna Jung in Dribble.

我们了解这种架构模式并且已经运用到实际项目中了,但是我们真的能在该架构模式下获得高效的利益吗?MVP和RxJava库能为用户带来价值吗或者能帮助开发者提升迭代速度吗?

我们都知道MVP能带来两方面的好处:

  • 简洁的代码
  • 完全可测试的业务逻辑

这就是为什么使用MVP+RxJava,因为基于以上两点我们可以在未来的迭代中更加快速。

架构背景

首先在我们对架构知识理解的基础上对代码逻辑分为三(或者更多)层:

  • 数据层(Data):负责从磁盘、网络或者其他实体提供数据。例如,用户位置信息、服务器数据等
  • 领域层(Domain):通常在该层编写可复用的业务逻辑代码来用于数据层和UI层的交互。例如,对位置信息做一些操作等
  • UI层(或者描述层Presentation):界面显示或者用户交互的直接目标。例如,一个按钮的点击等行为


架构分层

在上图的分层中,对于数据层来说,应该保有高百分比可测试性的代码,我们通常不对网络或者存储类进行测试;对于领域层应该是可100%测试的代码;最大的挑战是对UI层代码的测试。

有了对分层架构的基本认识后,我们需要将MVP引入到分层中:



MVP is a pattern used only in the UI layer.

将大量的业务逻辑从UI层移到Presenter层,并且归为相应的类文件,这样也有利于单元测试。

那么,RxJava呢?



RxJava is a library to have these 3 layers communicate with each other.

有了以上知识,我们使用MVP来创建一个UI层的逻辑:

描述层

首先第一步需要清楚将要与用户交互的View都有哪些视图状态。我们不需要关心视图是什么(Activity、Fragment、View 等)因为我们只需要调用接口(StateView)就能完成。

通用的MVP框架

在描述层要定义的动作最少应该有两个,一个是start一个是stop。对于StateView来说可以随着状态的变化而动态更新。视图状态决定当前视图所显示的内容。

Presenter.java

1
2
3
4
5
6
public interface Presenter<S, V extends StateView<S>> {
void onStart(V view);
void onStop();
}

StateView.java

1
2
3
4
public interface StateView<S> {
void updateState(S state);
}

实现

首先,我们启动一个Presenter,Presenter将会收到所有在领域层需要观察的Rx observables并且更新相应的视图。例如检查用户是否登陆

1
accountInteractor.isUserLoggedIn()

接着,转换数据并且判断视图状态做相应的视图变化:

1
2
accountInteractor.isUserLoggedIn()
.map(isUserLoggedIn -> new FilterButtonState(isUserLoggedIn))

最后,我们在onStart中启动并且更新视图

1
2
3
4
5
void onStart(StateView<FilterButtonState> view){
subscription = accountInteractor.isUserLoggedIn()
.map(isUserLoggedIn -> new FilterButtonState(isUserLoggedIn))
.subscribe(view::updateState)
}

当我们停止一个Presenter时,我们需要注销相应的被观察者以防止内存泄漏。

整个代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@ViewScope // dagger 2 scope
class FilterButtonPresenter implements Presenter<FilterButtonState, StateView<FilterButtonState>> {
final AccountInteractor accountInteractor;
Subscription subscription;
@Inject // dagger 2 dependency injection
FilterButtonPresenter(AccountInteractor accountInteractor){
this.accountInteractor = accountInteractor;
}
@Override
void onStart(StateView<FilterButtonState> view){
subscription = accountInteractor.isUserLoggedIn()
.map(isUserLoggedIn -> new FilterButtonState(isUserLoggedIn))
.subscribe(view::updateState);
}
@Override
void onStop(){
subscription.unsubscribe();
}
@AllArgsConstructor // lombok
static class FilterButtonState {
final boolean enabled;
}
}

测试

FilterButtonPresenterTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@RunWith(MockitoJUnitRunner.class)
class FilterButtonPresenterTest {
@Mock
AccountInteractor accountInteractor;
@Mock
StateView<FilterButtonState> view;
@InjectMocks
FilterButtonPresenter testee;
@Test
void userIsLoggedIn(){
given(accountInteractor.isUserLoggedIn())
.willReturn(just(true));
// When
testee.onStart(view);
// Then
verify(view)
.updateState(new FilterButtonState(true));
}
@Test
void userIsLoggedOut(){
given(accountInteractor.isUserLoggedIn())
.willReturn(just(false));
// When
testee.onStart(view);
// Then
verify(view)
.updateState(new FilterButtonState(false));
}
}

UI层

使用已经定义好的视图状态类来更新相应的UI组件。

FilterButtonActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyActivity extends Activity implements StateView<FilterButtonState> {
private Button button;
@Inject
FilterButtonPresenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
// set some content views
Component.activity(this).inject(this); // dagger 2 injection
presenter.start(this);
}
@Override
protected void onDestroy() {
presenter.stop();
}
@Override
public void updateState(FilterButtonState state) {
button.setEnabled(state.enabled);
}
}

这里出现一个问题值得我们深思,我们是要在Activity的相应生命周期中调用Presenter的start和stop吗?还是其他时机也需要调用?

如果我们只是简单的在onStart/onDestroy生命周期中调用start和stop,但我们的activity异常退出怎么办?onSaveedInstance要不要调用相应的状态保存呢?这些事情是很头疼的。再如设备开启了接电保护后,我们不希望在onStop后的Activity中一直运行Presenter的action,那么我们就需要在onStop中执行presenter.stop()。

进一步说明

  • 当计划使用MVP时,一定要把握好视图的可能出现的状态。
  • 如果你要开发一个全部交互的app,那么数据层与领域层以及领域层与UI层都要使用RxJava库来进行通信。这样也比使用从数据层的视图状态来更新UI层的方式更加简单
  • 如果定义两个或者两个以上不同的Presenter时需要清楚UI的状态(例如, 用户从一个ListView中选择一个Item并你想同时更新两个不同的UI),那么建议使用视图状态来动态更新UI
  • 有些情况基于用户与UI界面的交互我们需要更新领域层或者数据层的信息,简单的可以在Presenter中添加相应的方法,在UI交互的时候调用这些方法。