MVP是从MVC衍生出来的,所以,先说一下MVC,再到MVP。
MVC
MVC分为三个部分:
视图(View):用户界面。
控制器(Controller):业务逻辑,用于控制应用程序的流程。
模型(Model):数据处理。即网络请求、数据库等一些对数据的操作处理。
它们之间的交互通常如下图:

那么在我们android里,如何划分这几层?在这里,参考部分博文,以及根据我个人的理解,列出下面两种可能的情况:
(一)View:xml。Controller:Activity/Fragment。Model:数据处理。
如果是这样划分的话。实际运用中,往往会出现Activity/Fragment虽然划分为Controller层,但看起来又像View层。
为什么?因为如果只用xml作为View层,对界面的控制能力实在太弱了,无法动态更新UI。所以,Activity/Fragment也要担当起一部分View层的责任,负责动态更新视图。这就导致Activity/Fragment既有更新View控件的代码,又有对源自Model的数据进行进一步逻辑处理的代码,随着迭代开发,Activity/Fragment会显得越来越臃肿,分分钟几千行代码,给后期维护带来极大的困扰。如果你接手这样的代码,心里恐怕会不停赞叹前人真牛逼!

很多博客,描述MVC时,都是这样划分MVC的,并指出:“View层和Model层是相互可知的,这意味着两层之间存在耦合”。
我个人觉得这话有谬误。View层和Model层一定是互相可知的?MVC的交互图,V与M的箭头,只代表它们可以互相给对方发送消息。但并不意味着它们之间就一定互相可知。

如果View层和Model层是相互可知的,这也就意味着它们互相持有对方的引用,是通过对方的引用来给彼此发送消息。这确实意味着两层之间存在耦合。下面是这种情况下,View和Model交互的代码。
class XActivity {
public void attachModel() {
VersionModel model = new VersionModel(this);
model.checkUpdate();
}
public void onResult(String result) {
System.out.println(this.getClass().getSimpleName() + "收到了回应:" + result);
}
}
class VersionModel {
private XActivity mActivity;
//注意:activity的引用通过构造方法传递了进来
public VersionModel(XActivity activity) {
mActivity = activity;
}
public void checkUpdate() {
System.out.println("检查更新!");
//用activity的引用传递更新信息。这样,该方法就会绑定死了这个activity。
mActivity.onResult("暂无更新!");
}
}
public class Couple {
public static void main(String[] args) {
//这个VersionModel只能提供给XActivity这个界面使用。
XActivity xActivity = new XActivity();
xActivity.attachModel();
}
}/* Output:
检查更新!
XActivity收到了回应:暂无更新!
*/
但这样的代码,实在是太糟糕了。因为Model层的代码,复用性是很有必要的。比如,这个检查版本更新的Model,也许除了闪屏页需要,你的设置界面也需要有这个功能。像上面这样写,那你的Model和View绑定死了。所以,更恰当的做法,是下面这样。Model的数据处理结果,通过接口回调给View,保证Model的复用性。
class XActivity {
public void attachModel() {
//跟前面不同,不再是直接传递自身引用给Model,而是传一个回调接口,通过回调接口,获取数据处理结果
VersionModel model = new VersionModel(new Callback() {
@Override
public void onResult(String result) {
System.out.println(XActivity.this.getClass().getSimpleName() + "收到了回应:" + result);
}
});
model.checkUpdate();
}
}
class YActivity {
public void attachModel() {
//跟前面不同,不再是直接传递自身引用给Model,而是传一个回调接口,通过回调接口,获取数据处理结果
VersionModel model = new VersionModel(new Callback() {
@Override
public void onResult(String result) {
System.out.println(YActivity.this.getClass().getSimpleName() + "收到了回应:" + result);
}
});
model.checkUpdate();
}
}
interface Callback {
void onResult(String result);
}
class VersionModel {
private Callback callback;
//不再是直接接受某个Activity实例做参数,这样就不会再与某个Activity过于耦合,提高了复用性
public VersionModel(Callback callback) {
this.callback = callback;
}
public void checkUpdate() {
System.out.println("检查更新!");
//用的是回调接口,传递更新信息。该方法,不会再是绑定死某个activity。
callback.onResult("暂无更新!");
}
}
public class Decouple {
public static void main(String[] args) {
//这个VersionModel可以随意提供给N个界面使用。
XActivity xActivity = new XActivity();
xActivity.attachModel();
YActivity yActivity = new YActivity();
yActivity.attachModel();
}
}/* Output:
检查更新!
XActivity收到了回应:暂无更新!
检查更新!
YActivity收到了回应:暂无更新!
*/
(二)View:"xml+Activity/Fragment"视为View层,仅仅负责控件更新。Controller:将Activity/Fragment中的逻辑控制,包括对源自Model层的数据的进一步处理的操作,抽取出来,到类似XxxController这样命名的类里,作为Controller。作为Controller。Model:数据处理。
这是我自己瞎琢磨的一种划分方法。
如果这样划分,可以极大地减轻了View层的负担。但是,现在业务逻辑处理都被抽调到了Controller。而当Controller需要根据Model的数据处理结果,来决定View下一步做什么的时候,只能通过View去获得(因为View层和Model层可以互相交互。而Controller只能单方向跟Model发消息)。而View自身已经不负责业务逻辑处理了,还得夹在它们中间,简直多此一举。
MVP的写法,其实就是基于这种划分的。但使用MVP来实现,不会有这种尴尬。
MVP
如果MVC改一改:Controller改名为Presenter,View和Model不再允许交互,Controller(Presenter)和Model之间的单向通讯,改为双向。那么这种做法,就是所谓的MVP了。
Google开源的MVP架构示例项目:android-architecture,也是基于这种划分:
View:xml+Activity/Fragment。
Presenter:根据业务逻辑,对Model获得的结果进一步处理,最后决定View什么时候更新UI。
Model:数据处理。
如下图:

