Android MVP架构搭建


前言

对于MVP (Model View Presenter)架构是从著名的MVC(Model View Controller)架构演变而来的。而对于Android应用的开发中本身可视为一种MVC架构。通常在开发中将XML文件视为MVC中的View角色,而将Activity则视为MVC中的Controller角色。不过更多情况下在实际应用开发中Activity不能够完全充当Controller,而是Controller和View的合体。于是Activity既要负责视图的显示,又要负责对业务逻辑的处理。这样在Activity中代码达到上千行,甚至几千行都不足为其,同时这样的Activity也显得臃肿不堪。所以对于MVC架构并不很合适运用于Android的开发中。


一、MVP架构简介

对于一个应用而言我们需要对它抽象出各个层面,而在MVP架构中它将UI界面和数据进行隔离,所以我们的应用也就分为三个层次。

  • View: 对于View层也是视图层,在View层中只负责对数据的展示,提供友好的界面与用户进行交互。在Android开发中通常将Activity或者Fragment作为View层。
  • Model: 对于Model层也是数据层。它区别于MVC架构中的Model,在这里不仅仅只是数据模型。在MVP架构中Model它负责对数据的存取操作,例如对数据库的读写,网络的数据的请求等。
  • Presenter:对于Presenter层他是连接View层与Model层的桥梁并对业务逻辑进行处理。在MVP架构中Model与View无法直接进行交互。所以在Presenter层它会从Model层获得所需要的数据,进行一些适当的处理后交由View层进行显示。这样通过Presenter将View与Model进行隔离,使得View和Model之间不存在耦合,同时也将业务逻辑从View中抽离。

可以通过下面的MVP结构图来看一下MVP中各个层次之间的关系。
在这里插入图片描述
在MVP架构中将这三层分别抽象到各自的接口当中。通过接口将层次之间进行隔离,而Presenter对View和Model的相互依赖也是依赖于各自的接口。这点符合了接口隔离原则,也正是面向接口编程。在Presenter层中包含了一个View接口,并且依赖于Model接口,从而将Model层与View层联系在一起。而对于View层会持有一个Presenter成员变量并且只保留对Presenter接口的调用,具体业务逻辑全部交由Presenter接口实现类中处理。

注意:三层之间调用顺序为view->presenter->model,为了调用安全着想不可反向调用!不可跨级调用!

二、乞丐版MVP架构模式的代码实现

接下来我开始用 MVP 模式构造一个简易模拟请求网络的小程序。


1.文件目录结构

在这里插入图片描述

2.编写代码

  • Callback接口
package com.chen.mvpdemo;

public interface MvpCallback {
    /**
     * 数据请求成功
     * @param data 请求到的数据
     */
    void onSuccess(String data);

    /**
     *  使用网络API接口请求方式时,虽然已经请求成功但是由
     *  于{@code msg}的原因无法正常返回数据。
     */
    void onFailure(String msg);

    /**
     * 请求数据失败,指在请求网络API接口请求方式时,出现无法联网、
     * 缺少权限,内存泄露等原因导致无法连接到请求数据源。
     */
    void onError();

    /**
     * 当请求数据结束时,无论请求结果是成功,失败或是抛出异常都会执行此方法给用户做处理,通常做网络
     * 请求时可以在此处隐藏“正在加载”的等待控件
     */
    void onComplete();

}
  • Model 类
package com.chen.mvpdemo;

import android.os.Handler;

public class MvpModel {
    public static void getNetData(final String param,final MvpCallback callback){
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                switch (param){
                    case "normal":
                        callback.onSuccess("根据参数"+param+"的请求网络数据成功");
                        break;
                    case "failure":
                        callback.onFailure("请求失败:参数有误");
                        break;
                    case "error":
                        callback.onError();
                        break;
                }
                callback.onComplete();
            }
        },2000);
    }
}

  • View 接口
package com.chen.mvpdemo;

public interface MvpView {
    /**
     * 显示正在加载进度框
     */
    void showLoading();
    /**
     * 隐藏正在加载进度框
     */
    void hideLoading();
    /**
     * 当数据请求成功后,调用此接口显示数据
     * @param data 数据源
     */
    void showData(String data);
    /**
     * 当数据请求失败后,调用此接口提示
     * @param msg 失败原因
     */
    void showFailureMessage(String msg);
    /**
     * 当数据请求异常,调用此接口提示
     */
    void showErrorMessage();
}

  • Presenter类
package com.chen.mvpdemo;

public class MvpPresenter {
    private MvpView mView;
    public MvpPresenter(MvpView view){
        this.mView=view;
    }

