Android MVP 十分钟入门!

前言##

在日常开发APP 的过程中,随着业务的扩展,规模的变化。我们的代码规模也会逐渐变得庞大,每一个类里的代码也会逐渐增多。尤其是Activity和Fragment ,由于Context 的存在,基本上所有对视图的操作我们只能在Activity和Fragment中完成;即便是对某些逻辑进行封装,Activity和Fragment 依旧会显得过于臃肿。因此,我们需要换一种思路去写代码,这个时候MVP模式就应用而生了!那么MVP 怎么用呢,下面就来说一说。

假设你现在如要实现下图中的功能:

这个需求很简单,就是点击按钮,下载一张图片,显示下载进度;下载完成后,在ImageView中显示这张图片。
下面我们就分别用传统的方式(也就是所谓的MVC)和MVP 模式分别取实现这个功能。然后分析一下MVP 到底好在哪里。

MVC##

public class MVCActivity extends AppCompatActivity {

    private Context mContext;
    private ImageView mImageView;
    private MyHandler mMyHandler;
    private ProgressDialog progressDialog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvc);
        mContext = this;
        init();
    }

    private void init() {
        //view init
        mImageView = (ImageView) findViewById(R.id.image);
        mMyHandler = new MyHandler();

        progressDialog = new ProgressDialog(mContext);
        progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, "Cancle", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                progressDialog.dismiss();
            }
        });
        progressDialog.setCanceledOnTouchOutside(false);
        progressDialog.setTitle("下载文件");
        progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);


        //click-event
        findViewById(R.id.downloadBtn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                progressDialog.show();
                HttpUtil.HttpGet(Constants.DOWNLOAD_URL, new DownloadCallback(mMyHandler));
            }
        });

        findViewById(R.id.downloadBtn1).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                progressDialog.show();
                HttpUtil.HttpGet(Constants.DOWNLOAD_ERROR_URL, new DownloadCallback(mMyHandler));
            }
        });

    }


    class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case 300:
                    int percent = msg.arg1;
                    if (percent < 100) {
                        progressDialog.setProgress(percent);
                    } else {
                        progressDialog.dismiss();
                        Glide.with(mContext).load(Constants.LOCAL_FILE_PATH).into(mImageView);
                    }
                    break;
                case 404:
                    progressDialog.dismiss();
                    Toast.makeText(mContext, "Download fail !", Toast.LENGTH_SHORT).show();
                    break;
                default:
                    break;
            }
        }
    }
}

用mvc的方式,一个Activity就能搞定。代码逻辑很简单,点击按钮后显示之前初始化好ProgressDialog,然后开始下载任务(这里HttpUtil 内部简单封装了OKHttp 的异步GET请求,实现下载文件保存到本地的功能,实现细节在此不做深入探讨,有兴趣的同学可以查看源码),然后将请求结果通过Handler返回,在handleMessage中根据返回数据的信息做出不同的UI 处理;下载成功时在ImageView中显示图片,下载失败时Toast提示。

可以发现,在这种情况之前,Activity的任务十分繁重,既要负责下载任务的具体实施,还要根据下载进行再次的逻辑判断,才能去更新UI。这里只是一个简单的任务,你可能觉得无所谓,但是实际开发中,一个Activity中有许多的交互事件,这个时候Activity的代码就显得特别的庞大;一旦需求变更或出现bug,那简直就是噩梦一场。

因此,我们希望Activity可以变成下面这样

  • 他负责发起处理和用户交互的内容,但又不负责具体的实现;
  • 需要显示什么,不显示什么,什么东西显示多少,有个东西可以直接告诉他,
  • Activity不再做复杂的逻辑处理;

具体到上面的demo里就是,Activity负责发起下载任务,但是不负责具体实现;什么时候显示ProgressDialog,显示多少?什么时候提示错误信息,这一切都希望有个东西能直接告诉Activity,而不再是在Activity里再做判断。怎样才能做到呢?那就得靠MVP 了。

MVP

MVP 模式所做的事情很简单,就是将业务逻辑和视图逻辑抽象到接口中。

怎么理解呢,我们就根据此次要实现的下载功能,用代码说话。

定义Model,View,Presenter 接口###

Model Interface####

Model 接口定义所有需要实现的业务逻辑,在我们的下载任务中,业务逻辑只有一个,就是下载;因此Model 接口可以这么定义 :

public interface IDownloadModel {
    /**
     * 下载操作
     * @param url
     */
    void download(String url);
}

View Interface####

View 接口定义所有需要实现的视图逻辑,在我们的下载任务中,视图逻辑包括

  • 显示ProgressDialog;
  • 显示Dialog具体进度;
  • 显示具体的View(设置图片);
  • 显示错误信息(Toast提示)

因此View接口可以这么定义:

public interface IDownloadView {
    /**
     * 显示进度条
     * @param show
     */
    void showProgressBar(boolean show);

    /**
     * 设置进度条进度
     * @param progress
     */
    void setProcessProgress(int progress);

