Android MVP 模式解析与基本实现方式

文章已同步至 CSDN:http://blog.csdn.net/qq_24867873/article/details/79459856

前言

记得自己接手的第二个项目采用的是 MVP 模式进行开发的,当时架构已经设计好,我看了几篇关于 MVP 的文章,对其有了基本的了解之后,便照猫画虎进行了开发,之后便再也没接触过 MVP。

最近空闲的时候读了一篇 MVP 相关的文章,受益匪浅。于是打算写一篇关于它的文章,一方面是作为自己的学习笔记方便查看,另一反面希望能给没有接触过 MVP 模式的新人提供帮助,以便可以快速入门。

什么是 MVC

在讲 MVP 之前,我们先来了解一下 MVC。

MVC 结构图

MVC 模式是经典的三层架构一种具体的实现方式,全称为 Model(模型层) 、View(视图层)、Controller(控制器)。下面介绍一下它们各自的职责:

  • Model 层:用来定义实体对象,处理业务逻辑,可以简单地理解成 Java 中的实体类。
  • View 层:负责处理界面的显示,在 Android 中对应的就是 xml 文件。
  • Controller 层:对应的是 Activity/Fragment ,当加载完成 xml 布局之后,我们需要找到并设置布局中的各个 View,处理用户的交互事件,更新 View 等。

下面我们通过一个简单的例子来说明这三者是如何交互的。

首先是 View 层,布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical"
              android:padding="16dp">

    <EditText
        android:id="@+id/et_height"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="身高cm"/>

    <EditText
        android:id="@+id/et_weight"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="体重kg"/>

    <Button
        android:id="@+id/btn_cal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"/>

</LinearLayout>

然后是 Controller 层:

public class MVCActivity extends AppCompatActivity implements View.OnClickListener {

    private EditText mEtHeight;
    private EditText mEtWeight;
    private Button mBtnCal;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Controller 访问了 View 的组件
        mEtHeight = findViewById(R.id.et_height);
        mEtWeight = findViewById(R.id.et_weight);
        mBtnCal = findViewById(R.id.btn_cal);
        // 这个点击事件属于 View,它是 View 的监听器
        mBtnCal.setOnClickListener(this);

        // Controller 调用了 Model
        String btnText = User.instance().getBtnText();
        // 然后 Controller 更新了 View 的属性
        mBtnCal.setText(btnText);
    }

    @Override
    public void onClick(View v) {
        int height = Integer.parseInt(mEtHeight.getText().toString());
        float weight = Float.parseFloat(mEtWeight.getText().toString());
        // Controller 更新了 Model 中的数据
        User.instance().setHeight(height);
        User.instance().setWeight(weight);
        // 这里 View 又访问了 Model 的数据,并呈现在 UI 上
        String valueBMI = String.valueOf(User.instance().getBMI());
        Toast.makeText(this, "BMI: " + valueBMI, Toast.LENGTH_LONG).show();
    }
}

最后是 Model 层:

public class User {

    private int height;
    private float weight;

    private static User mUser;

    public static User instance(){
        if (mUser == null) {
            synchronized (User.class) {
                if (mUser == null) {
                    mUser = new User();
                }
            }
        }
        return mUser;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public float getWeight() {
        return weight;
    }

    public void setWeight(float weight) {
        this.weight = weight;
    }

    public String getBtnText() {
        // 在这里,我们可以从数据库中查询数据
        // 或者访问网络获取数据
        return "计算BMI";
    }

    public float getBMI() {
        // 通过已有的属性计算出新的属性,也属于业务逻辑的操作
        return weight / (height * height) * 10000;
    }
}

从上面的代码中,我们可以看到 View 层的职责是非常简单的,向用户呈现 xml 文件中的布局,并且响应用户的触摸事件。

而 Controller 层的职责逻辑则复杂很多。它对于 View 层,需要将从 Model 中获取到的数据及时地呈现在 UI 上。而对于 Model 层,当 app 的生命周期发生变化或者接收到某些响应时,需要对 Model 的数据进行 CRUD。在这个例子中,用户点击按钮的时候,首先获取 View 层用户的输入,然后更新 Model 层的属性,最后获取到 Model 层计算得出的新数据并显示在 UI 上。

对于 Model 来说,它不仅仅是个简单的实体类,还应该包括数据处理与业务逻辑的操作,比如说对数据库的操作、网络请求等,但是很多情况下,我们很少把这些操作写在实体类中。

demo 运行效果如下:

运行效果

在 MVC 模式中,Controller 层扮演着重要的角色,它不仅要处理 UI 的显示与事件的响应,还要负责与 Model 层的通信,同时 Model 层与 View 层也会通信,三者的耦合度很大。

作为 Android 开发中默认使用的架构模式,MVC 易于上手,适合快速开发一些小型项目。但是随着业务逻辑的复杂度越来越大,Activity/Fragment 会越来越臃肿,因为它同时承担着 Controller 与 View 的角色,这对于项目后期的更新维护与测试交接都是非常不方便的,大大提高了生产成本。这么一来,它就违背了 “提高生产力” 的初衷,于是 MVP 模式就应运而生了。

什么是 MVP

MVP 结构图

MVP 是 MVC 的一种升级进化,全称为 Model(模型层)、View(视图层)、Presenter(主持者)。从结构图中,我们可以看到它与 MVC 的区别:Presenter 代替了 Controller,去除了 View 与 Model 的关联与耦合。