    public void getData(String params){
        mView.showLoading();
        MvpModel.getNetData(params, new MvpCallback() {
            @Override
            public void onSuccess(String data) {
                mView.showData(data);
            }

            @Override
            public void onFailure(String msg) {
                mView.showFailureMessage(msg);
            }

            @Override
            public void onError() {
                mView.showErrorMessage();
            }

            @Override
            public void onComplete() {
                mView.hideLoading();
            }
        });
    }
}

  • xml布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:text="点击按钮获取网络数据"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="获取数据【成功】"
        android:onClick="getData"
        />
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="获取数据【失败】"
        android:onClick="getDataForFailure"
        />
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="获取数据【异常】"
        android:onClick="getDataForError"
        />


</LinearLayout>
  • Activity
package com.chen.mvpdemo;

import androidx.appcompat.app.AppCompatActivity;

import android.app.ProgressDialog;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity implements MvpView {

    //进度条
    ProgressDialog progressDialog;
    TextView text;
    MvpPresenter presenter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        text = (TextView)findViewById(R.id.text);
        // 初始化进度条
        progressDialog = new ProgressDialog(this);
        progressDialog.setCancelable(false);
        progressDialog.setMessage("正在加载数据");
        //初始化Presenter
        presenter = new MvpPresenter(this);
    }
    // button 点击事件调用方法
    public void getData(View view){
        presenter.getData("normal");
    }
    // button 点击事件调用方法
    public void getDataForFailure(View view){
        presenter.getData("failure");
    }
    // button 点击事件调用方法
    public void getDataForError(View view){
        presenter.getData("error");
    }
    @Override
    public void showLoading() {
        if (!progressDialog.isShowing()) {
            progressDialog.show();
        }
    }
    @Override
    public void hideLoading() {
        if (progressDialog.isShowing()) {
            progressDialog.dismiss();
        }
    }
    @Override
    public void showData(String data) {
        text.setText(data);
    }
    @Override
    public void showFailureMessage(String msg) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
        text.setText(msg);
    }
    @Override
    public void showErrorMessage() {
        Toast.makeText(this, "网络请求数据出现异常", Toast.LENGTH_SHORT).show();
        text.setText("网络请求数据出现异常");
    }
}

效果图如下:
在这里插入图片描述
至此,已经完整的实现了一个简易的MVP架构。


三、平民版MVP架构 - base层顶级父类

乞丐版MVP架构模式中还存在很多问题不能应用到实际的开发中,大概存在的问题有:

  • 构架存在漏洞
  • 代码冗余量大
  • 通用性差

针对这些问题我们需要进一步优化,单车变摩托,升级为可以在实际开发中使用的平民版MVP架构。


1.结合Activity构建base层

由于MVP模式代码量的巨大,冗余代码实在太多,所以接下需要为MVP中所有单元都设计一个顶级父类来减少重复的冗余代码。

  • Callback

    在乞丐版中Callback接口中的onSuccess()方法需要根据请求数据类型的不同设置为不同类型的参数,所以每当有新的数据类型都需要新建一个Callback,解决方法是引入泛型的概念,用调用者去定义具体想要接收的数据类型:
package com.chen.mvpdemo2;

public interface MvpCallback<T> {
    /**
     * 数据请求成功
     * @param data 请求到的数据
     */
    void onSuccess(T data);
    /**
     *  使用网络API接口请求方式时,虽然已经请求成功但是由
     *  于{@code msg}的原因无法正常返回数据。
     */
    void onFailure(String msg);
    /**
     * 请求数据失败,指在请求网络API接口请求方式时,出现无法联网、
     * 缺少权限,内存泄露等原因导致无法连接到请求数据源。
     */
    void onError();
    /**
     * 当请求数据结束时,无论请求结果是成功,失败或是抛出异常都会执行此方法给用户做处理,通常做网络
     * 请求时可以在此处隐藏“正在加载”的等待控件
     */
    void onComplete();
}

  • BaseView

    View接口中定义Activity的UI逻辑。因为有很多方法几乎在每个Activity中都会用到,例如显示和隐藏正在加载进度条,显示Toast提示等,索性将这些方法变成通用的:
package com.chen.mvpdemo2;

import android.content.Context;

public interface BaseView {
    /**
     * 显示正在加载view
     */
    void showLoading();
    /**
     * 关闭正在加载view
     */
    void hideLoading();
    /**
     * 显示提示
     * @param msg
     */
    void showToast(String msg);
    /**
     * 显示请求错误提示
     */
    void showErr();
    /**
     * 获取上下文
     * @return 上下文
     */
    Context getContext();
}

  • BasePresenter

    Presenter中可共用的代码就是对View引用的方法了,值得注意的是,上面已经定义好了BaseView,所以我们希望Presenter中持有的View都是BaseView的子类,这里同样需要泛型来约束:
