聊一聊MVP

MVP的简介

基本架构

在Android开发过程中,随着页面逻辑的越来越复杂,如果遵从MVC的模式进行开发的话,会导致Activity中的代码变得非常臃肿,增加维护的难度。为了将业务逻辑从Activity中剥离出来,引入了MVP模式。

相较于MVC模式,MVP剥离了View对Model的依赖,剥离了Activity中的业务逻辑代码,使整体的依赖复杂度得以降低。简单的模式图如下:

img

模块分工

在MVP模式中,各个模块的分工如下

  • View:一般为Activity或Fragment,或其他View以及View的容器。它的工作模式就像一个黑盒,给定特定的数据输入,然后执行相应的View更新操作。
  • Presenter:主要职责是从Model层获取数据,然后传递给View。定义了数据与视图交互的业务逻辑。
  • Model:决定了数据获取的具体逻辑,具体实现应该与上层的业务逻辑剥离。
优缺点

MVP模式对于Android项目来说,主要的优势体现在两方面,

  • 一个是相较于MVC模式,将大量的业务逻辑从Activity中剥离了出来,使得代码逻辑更清晰且容易维护
  • 另一方面是相较于MVC,剥离了M与V的依赖关系,这样的实现使得单元测试更容易编写。

MVP的缺点主要体现在:

  • 为了实现解耦,引入了大量的接口
  • Activity虽然不再臃肿,但是Presenter却更加臃肿了
  • 从架构图上看,所有的UI改变都是由Presenter驱动的主动更新,因此无法做到响应式的被动更新,也就不好应对一些适合响应式的场景。

MVP基类的设计

IPresenter

由于Model层的数据获取大部分都是异步的,因此有可能出现数据到达后,View因为被回收或者被遮挡而无需更新的情况,因此需要Presenter能动态绑定与解绑View。所以Presenter的基类可以这么设计

1
2
3
4
5
public interface IPresenter<V extends IView> {
void bindView(V view);

void unbindView();
}
IView

一个空实现的IView就可以满足要求。

不过有些文章的实现中,会抽取一些公共的UI逻辑到这一层,但是个人认为没有必要这样做,因为保持顶层接口的简单更有利于扩展,更灵活。如果确实有很通用的UI逻辑,那么可以实现一个接口,作为其他View的父接口,当然还得配套实现一个对应的Presenter。

1
2
3
4
5
6
7
8
9
10
11
public interface IView {
// 诸如此类的方法可以下沉到下层的接口中,保持IView接口为空
void showLoading();
void hideLoading();
}

// 单独实现一个View接口作为其他View接口的父接口
public interface ILoadingView extends IView {
void showLoading();
void hideLoading();
}

MVP的具体设计

View模块

先讨论的是IXXView接口的设计。

有两种主要的实现方案,主要区别在于接口粒度的不同,具体如下。

考虑这样一个场景,Presenter获取用户数据User,然后更新用户界面的用户信息。

第一种方案是很多MVPDemo中的标准设计,接口粒度小。

1
2
3
public interface IUserView extends IView {
void updateUserInfo(User user);
}

这种设计的优点在于View是完全和业务无关的,View的具体实现层需要的就是更新用户名。但是如果有另外的场景,View在取到User后不是用来更新用户信息,而是用用户信息进行了其他的UI更新(用updateUserInfo无法描述这种更新行为),那么此时就会遇到接口难以复用的问题。为了解决这种问题,就有了下面的第二种设计方式:

1
2
3
public interface IUserView extends IView {
void onUserInfoLoaded(User user);
}

显而易见,这样设计的结果使得IXXView接口与Presenter的关系更紧密,与View的具体实现关系更疏远,使得IUserView更容易被复用。关于复用,具体会在后面再讨论,这里暂时不展开讲了。