    /**
     * 根据数据设置view
     * @param result
     */
    void setView(String result);

    /**
     * 设置请求失败时的view
     */
    void showFailToast();
}

Presenter Interface####

Presenter 接口作为连接Model和View的中间桥梁,需要将二者连接起来,因此他需要完成以下工作:

  • 执行下载任务
  • 下载成功返回下载结果
  • 下载过程返回下载进度
  • 下载失败回调

因此,Presenter 就可以这么定义:

public interface IDowndownPresenter {
    /**
     * 下载
     * @param url
     */
    void download(String url);

    /**
     * 下载成功
     * @param result
     */
    void downloadSuccess(String result);

    /**
     * 当前下载进度
     * @param progress
     */
    void downloadProgress(int progress);

    /**
     * 下载失败
     */
    void downloadFail();
}

接口Model,View,Presenter 具体实现###

上面实现了,各个接口的定义,下面来看看他们具体的实现:

Model 具体实现####

public class DownloadModel implements IDownloadModel {
    private IDowndownPresenter mIDowndownPresenter;
    private MyHandler mMyHandler = new MyHandler();

    public DownloadModel(IDowndownPresenter IDowndownPresenter) {
        mIDowndownPresenter = IDowndownPresenter;
    }

    @Override
    public void download(String url) {
        HttpUtil.HttpGet(url, new DownloadCallback(mMyHandler));
    }

    class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case 300:
                    int percent = msg.arg1;
                    if (percent < 100) {
                        mIDowndownPresenter.downloadProgress(percent);
                    } else {
                        mIDowndownPresenter.downloadSuccess(Constants.LOCAL_FILE_PATH);
                    }
                    break;
                case 404:
                    mIDowndownPresenter.downloadFail();
                    break;
                default:
                    break;
            }
        }
    }
}

在MVP模式中,Model的工作就是完成具体的业务操作,网络请求,持久化数据增删改查等任务。同时Model中又不会包含任何View。
这里Model的具体实现很简单,将Http任务的结果返回到Handler当中,而在Handler中的实现又是由Presenter完成。那么Presenter接口又是怎样实现的呢?赶紧来看看

Presenter 具体实现####

public class DownloadPresenter implements IDowndownPresenter {
    private IDownloadView mIDownloadView;
    private IDownloadModel mIDownloadModel;


    public DownloadPresenter(IDownloadView IDownloadView) {
        mIDownloadView = IDownloadView;
        mIDownloadModel = new DownloadModel(this);
    }

    @Override
    public void download(String url) {
        mIDownloadView.showProgressBar(true);
        mIDownloadModel.download(url);
    }

    @Override
    public void downloadSuccess(String result) {
        mIDownloadView.showProgressBar(false);
        mIDownloadView.setView(result);
    }

    @Override
    public void downloadProgress(int progress) {
        mIDownloadView.setProcessProgress(progress);
    }

    @Override
    public void downloadFail() {
        mIDownloadView.showProgressBar(false);
        mIDownloadView.showFailToast();
    }
}

可以看到,我们在DownloadPresenter的构造方法中,同时实例化了Model和View,这样Presenter中就同时包含了两者;这样;在Presenter具体实现中,业务相关的操作由Model去完成(例如download),视图相关的操作由View去完成(如setView等)。Presenter 作为桥梁的作用就这样体现出来了,巧妙的将View和Model的具体实现连接了起来。

View具体实现####

最后再看一下View接口的具体实现,也就是Activity的实现:

public class MVPActivity extends AppCompatActivity implements IDownloadView {
    private Context mContext;
    private ImageView mImageView;
    private ProgressDialog progressDialog;

    private DownloadPresenter mDownloadPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mContext = this;
        setContentView(R.layout.activity_mvp);
        init();
    }

    private void init() {
        mDownloadPresenter = new DownloadPresenter(this);
        //view init
        mImageView = (ImageView) findViewById(R.id.image);
        findViewById(R.id.downloadBtn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mDownloadPresenter.download(Constants.DOWNLOAD_URL);
            }
        });

        findViewById(R.id.downloadBtn1).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mDownloadPresenter.download(Constants.DOWNLOAD_ERROR_URL);
            }
        });

        progressDialog = new ProgressDialog(mContext);
        progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, "Cancle", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                progressDialog.dismiss();
            }
        });
        progressDialog.setCanceledOnTouchOutside(false);
        progressDialog.setTitle("下载文件");
        progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);

    }

    @Override
    public void showProgressBar(boolean show) {
        if (show) {
            progressDialog.show();
        } else {
            progressDialog.dismiss();
        }
    }

    @Override
    public void setProcessProgress(int progress) {
        progressDialog.setProgress(progress);
    }

    @Override
    public void setView(String result) {
        Glide.with(mContext).load(result).into(mImageView);
    }

    @Override
    public void showFailToast() {
        Toast.makeText(mContext, "Download fail !", Toast.LENGTH_SHORT).show();
    }
}