  • Model 层:和 MVC 模式中的 Model 层是一样的,这里不再说了。
  • View 层:视图层。在 MVP 中,它不仅仅对应 xml 布局了,Activity/Fragment 也属于视图层。View 层现在不仅作为 UI 的显示,还负责响应生命周期的变化。
  • Presenter 层:主持者层,是 Model 层与 View 层进行沟通的桥梁,处理业务逻辑。它响应 View 层的请求从 Model 层中获取数据,然后将数据返回给 View 层。

在 MVP 的架构中,最大的特点就是 View 与 Model 之间的解耦,两者之间必须通过 Presenter 来进行通信,使得视图和数据之间的关系变得完全分离。但是 View 和 Presenter 两者之间的通信并不是想怎么调用就可以怎么调用的,下面讲一下 MVP 模式最基本的实现方式。

MVP 基本的实现方式

  • 创建 IPresenter 接口(接口或类名自己定义,一般有约定成俗的写法),把所有业务逻辑的接口都放在这里,并创建它的实现类 PresenterImpl。
  • 创建 IView 接口,把所有视图逻辑的接口都放在这里,其实现类是Activity/Fragment。
  • 在 Activity/Fragment 中包含了一个 IPresenter 的实例,而 PresenterImpl 里又包含了一个 IView 的实例并且依赖了 Model。Activity/Fragment 只保留对 IPresenter 的调用,当 View 层发生某些请求响应或者生命周期发生变化,则会迅速的向 Presenter 层发起请求,让 Presenter 做出相应的处理。
  • Model 并不是必须有的,但是一定会有 View 和 Presenter。

我们还是以上面的功能为例,用 MVP 模式具体实现它。

IPresenter 接口:

public interface IPresenter {

    /**
     * 调用该方法表示 Presenter 被激活了
     */
    void start();

    void onBtnClick(int height, float weight);

    /**
     * 调用该方法表示 Presenter 要结束了
     * 为了避免相互持有引用而导致的内存泄露
     */
    void destroy();

}

IView 接口:

public interface IView {

    /**
     * 用来更改按钮的文本
     *
     * @param text
     */
    void updateBtnText(String text);

    /**
     * 用来弹出吐司显示 BMI
     *
     * @param bmi
     */
    void showToast(float bmi);

}

IPresenter 接口的实现类 PresenterImpl:

public class PresenterImpl implements IPresenter {

    private IView mView;

    public PresenterImpl(IView mView) {
        this.mView = mView;
    }

    @Override
    public void start() {
        String text = User.instance().getBtnText();
        mView.updateBtnText(text);
    }

    @Override
    public void onBtnClick(int height, float weight) {
        User.instance().setHeight(height);
        User.instance().setWeight(weight);
        float bmi = User.instance().getBMI();
        mView.showToast(bmi);
    }

    @Override
    public void destroy() {
        mView = null;
    }
}

IView 接口的实现类 MVPActivity:

public class MVPActivity extends AppCompatActivity implements IView, View.OnClickListener {

    private EditText mEtHeight;
    private EditText mEtWeight;
    private Button mBtnCal;

    private IPresenter mPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 实例化 PresenterImpl
        mPresenter = new PresenterImpl(this);
        // View 的相关初始化
        mEtHeight = findViewById(R.id.et_height);
        mEtWeight = findViewById(R.id.et_weight);
        mBtnCal = findViewById(R.id.btn_cal);
        mBtnCal.setOnClickListener(this);
    }