由于View的具体实现一般是Activity与Fragment,所以避不开的是监听事件的注册、Activity.onActivityResult()、Activity跳转等逻辑在哪处理的问题。个人的建议是为了保持Presenter的简单,可以将这一部分逻辑就放在Activity中完成,让View层去处理这些比较特殊的逻辑。

Presenter模块

Presenter的主要功能就是调用Model获取数据,整合业务逻辑,最后将结果传递给View,用于更新UI。

这里的重点在于理解什么是整合业务逻辑,举例来说,有一个场景,我们要获取用户信息并显示,如果获取失败则报错,这个行为其实包括以下几个子行为:获取用户信息、显示用户信息、显示错误信息。如何调节这几个行为(整合业务逻辑)就是Presenter的主要工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 一个简单的Presenter实现
public class UserInfoPresenter extends IPresenter {
private IUserView view;
private UserModel model;
void getUserInfo(long id) {
// 这里的代码就是整合业务逻辑
view.showLoading();
model.getUserInfo(id, new Callback<User> {
void onSucceed(User user) {
view.onUserInfoLoaded(user);
view.hideLoading();
}

void onFailed(Exception e) {
view.onUserInfoLoadFailed(e);
view.hideLoading();
}
});
}
}

另外关于Presenter的细节讨论较多的有两点:

  • 应不应该将Context传递给它。为了保证Presenter更易于测试,我们应该保证Presenter与Android Framework是无关的。如果你一定要在Presenter中使用Context,可以参考这个问题中的建议,Get Context in Presenter
  • 线程调度是否应该由Presenter实现。可以根据个人的喜好,选择将线程切换的功能放在Presenter实现或者Model中实现。如果在Presenter中实现的话,需要考虑封装一下任务调度器,以屏蔽Android Framework中的Handler对Presenter的直接侵入。具体的例子可以参考:BaseSchedulerProvider.java
Model模块

Model层负责获取与管理数据,并向Presenter提供它所需要的数据,由于数据获取是异步的,因此它需要通过回调通知Presenter数据的获取状态。

为了获取数据,我们首先需要实现的是提供数据获取的类,这里的实现没有任何限制。例如从网络获取数据,我们可以使用okHttp封装或使用其他你喜欢的框架。总之最后的类能做到提供接口向调用者提供所需数据即可。

如果我们的数据有内存缓存的需求,那么我们可以实现一个XXXRepository类来缓存数据,并由它来调用上面实现的数据获取类,然后在获取数据后进行缓存相关的处理再返回给Presenter。

如果我们没有在Presenter层与View层做线程切换的操作,那么我们的Model层需要完成这一部分工作。

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
38
39
40
41
42
// Repository负责管理缓存以及数据获取的相关逻辑
class UserInfoRepository {
// 从本地缓存获取数据
private UserLocalSource localSource;
// 从网络获取数据
private UserRemoteSource remoteSource;
// 内存缓存
private User mCachedUserinfo;

public void getUserInfo(long id, boolean forceUpdate, Callback callback) {
// 根据是否需要强制更新数据决定是否需要从内存缓存读取数据
if (forceUpdate) {
remoteSource.getUserInfo(id, new Callback() {
void onSucceed(User user) {
mCachedUserinfo = user;
callback.onSucceed(user);
}
}
});
} else {
callback.onSucceed(user);
}
}
}

// 从网络获取数据,这里的线程切换实现可以用任意你喜欢的方式,这里只是随便举一个例子
class UserRemoteSource {
public void getUserInfo(long id, Callback callback) {
TaskManager.execute(new Task<User>() {
// 运行在子线程
User doInBackground() {
// 同步的网络请求
return UserInfoApi.getUserInfo(id);
}

// 运行在主线程
void onPostExecute(User user) {
callback.onSucceed(user);
}
})
}
}