在点下按钮执行开始下载任务的时候,View(Activity)中没有具体的实现,只是调用了Presenter中的download方法,而Presenter中的download又会去调用Model的download方法,Model又会在根据具体逻辑(在这里就是Http请求)的状态去调用Presenter中的方法,例如我们在handleMessage方法中,调用mIDowndownPresenter.downloadProgress(percent)时,就会去调用Presenter的具体实现

    @Override
    public void downloadProgress(int progress) {
        mIDownloadView.setProcessProgress(progress);
    }

而他的内部实现又是操作具体的View,也就是我们在Activity中初始化Presenter中传递的this,也就是当前Activity(View),这样最终回到了Activity中的


    @Override
    public void setProcessProgress(int progress) {
        progressDialog.setProgress(progress);
    }

我们为progressDialog 设置进度。

至此,我们就通过MVP 的模式实现了我们之前所设想的Activity

  • Button的click方法负责发起下载任务,但又不负责具体实现,而是由Presenter转接给Model去实现
  • Activity 什么时候显示ProgressDialog,什么时候显示Toast直接由Presenter告诉他,他只做一个View想做的事情
  • Activity里没有任何逻辑处理,所有的逻辑判断都在Model中完成了。

这就是MVP !!!

MVC VS MVP##

通过上面的两种实现方案,相信每个人都已经理解了MVC和MVP的区别;下面就其各自的优缺点再做一下总结;当然,这里的优缺点只是相对而言

优点####

MVC
MVC
MVP
MVP

上面两张图分别是MVC和MVP架构图。相信许多和我一样尝试去学习和了解MVP架构的同学对这两图(或类似的图)并不陌生。

结构更加清晰

我们回过头再去看MVCActivity 的实现,暂且将我们对Http请求的封装归结为Model (M),那么剩下的就只有Activity了,而这个Activity即负责实现视图逻辑,又需要实现部分业务逻辑,也就是说他既是Controller(C) 又是 View(V)。V和C的划分完全不清晰;因此,传统的代码结构只能勉强称为MV 或者是MC,如果算上xml 的布局文件,才能牵强的称为MVC 结构。

而MVP 就不同了,Model,View,Presenter各司其职,互相搭配,实现了解耦,完全解放了Activity(或者是Fragment)。这就是MVP 的优势,代码结构更加清晰。可以这样说,同一个模块的实现,甚至允许几个人分工完成;假设有一个非常复杂的Activity,如果使用MVP 的模式开发;那么这个时候,定义好MVP的接口之后,就可以有人专门去做Model,另一个人专门去做View;再由一个人写Presenter的代码,当然这需要极强的代码规范和协作能力;但这在传统的MVC模式中根本是无法想象的,所有的东西都在一个类里,两个人一起改,有了冲突怎么玩/(ㄒoㄒ)/~~。

需求变更,不再是噩梦

假设现在有新的需求,产品经理认为下载失败后只有一个Toast提示太单调了(而且用户有可能错过了这Toast的显示,而误以为APP失去了响应),因此,现在希望在下载失败后弹出一个Dialog,可以重试下载任务。是想,如果代码使用传统的MVC 结构,恰巧这个代码不是你写的,或者说就是你写的,但是你已经忘记了具体的逻辑;那么为了实现这个需求你又得去重新捋一遍逻辑,到某个类的xxx行进行修改;但是如果使用MVP就不同了View接口已经定义好了showFailToast就是用来显示错误提示的;因此即便代码不是你写的,你都可以很快的找到,应该去哪里改;而省去很多时间。

更容易写单元测试

这个就不展开说了,总之写过单元测试的人应该都有这样的体会。

缺点####

MVP这么好,也不是没有缺点。

arch.png

如图中所示,使用MVP 架构之后,多出了许多类;这是必然的;每一个View(Activity或Fragment)都至少需要各自的Model、Presenter和View接口,在加上他们各自的实现,也就是说每一个页面都会有6个java文件(算上Fragment或Activity,因为他就是View的实现),这样一个稍有点规模的APP,类就会变得异常的多,而每一个类的加载又会消耗资源;因此,相较于MVC,这算是MVP最大的缺点了吧。

当然,对于这个问题我们可以通过泛型参数、抽象父类的方式,将一些公用的Model及Presenter抽象出来。这应该就是使用MVP架构的精髓了。

最后##

个人感觉,使用MVP 架构是利大于弊的;随着项目规模的增加,代码逻辑的清晰才是最重要的事情。况且Google官方也出推出了一系列关于MVP的使用demo。因此,这也是官方提倡大家使用的。凡事,有利必有弊;类数目的增长是无法避免的事情,因此如何使用泛型和抽象优化MVP 的结构就变成了我们用好MVP的关键了。

当然,我们不能为了MVP而去MVP,如果项目结构不是很庞大,业务不是很复杂;那么传统的MVC 架构足以,而且也方便!


年前的最后一个工作日了,我居然写了一篇学习笔记;今天一定是上了假的班儿!明天回家过年,O(∩_∩)O哈哈哈~!每一个人,新年快乐!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容