    @Override
    protected void onStart() {
        super.onStart();
        mPresenter.start();
    }

    @Override
    public void onClick(View v) {
        int height = Integer.parseInt(mEtHeight.getText().toString());
        float weight = Float.parseFloat(mEtWeight.getText().toString());
        mPresenter.onBtnClick(height, weight);
    }

    @Override
    public void updateBtnText(String text) {
        mBtnCal.setText(text);
    }

    @Override
    public void showToast(float bmi) {
        Toast.makeText(this, "BMI: " + bmi, Toast.LENGTH_LONG).show();
    }

    @Override
    protected void onDestroy() {
        if (mPresenter != null) {
            mPresenter.destroy();
            mPresenter = null;
        }
        super.onDestroy();
    }
}

Model 层的代码与 MVC 例子中的相同,这里就不再帖代码了。

看完代码可能有的人会发现,相对于 MVC 模式来说,代码不仅没有减少,反而还增加了许多接口,看起来有些晕。但我们仔细观察可以发现,虽然增加了许多接口,但是 MVP 的结构是非常清晰的,也是有很大的好处的,下面我们仔细分析一下。

MVPActivity 实现了IView 接口,并实现了 updateBtnText(..)showToast(..) 这两个方法,但是这两个方法看起来好像都没有被调用,只是在 onCreate() 的时候创建了一个 PresenterImpl 对象,在 onStart() 的时候调用了 mPresenter.start() 方法,然后在 onDestroy() 的时候调用了 mPresenter.destroy() 方法,而当按钮的点击事件响应的时候又调用了 mPresenter.onBtnClick(..) 方法,那么既没有回调也没有直接调用,那 IView 中的两个接口方法又是何时何地被调用的呢?接下来我们将继续分析 Presenter 层的实现代码。

在 PresenterImpl 中实现了 IPresenter 接口并实现了 start() onBtnClick(..) destroy() 方法,在构造方法中有一个IView的参数,这个对象是 IView 的引用,这个对象可以是 Activity 或者是 Fragment 也可以是 IView 接口的任何一个实现类,但对于 PresenterImpl 而言具体的 IView 到底是谁并不知道。在 PresenterImpl 中,在 start()onBtnClick() 方法中除了调用 Model 外都调用了 IView 的方法:mView.updateBtnText(..)
mView.showToast(..),以此来对 View 层的 UI 呈现以及交互提醒做出相应的响应。而最后的 destroy() 方法则是用于释放对 IView 的引用。

由此我们可以得出几个结论:

对于 View 而言:

  • 我需要一位主持者,当出现视图相关事件的响应或者生命周期的变化时,我需要告诉这位主持者,我想要做些什么。
  • 我会提供一系列通用接口,以便于当主持者完成我的请求后,调用相应的接口告诉我这件事的结果。
  • 我所有的请求都发给主持者,让他帮我做决定,但是这件事是怎么做的,我并不知道也不关心,我只是需要结果。

对于 Presenter 而言:

  • 我接收到 View 的请求后找 Model 寻求帮助,等 Model 做完事情后通知我了,我在把结果告诉 View。
  • 我只知道指挥 Model做事、告诉 View 显示数据,但我不干活。
  • 我相当于一座桥,连接着 View 和 Model,他们谁也不认识谁,想要通信必须要通过我,如果没有我,他们两永远都不会认识。没错,我就是这么重要。

由于有 Presenter 的存在,View 层的代码看起来是非常清晰的,每一个方法都有它自己的功能职责,彼此之间并不会相互耦合。而 Presenter 中的代码也是如此,每一个方法都只处理一件事,并不会做其他无相关的事情。另外我们观察到,在 MVPActivity 中并没有直接对 PresenterImpl 进行持有,而是持有了一个 IPresenter 对象;同样的在 PresenterImpl 也并没有直接持有 MVPActivity 而是持有了一个 IView 对象。也就是说,凡是实现了 IPresenter 便是 Presenter 层,凡是实现了 IView 便是 View 层,这样就能很方便地变更业务逻辑或者进行单元测试。下面就讲一讲 MVP 的优势与不足。

MVP 的优势与不足