上面的Model层实现将Model分为了两层,好处是Presenter不需要知道数据的具体来源,数据管理的任务全权由Repository来负责。当然有的时候我们的业务很简单,只需要从网络获取数据然后显示即可,那么这时候Presenter就可以直接去持有LocalSource(要实现这样的功能,还需要一个通用的接口,即Model接口,约定数据获取接口,被Repository与XXXSource实现),而不需要Repository作为中介。如果业务更复杂,那么Model层也可以加入更多的类去辅助实现。

但是无论如何,不变的是,对于Presenter来说,Model层必须提供数据获取功能,其他功能都是围绕这个核心点按需扩展的。

将上面的结构画成简单的结构图,如下:

image-20180626212442230

简单总结

上面给出的实现方案只是我个人在实践中觉得比较合适的一种方案,可能受限于我们自己项目的特殊性质。所以在实际应用MVP模式的时候,不一定就要按照某种模板来,应该根据自己的业务场景做一定的扩展与改动。

将我们上面讨论的设计画成简单的图,如下:

image-20180627203504539

对于抽象层来说,我们可以抽取一些公共业务用于建立IPresenter与IView以及IModel间的依赖关系。需要注意的是,由于不同业务的Model可能复杂度差异较大,且并不是所有的Model实现都需要可复用,所以要根据实际情况决定是否定义IXXModel,避免定义过多无用的接口。通用的View接口与Presenter接口也是同理,按需去实现。

业务层的实现要点在于根据业务确定Presenter的功能,然后根据这些功能设计抽象的View与Model接口。

功能实现层不用多说,就是具体的View操作与数据获取的实现,根据具体业务可以调整Model层的具体设计。其他的一些设计细节在上面都讨论过了,这里就不再赘述了。

关于复用

View的复用

这里的View复用指的是功能实现层的View的复用,这一层的复用与MVP的设计关系不大,所以这里就不讨论了。

Model的复用

这里的Model复用指的是功能实现层的复用。由于我们的Model可以说是完全独立的,所以如果有其他的Presenter也需要用到UserRemoteSource,那么就可以直接复用,没有任何额外的成本开销。

Presenter的复用

比如我们有一个评论列表展示的功能,评论列表的种类有很多,分别在展示不同的页面中,他们的展示逻辑相同,但是获取数据的网络请求以及UI细节不同。那么这时候我们就需要在这些页面复用我们的Presenter。

因为我们将Model都抽象出了接口,我们就可以实现不同的Model以适应网络请求不同的需求。

至于UI细节的不同,就需要考虑到我们上面提到的IXXView的接口粒度的设计,在这个场景下,一个粗粒度的接口设计更有利于Presenter的复用。这里再提一个细节,有的View接口不一定在所有的View实现中都有具体的实现,因此在复用的时候可能会有很多空实现,为了解决这个问题,可以考虑使用Java 8或者Kotlin接口默认实现的特性。

MVP在不同业务场景的使用

在开始这一节之前,先提一点,在根据业务去设计MVP相关类的时候,需要先梳理某个具体功能模块的需求,将这个功能模块(如果它比较复杂)拆分成独立的几个子模块,然后这几个子模块各自使用MVP去实现。这样的实现可以保证不同功能模块的View与Presenter都是独立的,不会相互影响。这样的做法一方面使得代码的逻辑更清晰,另一方面也可以规避一些MVP不好处理的场景。

接下来讨论在几种较为复杂的场景下,MVP的具体应用。

一个界面,多个独立的View

这种情况就是我们在本节开头说的一种情况,页面内多个View在业务逻辑上相互独立,View相互之间不知道对方的存在。对于这种情况,只需要每个View各自实现一组业务层MVP即可。

多个界面,多个View,使用同一个Model

这种情况,举例来说,列表页A是用户列表页,点击其中一个Item,跳转到用户详情页B。这两个界面使用的是同一个模型实例来渲染各自的UI。为了做到模型实例的复用,可以将Repository设计为单例模式,同时A、B使用同一组业务层MVP。

具体的细节可以参考谷歌开源的安卓架构sample项目:android-architecture