android-architecture这个项目有很多分支,本文主要参考的是todo-mvp、todo-mvp-rxjava、todo-mvp-dagger三个分支。这几个分支对MVP的划分是一致的,只是实现细节上的差别。想学习rxjava2和dagger2的,推荐翻阅这两个分支。
MVP的运作过程,大致可以这么理解:View给Presenter指派任务,然后Presenter调控一个或多个Model,给它们划分各种小任务,配合完成任务。最后,Model通过回调接口,告诉Presenter任务结果,然后Presenter根据任务的完成情况,通知View更新UI。
之前用于mvc讨论的检查更新功能,如果用MVP写。关键的代码如下:
public interface SplashContract {
interface View{
void showUpdateDialog(VersionBean versionBean);
void jumpToMain();
}
interface Presenter{
void attachView(SplashContract.View view);
void detachView();
void checkUpdate();
}
}
public interface VersionCallback{
void onUpdate(VersionBean versionBean);
void onNoUpdate();
}
public class VersionBean{
private int versionCode;
private String versionName;
private String updateUrl;
public VersionBean(int versionCode,String versionName,String updateUrl){
this.versionCode = versionCode;
this.versionName = versionName;
this.updateUrl = updateUrl;
}
//省略get/set方法
......
}
public class SplashActivity extends AppCompatActivity implements SplashContract.View{
private SplashContract.Presenter mPresenter;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
attachPresenter();
checkUpdate();
}
public void attachPresenter(){
//注意,使用的是Application的Context
VersionModel versionModel = new VersionModel(getApplicationContext());
mPresenter = new SplashPresenter(versionModel);
mPresenter.attachView(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
mPresenter.detachView();
}
public void checkUpdate(){
mPresenter.checkUpdate();
}
@Override
public void showUpdateDialog(VersionBean versionBean){
//弹更新提示dialog
}
@Override
public void jumpToMain(){
//跳转到MainActivity
}
}
public class SplashPresenter implements SplashContract.Presenter{
private VersionModel mVersionModel;
private SplashContract.View mView;
public SplashPresenter(VersionModel versionModel){
mVersionModel = versionModel;
}
@Override
public void attachView(SplashContract.View view){
mView = view;
}
public void onDetachView() {
//可以在此取消所有的异步订阅
}
@Override
public void checkUpdate(){
mVersionModel.checkUpdate(new VersionCallback(){
@Override
public void onUpdate(VersionBean versionBean){
//异步任务回来后,先判断View是否处于正确的状态
if(mView != null){
mView.showUpdateDialog(versionBean);
}
}
@Override
public void onNoUpdate(){
//异步任务回来后,先判断View是否处于正确的状态
if(mView != null){
mView.jumpToMain();
}
}
});
}
}
public class VersionModel{
private Context mContext;
public VersionModel(Context context){
mContext = context;
}
public void checkUpdate(VersionCallback callback){
//通过网络获取更新信息
VersionBean versionBean = fromServer();
if(BuildConfig.VERSION_CODE < versionBean.getVersionCode()){
callback.onUpdate(versionBean);
}else{
callback.onNoUpdate();
}
}
}
区区一个检测更新而已,这代码量会不会有点多了?


