MVP的简介
基本架构
在Android开发过程中,随着页面逻辑的越来越复杂,如果遵从MVC的模式进行开发的话,会导致Activity中的代码变得非常臃肿,增加维护的难度。为了将业务逻辑从Activity中剥离出来,引入了MVP模式。
相较于MVC模式,MVP剥离了View对Model的依赖,剥离了Activity中的业务逻辑代码,使整体的依赖复杂度得以降低。简单的模式图如下:

模块分工
在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 | public interface IPresenter<V extends IView> { |
IView
一个空实现的IView就可以满足要求。
不过有些文章的实现中,会抽取一些公共的UI逻辑到这一层,但是个人认为没有必要这样做,因为保持顶层接口的简单更有利于扩展,更灵活。如果确实有很通用的UI逻辑,那么可以实现一个接口,作为其他View的父接口,当然还得配套实现一个对应的Presenter。
1 | public interface IView { |
MVP的具体设计
View模块
先讨论的是IXXView接口的设计。
有两种主要的实现方案,主要区别在于接口粒度的不同,具体如下。
考虑这样一个场景,Presenter获取用户数据User,然后更新用户界面的用户信息。
第一种方案是很多MVPDemo中的标准设计,接口粒度小。
1 | public interface IUserView extends IView { |
这种设计的优点在于View是完全和业务无关的,View的具体实现层需要的就是更新用户名。但是如果有另外的场景,View在取到User后不是用来更新用户信息,而是用用户信息进行了其他的UI更新(用updateUserInfo无法描述这种更新行为),那么此时就会遇到接口难以复用的问题。为了解决这种问题,就有了下面的第二种设计方式:
1 | public interface IUserView extends IView { |
显而易见,这样设计的结果使得IXXView接口与Presenter的关系更紧密,与View的具体实现关系更疏远,使得IUserView更容易被复用。关于复用,具体会在后面再讨论,这里暂时不展开讲了。
由于View的具体实现一般是Activity与Fragment,所以避不开的是监听事件的注册、Activity.onActivityResult()、Activity跳转等逻辑在哪处理的问题。个人的建议是为了保持Presenter的简单,可以将这一部分逻辑就放在Activity中完成,让View层去处理这些比较特殊的逻辑。
Presenter模块
Presenter的主要功能就是调用Model获取数据,整合业务逻辑,最后将结果传递给View,用于更新UI。
这里的重点在于理解什么是整合业务逻辑,举例来说,有一个场景,我们要获取用户信息并显示,如果获取失败则报错,这个行为其实包括以下几个子行为:获取用户信息、显示用户信息、显示错误信息。如何调节这几个行为(整合业务逻辑)就是Presenter的主要工作。
1 | // 一个简单的Presenter实现 |
另外关于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 | // Repository负责管理缓存以及数据获取的相关逻辑 |
上面的Model层实现将Model分为了两层,好处是Presenter不需要知道数据的具体来源,数据管理的任务全权由Repository来负责。当然有的时候我们的业务很简单,只需要从网络获取数据然后显示即可,那么这时候Presenter就可以直接去持有LocalSource(要实现这样的功能,还需要一个通用的接口,即Model接口,约定数据获取接口,被Repository与XXXSource实现),而不需要Repository作为中介。如果业务更复杂,那么Model层也可以加入更多的类去辅助实现。
但是无论如何,不变的是,对于Presenter来说,Model层必须提供数据获取功能,其他功能都是围绕这个核心点按需扩展的。
将上面的结构画成简单的结构图,如下:

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

对于抽象层来说,我们可以抽取一些公共业务用于建立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 | // UserActivity.java |
单个界面,多个View,使用不同Model,但View的行为之间存在交互
与上面的例子类似,有一个Activity,包含一个文件列表和一个用户详情页,如果我们点击了其中一个文件Item,那么用户详情页就显示该文件所有者(用户)的详情。
针对这样的情况,在这个问题中,有所讨论,Android MVP, how to coordinate multiple views?
但我个人因为不喜欢使用EventBus,所以不建议使用答题者提到的实现方式。
个人倾向于直接在Activity或者Fragment的IView回调接口中直接处理这种交互逻辑,因为这种交互逻辑很多时候就是这个页面专属的一种逻辑,直接在这里处理并无不妥。
1 | // UserActivity.java |
与其他框架组合使用
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