优势:

  • 解耦,抽这么多接口出来就是为了解耦,非常适合多人协同开发。
  • 各模块分工明确,结构清晰。在 MVC 模式中,Activity/Fragment 兼顾着 Controller 与 View 的作用,杂乱且难以维护,而 MVP 模式大大减少了 Activity/Fragment 的代码,容易看懂、容易维护和修改。
  • 方便地变更业务逻辑。比如有三个功能,它们的 View 层完全一致,只是各自的业务逻辑不同,那么我们可以分别创建三个不同的 PresenterImpl (当然他们都要实现 IPresenter 接口),然后在 Activity 中创建 IPresenter 对象的时候,就可以根据不同的外部条件创建出不同的 PresenterImpl,这样就能方便的实现它们各自的业务。
  • 方便进行单元测试。由于业务逻辑都是在 IPresenter 中实现的,那么我们可以创建一个 PresenterTest 实现 IPresenter 接口,然后把 MVPActivity 中对 PresenterImpl 的创建改成 PresenterTest 的创建,然后就可以对 IView 的方法随意进行测试了。如果想要测试 IPresenter 中的方法,那就新建一个 ViewTest 类实现 IView 接口,然后将其传入 PresenterImpl,便可以自由的测试 IPresenter 中的方法是否有效。
  • 避免 Activity 内存泄露。Activity 是有生命周期的,用户随时可能切换 Activity,当 APP 的内存不够用的时候,系统会回收处于后台的 Activity 的资源以避免 OOM。采用传统的模式,一大堆异步任务都有可能保留着对 Activity 的引用,比如说许多图片加载框架。这样一来,即使 Activity 的 onDestroy() 已经执行,这些 异步任务仍然保留着对 Activity 实例的引用, 所以系统就无法回收这个 Activity 实例了,结果就是 Activity Leak。Android 的组件中,Activity 对象往往是在堆里占最多内存的,所以系统会优先回收 Activity 对象, 如果有 Activity Leak,APP很容易因为内存不够而 OOM。采用 MVP 模式,只要在当前的 Activity 的 onDestroy() 里,分离异步任务对 Activity 的引用,就能避免 Activity Leak。

不足:

  • 有点笨重,不适合短期小型的项目开发。你一个 Activity 就能搞定的事,非要用 MVP 干嘛。
  • 虽然 Activity 变得轻松了,但是 Presenter 的业务越来越复杂。
  • 提高了学习成本,由于 MVP 的变种非常多,需要自己在实战中慢慢摸索。

补充

  1. 关于 MVP 的分包结构,有的人习惯按照下面这种方式分包:

将所有的 Model/View/Presenter 的代码分别放在同一个包下,这样业务多了会很乱。也有人喜欢按照模块分包,将同一个功能模块的 Model/View/Presenter 放在一个模块包下。具体的分包方式还是要按照具体的项目和自己的喜好来定。

  1. 在使用上述 MVP 模式进行开发的过程中,还遇到了空指针的问题。当 Presenter 中通过异步方式获取数据然后需要更新 View 的时候,这个时候 View 有可能已经消失了,极度容易引起 NullPointerException。比如下面的示例代码:
@Override
public void login(String phone, String pwd) {
    OkGo.<BaseModal<User>>get(url).tag(this)
            .params(AppInterface.getLoginParams(phone, pwd))
            .execute(new JsonCallback<BaseModal<User>>() {
                @Override
                public void onSuccess(Response<BaseModal<User>> response) {
                    if (mView == null) {
                        return;
                    }
                    mView.showToast("登录成功");
                }

                @Override
                public void onError(Response<BaseModal<User>> response) {
                    if (mView == null) {
                        return;
                    }
                    mView.showToast("登录失败");
                }
            });
}

由上面的代码可以看出,在 Presenter 进行异步回调后,一定要对 mView 进行非空判断,否则会出现大面积的 NullPointerException。

总结

以上就是 MVP 模式基本的实现方式,可能示例代码太简单无法体现 MVP 的优势,但是真正地理解了它并在项目中实际使用,你便能体会到它所带来的好处。MVP 有很多变种与改进,网上也有很多资料,如果想学的话,可以很方便地找到。另外,Google 官方也开源了一系列 Andorid 架构的使用示例,其中就包括了 MVP 模式,地址:https://github.com/googlesamples/android-architecture

本篇博客示例代码:https://github.com/ayuhani/mvp_demo

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

推荐阅读更多精彩内容