package com.chen.mvpdemo2;

public class BasePresenter<V extends BaseView> {
    /**
     * 绑定的view
     */
    private V mvpView;

    /**
     * 绑定view,一般在初始化中调用该方法
     */

    public void attachView(V mvpView) {
        this.mvpView = mvpView;
    }

    /**
     * 断开view,一般在onDestroy中调用
     */

    public void detachView() {
        this.mvpView = null;
    }

    /**
     * 是否与View建立连接
     * 每次调用业务请求的时候都要出先调用方法检查是否与View建立连接
     */
    public boolean isViewAttached() {
        return mvpView != null;
    }

    /**
     * 获取连接的view
     */
    public V getView() {
        return mvpView;
    }
}

  • BaseActivity

    BaseActivity主要是负责实现 BaseView 中通用的UI逻辑方法,如此这些通用的方法就不用每个Activity都要去实现一遍了。
package com.chen.mvpdemo2;

import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.Bundle;
import android.widget.Toast;

public abstract class BaseActivity extends Activity implements BaseView {
    private ProgressDialog mProgressDialog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mProgressDialog = new ProgressDialog(this);
        mProgressDialog.setCancelable(false);
    }

    @Override
    public void showLoading() {
        if (!mProgressDialog.isShowing()) {
            mProgressDialog.show();
        }
    }

    @Override
    public void hideLoading() {
        if (mProgressDialog.isShowing()) {
            mProgressDialog.dismiss();
        }
    }

    @Override
    public void showToast(String msg) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void showErr() {
        showToast(getResources().getString(R.string.api_error_msg));
    }

    @Override
    public Context getContext() {
        return BaseActivity.this;
    }
}

2.代码实现

  • Model
package com.chen.mvpdemo2;

import android.os.Handler;

public class MvpModel {
    /**
     * 获取网络接口数据
     * @param param 请求参数
     * @param callback 数据回调接口
     */
    public static void getNetData(final String param, final MvpCallback<String> callback){
        // 利用postDelayed方法模拟网络请求数据的耗时操作
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                switch (param){
                    case "normal":
                        callback.onSuccess("根据参数"+param+"的请求网络数据成功");
                        break;
                    case "failure":
                        callback.onFailure("请求失败:参数有误");
                        break;
                    case "error":
                        callback.onError();
                        break;
                }
                callback.onComplete();
            }
        },2000);
    }
}

  • View 接口
package com.chen.mvpdemo2;

public interface MvpView extends BaseView{
    /**
     * 当数据请求成功后,调用此接口显示数据
     * @param data 数据源
     */
    void showData(String data);
}

  • Presenter类
package com.chen.mvpdemo2;

public class MvpPresenter extends BasePresenter<MvpView> {
    /**
     * 获取网络数据
     * @param params 参数
     */
    public void getData(String params){
        if (!isViewAttached()){
            //如果没有View引用就不加载数据
            return;
        }
        //显示正在加载进度条
        getView().showLoading();
        // 调用Model请求数据
        MvpModel.getNetData(params, new MvpCallback<String>() {
            @Override
            public void onSuccess(String data) {
                //调用view接口显示数据
                if(isViewAttached()){
                    getView().showData(data);
                }
            }

            @Override
            public void onFailure(String msg) {
                //调用view接口提示失败信息
                if(isViewAttached()){
                    getView().showToast(msg);
                }
            }

            @Override
            public void onError() {
                //调用view接口提示请求异常
                if(isViewAttached()){
                    getView().showErr();
                }
            }

            @Override
            public void onComplete() {
                // 隐藏正在加载进度条
                if(isViewAttached()){
                    getView().hideLoading();
                }
            }
        });
    }
}

  • Activity
package com.chen.mvpdemo2;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends BaseActivity implements MvpView  {
    TextView text;
    MvpPresenter presenter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        text = (TextView)findViewById(R.id.text);
        //初始化Presenter
        presenter = new MvpPresenter();
        presenter.attachView(this);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        //断开View引用
        presenter.detachView();
    }
    @Override
    public void showData(String data) {
        text.setText(data);
    }
    // button 点击事件调用方法
    public void getData(View view){
        presenter.getData("normal");
    }
    // button 点击事件调用方法
    public void getDataForFailure(View view){
        presenter.getData("failure");
    }
    // button 点击事件调用方法
    public void getDataForError(View view){
        presenter.getData("error");
    }
}

3.最终效果

在这里插入图片描述


版权声明:本文为weixin_45208009原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。