浅谈MVP

496 阅读15分钟

MVP是从MVC衍生出来的,所以,先说一下MVC,再到MVP。

MVC

MVC分为三个部分:

视图(View):用户界面。

控制器(Controller):业务逻辑,用于控制应用程序的流程。

模型(Model):数据处理。即网络请求、数据库等一些对数据的操作处理。

它们之间的交互通常如下图:

MVC

那么在我们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:数据处理。

如下图:

MVP

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的应用场景:

Context的应用场景
有一些NO上添加了一些数字。

*数字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总结的。

如有谬误,请及时提醒,不胜感激!