浅谈MVP

MVP是从MVC衍生出来的,因此,先说一下MVC,再到MVP。java

MVC

MVC分为三个部分:android

视图(View):用户界面。git

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

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

它们之间的交互一般以下图:编程

MVC

那么在咱们android里,如何划分这几层?在这里,参考部分博文,以及根据我我的的理解,列出下面两种可能的状况:bash

(一)View:xml。Controller:Activity/Fragment。Model:数据处理。网络

若是是这样划分的话。实际运用中,每每会出现Activity/Fragment虽然划分为Controller层,但看起来又像View层。架构

为何?由于若是只用xml做为View层,对界面的控制能力实在太弱了,没法动态更新UI。因此,Activity/Fragment也要担当起一部分View层的责任,负责动态更新视图。这就致使Activity/Fragment既有更新View控件的代码,又有对源自Model的数据进行进一步逻辑处理的代码,随着迭代开发,Activity/Fragment会显得愈来愈臃肿,分分钟几千行代码,给后期维护带来极大的困扰。若是你接手这样的代码,内心恐怕会不停赞叹前人真牛逼!mvc

不少博客,描述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总结的。

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

相关文章
相关标签/搜索