注意事项
基于todo-mvp的mvp写法,个人归纳的注意事项如下:
1.View只负责简单的视图更新、界面跳转的代码。
你可以理解为,View是很懒很懒的,不愿意动脑子,几乎所有的逻辑处理,哪怕只是简单的“1+1=?”的问题,都推给Presenter。我觉得,除了实现视图和逻辑的分离,还有一部分原因是:如果是Presenter太臃肿,你完全可以根据功能不同,轻易拆分成两个甚至更多的Presenter。但是如果Activity/Fragment(View)太臃肿的话,可能就不好拆分了。
所以,View只负责初始化自身,根据需要,给Presenter指派任务,然后进入“瞌睡”状态。只有两种情况才会被重新唤醒:
1)当接受来自外界的刺激时,比如:点击事件;
2)当Presenter有处理返回:嗨,孙子,醒醒,起来倒茶了(更新UI)。

2.View和Presenter的交互,是用接口来实现的。
下面是接口写法的示例。
public interface XxxContract {
interface View{
}
interface Presenter{
}
}
Activity/Fragment实现XxxContract.View接口,Presenter实现XxxContract .Presenter接口。两者在使用对方的实例时,通常都会向上转型为对应的接口,也就是说,暴露给对方的方法,都会写在接口里面。
讨论:这个Contract接口到底有没有存在的必要?
其实,纵观谷歌的demo,View和Presenter之间的耦合度通常都是很高的,Presenter是为对应的View量身定制的,复用的可能性不大,通常也没有这个必要。
而这个接口,如其名,更像是一个契约接口。里面两个关键的子接口,在这种情况下,完全失去了接口存在的最大意义:"多继承"。
“确定接口是理想选择,因而应该总是选择接口而不是具体的类。”这其实是一种引诱。当然,对于创建类,几乎在任何时刻,都可以替代为创建一个接口和一个工厂。许多人都掉进了这种陷阱,只要有可能就去创建接口和工厂。这种逻辑看起来好像是因为需要使用不同的具体实现,因此总是应该添加这种抽象性。这实际上已经变成了一种草率的设计优化。任何抽象性都应该是应真正的需求而产生的。如果没有足够的说服力,只是为了以防万一添加新接口,并由此带来了额外的复杂性。那么应该质疑一下这样的设计了。恰当的原则,应该是优先选择类而不是接口。接口是一种重要工具,但它们容易被滥用。” ——摘自《Java编程思想》第9章 接口 第188-189页
尤其是MVP用得多了,这个契约接口,总让人感到别扭。比如,想在Presenter添加一个方法,除了在Presenter里写它一次,还得在契约接口里面声明一次。它给我带来的麻烦,似乎多于给我的便利。
这个Contract接口有没有存在的必要,真的有待商榷。
所以,个人更倾向于:去除Contract接口、Presenter接口,只保留View接口。Activity/Fragment则直接使用Presenter的实例时,不再将其向上转型为接口。在调用Presenter的attachView方法的时候,将Activigty/Fragment的实例向上转型为接口,避免Presenter滥用该实例引用。
以上仅是个人观点,也许鄙人目光短浅,有所谬误。欢迎指正。
3.Presenter异步任务回来后,通知View更新UI之前,要先判断Activity是否为null,或者Fragment是否已经从activity中移除。
因为Presenter的生命周期,通常与Activity/Fragment是不相同的。所以Presenter在执行异步操作后,在结束的时候,都要判断View是否还处于正确的状态。这样,就能有效得避免了在异步任务完成时,Activity/Fragment却已经被销毁而导致的空指针等问题。当然,同步任务通常不需要这种判断。
有趣的是,todo-mvp、todo-mvp-dagger、todo-mvp-rxjava这三个分支对上述操作的做法都不同,但有异曲同工之妙。下面给出它们做法的关键代码。
todo-mvp
public class TasksFragment extends Fragment implements TasksContract.View {
/**
* Return true if the fragment is currently added to its activity.
*/
@Override
public boolean isActive() {
return isAdded();
}
}
public class TasksPresenter implements TasksContract.Presenter {
@Override
public void loadTasks(boolean forceUpdate) {
//异步任务
mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() {
@Override
public void onTasksLoaded(List<Task> tasks) {
//异步任务结束后,再调View之前,必须先判断
//不过这种做法,其实只适合实现View接口的是Fragment的时候
if (!mTasksView.isActive()) {
return;
}
mTasksView.setLoadingIndicator(false);
}
});
}
}
todo-mvp-dagger
final class TasksPresenter implements TasksContract.Presenter {
@Override
public void loadTasks(boolean forceUpdate) {
//异步任务
mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() {
@Override
public void onTasksLoaded(List<Task> tasks) {
//异步任务结束后,再调View之前,必须先判断
if (mTasksView == null) {
return;
}
mTasksView.setLoadingIndicator(false);
}
});
}
//在View解除绑定时,会被调用(需要你自己在onDestroy/onDestroyView里面手动调用)
@Override
public void dropView() {
mTasksView = null;
}
}
todo-mvp-rxjava
public class TasksPresenter implements TasksContract.Presenter {
@Override
public void loadTasks(boolean forceUpdate) {
//异步任务
Disposable disposable = mTasksRepository
.getTasks()
.subscribe(tasks -> mTasksView.setLoadingIndicator(false));
//将能取消异步任务的disposable添加进CompositeDisposable
mCompositeDisposable.add(disposable);
}
//在View解除绑定时,会被调用(需要你自己在onDestroy/onDestroyView里面手动调用)
// 取消mCompositeDisposable里面添加的所有RxJava的异步任务。
@Override
public void unsubscribe() {
mCompositeDisposable.clear();
}
}
4.Model,作为数据源,要确保它的可复用性。
就像我上面讲MVC时举的例子,Model不应该持有Presenter的引用,它不应该知道会是哪个Presenter来调用它。它的数据处理结果,都应该通过回调接口来交给调用它的Presenter。如果,你觉得写这些回调接口很烦,那么你可以考虑学习一波RxJava了。
反面示例:
//不应该将Presenter的引用传给Model
public MainModel(MainContract.Presenter mainPresenter) {
this.mainPresenter= mainPresenter;
}
这是个很典型的反面例子...我也曾经这么写过Model的代码,直到有一天,我需要在其他地方复用某个Model的时候...笑哭.jpg。
5.Context。如果你要在Presenter和Model里面使用Context,那么你应该使用Application的Context,而不是Activity的Context。
原因如下:
1)避免Context在子线程使用时,由于Activity突然被销毁,导致的空指针。
2)避免自己偷懒,在Presenter和Model执行更新UI的操作。实际上,这也是很不应该的操作。我在很多项目里面,经常看到这样的错误操作:在Presenter里更新UI、把Adapter的代码放进Presenter里面等等。MVP的划分,不仅仅是为了代码的解耦、复用,也有很大一部分原因是因为Android类不能直接在JVM上运行,会影响我们写单元测试。而尽量隔离、减少使用Android类的Presenter、Model层,会更便于我们写相关单元测试。
3)Application的Context足够应付Presenter和Model里面的需求了。看下表,Context的应用场景:

*数字1:启动Activity在这些类中是可以的,但是需要创建一个新的task。一般情况不推荐。
*数字2:在这些类中去layout inflate是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。
*数字3:在receiver为null时允许,在4.2或以上的版本中,用于获取黏性广播的当前值。(可以无视) ContentProvider、BroadcastReceiver之所以在上述表格中,是因为在其内部方法中都有一个context用于使用。
你可以看见,Activity的Context能做的,Application的Context基本都能做。如果你非要在Presenter、Model里面使用Activity的Context,我想你更应该考虑是不是该把这段代码放到Activity/Fragment里面。
6.Presenter、Model的构造方法,要使用依赖注入。
所谓的依赖注入,就是A类里面需要使用到B类,而B类的实例,在一开始,就先在外面创建好,然后通过A类的构造方法传递进来的。先前的MVP示例里的这段代码,就完成了一个简单的依赖注入。
VersionModel versionModel = new VersionModel(getApplicationContext());
mPresenter = new SplashPresenter(versionModel);
现在流行的Dagger2这个依赖注入框架只是能极大地简化了你自己去手动new这些实例,甚至创建这些实例的单例,再注入的过程。
全局的Context,如果要在Presenter、Model里面使用,也要通过构造方法,将Context传递进来。很大的一部分原因也是:方便写单元测试代码。
因为单元测试里面有一个mock的概念。依赖注入,能方便你mock相应的类。如果对单元测试有兴趣的话,到时可以参考我的相关测试文章。
代码示例: todo-mvp里的某个Presenter构造方法的部分代码
public TasksPresenter(@NonNull TasksRepository tasksRepository) {
/**
* 相当于mTasksRepository = tasksRepository,并检测tasksRepository是否为null。
* 如果为空,抛空指针异常。
*/
mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
}
注:checkNotNull是谷歌的guava里面的方法。虽然谷歌demo的各个分支,都有用到guava库,一个工具类库。但实际项目中并不推荐适用它,因为android里64k方法的限制。这个库它有1w+方法...如果你喜欢用相关方法,建议把相关类复制出来即可。
7.关于Presenter、Model的划分
前面提到,Model是“数据处理”,Presenter是“根据业务逻辑,对Model获得的结果进一步处理”。很多人也许会对Presenter、Model的划分产生疑问:同样都会进行数据处理,怎么划分才更恰当?
其实MVP三层的关系,从另一个角度看的话,是层层递进的关系,如下图。

而前面我们已经知道Model的复用性是很重要。假设,如果有多个Presenter都对Model中获得的数据,进行同样的处理,那么我们就应该考虑,把这一段处理代码,“下沉”到Model层里。如果Model里的一个方法,有一段代码只针对某个Presenter做了特殊处理,那么我们就应该考虑,把这段处理代码,“上浮”到那个特定的Presenter里。
总结
相比MVC的各种尴尬,MVP之间的分工合作明显更加合理,便于维护、测试。个中好处,只能你理解和熟练使用MVP后,才能深有感触。
建议认真翻阅谷歌的官方架构demo:android-architecture
我个人也提供了一个用MVP写的讯飞语音交互demo:TalkDemo
如果你对dagger、rxjava熟悉,也可以参阅我的另一个基于mvp+dagger2(dagger.android)+rxjava2+retrofit2的架构demo:ArchitectureDemo
以上内容,均基于个人的理解和谷歌的架构demo总结的。
如有谬误,请及时提醒,不胜感激!