单个界面,多个View,使用同一个Model

如果你遇到了这种场景,那么先要考虑能否将它变为前面介绍的一个界面,多个独立的View的场景,如果不行的话,再考虑下面介绍的方案。

举例来说,有一个Activity,包含一个用户列表Fragment和一个用户详情Fragment。如果在用户详情Fragment中编辑了用户详情,那么用户列表对应的Item显示信息也要同时更新。

这样的场景其实就产生了View之间需要沟通的需求,解决方案是使用Activity作为Fragment之间沟通的桥梁,两个Fragment的Presenter持有Activity,而Activity负责管理两个View之间的关系。工作时,Presenter通知Activity数据更新了,然后Activity再通知View数据更新了。

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
38
39
40
41
42
43
44
45
46
47
48
49
// UserActivity.java
class UserActivity implements IUserView {

private UserListFragment listFragment;
private UserDetailFragment detailFragment;

@Override
void onUserInfoEdited(User user) {
listFragment.onUserInfoEdited(User user);
detailFragment.onUserInfoEdited(User user);
}

}

// UserDetailFragment.java
class UserDetailFragment implements IUserView {

private UserPresenter userPresenter;

@Override
public void onCreate(Bundle savedInstanceState) [
super.onCreate(savedInstanceState);
userPresenter = new UserPresenter((IUserView)getActivity())
]

@Override
void onUserInfoEdited(User user) {
// update UI
}

}

// UserListFragment.java
class UserListFragment implements IUserView {

private UserPresenter userPresenter;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
userPresenter = new UserPresenter((IUserView)getActivity())
}

@Override
void onUserInfoEdited(User user) {
// update UI
}

}
单个界面,多个View,使用不同Model,但View的行为之间存在交互

与上面的例子类似,有一个Activity,包含一个文件列表和一个用户详情页,如果我们点击了其中一个文件Item,那么用户详情页就显示该文件所有者(用户)的详情。

针对这样的情况,在这个问题中,有所讨论,Android MVP, how to coordinate multiple views?

但我个人因为不喜欢使用EventBus,所以不建议使用答题者提到的实现方式。

个人倾向于直接在Activity或者Fragment的IView回调接口中直接处理这种交互逻辑,因为这种交互逻辑很多时候就是这个页面专属的一种逻辑,直接在这里处理并无不妥。

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
// UserActivity.java
// 具体交互逻辑:点击文件item,请求文件item对应的userId,然后根据userId请求用户详情,然后更新UI
// 这样的接口设计虽然不合理,但是为了举例,就不用在意这些细节了
class UserFileActivity implements IUserView, IFileView {

private UserPresenter userPresenter;
private FilePresenter filePresenter;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
userPresenter = new UserPresenter(this));
filePresenter = new FilePresenter(this));
mAdapter.setOnItemClickListener(new OnItemClickListener<FileItem>(View view, int pos, FileItem file) {
filePresenter.getFileOwner(file.id);
})
}

@Override
public void onFileOwnerLoaded(long fileId, long userId) {
// 在这里处理交互逻辑
userPresenter.getUserInfo(userId);
}

@Override
public void onUserInfoLoaded(User user) {
// 更新用户详情页
}

}

与其他框架组合使用

RxJava

RxJava在MVP中的使用,主要体现在Presenter与Model的实现中。在Model层中使用Observable封装数据请求,然后再在Presenter中利用RxJava处理数据并做线程切换。具体的例子参考:googlesamples/android-architecture/todo-mvp-rxjava

Dagger2

MVP的实现过程中,有很多M、V、P之间绑定的代码,这些代码都可以通过Dagger2来简化,具体实现细节也可以参考谷歌官方的实现:googlesamples/android-architecture/todo-mvp-dagger

参考资料

Android MVP and Context

mosby

MVPArms

android-architecture

Implementing Model-View-Presenter on Android – Part 1