android-MVP架构

MVP

简介

MVP是模型(Model)、视图(View)、主持人(Presenter)的缩写,分别代表项目中3个不同的模块。如图所示:


image
  • View 对应于Activity、Fragment,负责界面的绘制以及与用户交互
  • Model 依然是业务逻辑和实体模型
  • Presenter 负责完成View于Model间的交互

设计前思考:

  • 首先在我们常用的MVC模式中,Activity承载了太多,做了不只是视图层的事情,而程序开发中最重要的 Context 一般也是在视图层才拥有的,所以我们需要把Context保持在视图中。
  • MVP相对于MVC,MVP中是依赖Presenter这个接口任务调度器来实现任务调度,则视图层中所有需要进行数据交互的,都需要将数据交给Presenter,而Presenter将调用Model来加载数据。
  • 在传统的MVC中,我常用 initView()、initData()、initEvent()、doOther() 这几个方法来实现数据流程加载、界面交互实现。现在我们需要拆分出来,Activity从BaseActivity中实现。

经过这样的构思,我们可以先实践一下,我们让View来实现Model的接口,View来调用presenter,presenter利用面向接口编程的思想来调用接口实现对View的操作。实例如下:


import android.content.Context;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;

import com.acheng.achengutils.mvp.model.BaseViewController;
import com.acheng.achengutils.mvp.presenter.BasePresenter;


/**
 * Created by pc859107393 on 2016/6/28.
 */
public abstract class BaseActivity<T extends BasePresenter, M extends BaseViewController> extends AppCompatActivity {

    public String TAG;  //当前Activity的标记

    protected T mPresenter;     //主持人角色

    protected abstract T initPresenter();    //获取到主持人


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TAG = String.format("%s::%s", getPackageName(), getLocalClassName());


        mPresenter = initPresenter();    //初始化Presenter,提供主持人,拥有主持人后才能提交界面数据给presenter

        setContentView(setLayoutId());

        initView();

        mPresenter.initData();

        initEvent();

        doOther();
    }

    protected void doOther() {

    }

    public Context getContext() {
        return this;
    }

    protected abstract void initEvent();


    protected abstract void initView();

    protected abstract int setLayoutId();

    @Override
    protected void onResume() {
        super.onResume();
        //如果presenter为空的时候,我们需要重新初始化presenter
        if (mPresenter == null) {
            mPresenter = initPresenter();
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
    }

    @Override
    public void onBackPressed() {   //返回按钮点击事件
        //当Activity中的 进度对话框正在旋转的时候(数据正在加载,网络延迟高,数据难以加载),关闭 进度对话框 , 然后可以手动执行重新加载

        super.onBackPressed();
    }

    /**
     * 恢复界面后,我们需要判断我们的presenter是不是存在,不存在则重置presenter
     *
     * @param savedInstanceState
     */
    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        if (mPresenter == null)
            mPresenter = initPresenter();
    }

    /**
     * onDestroy中销毁presenter
     */
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mPresenter = null;
    }

}

既然我们的Activity已经设定好了BaseActivity,我们需要接着完成BasePresenter,如下:

import com.acheng.achengutils.mvp.model.BaseViewController;

/**
 * Created by acheng on 2016/7/14.
 */
public abstract class BasePresenter<D extends BaseViewController> {


    public D model;

    /**
     * 在子类的构造函数中,设定参数为model,这时候可以presenter调用接口来实现对界面的操作。
     */
    public BasePresenter(D model) {
        this.model = model;
    }

    public abstract void initData();


}

关于我这个Presenter的设计,我想说的是我们需要将各层解耦,那么我的presenter就不应该持有Android程序流转的必然因子,如Context、Bundle、Intent、View等,如果我们需要实现对界面的操作,必须通过调用我们设定好的Model来实现,关于BaseModel更加简单了,直接是一个空的接口文件,如下:


public interface BaseViewController {
    //这里面添加实现类需要实现的方法即可
}

设计后的思考

  • presenter作为主持人,应该随着视图的关闭而关闭,所以我们需要在Activity和Fragment的关闭的时候,注销相应的presenter
  • 在应用程序被销毁的时候,我们重启了程序,但是这时应用的状态如果不恢复到前面的状态那么我们需要把对应的presenter重建
  • 在应用恢复后,如果想保持刚才的状态,那么我们需要在被销毁前把视图的状态保存,并且恢复对应的状态

说了这么多,我们直接手底下见真章:


import android.Manifest;
import android.annotation.TargetApi;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.support.v7.app.AlertDialog;
import android.view.View;
import android.widget.TextView;

import com.acheng.achengutils.mvp.view.BaseActivity;
import com.acheng.achengutils.utils.SPHelper;
import com.acheng.achengutils.widgets.AppUpdateDialog;
import com.acheng.achengutils.widgets.MustDoThingDailog;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import acheng1314.cn.a3dbuild.MyApplication;
import acheng1314.cn.a3dbuild.R;
import acheng1314.cn.a3dbuild.bean.LoginBean;
import acheng1314.cn.a3dbuild.view.activity.presenter.LoginActivityPresenter;
import acheng1314.cn.a3dbuild.view.activity.viewcontroller.LoginActivityViewController;
import acheng1314.cn.a3dbuild.widgets.MyProgressDialog;

/**
 * Created by pc859107393 on 2016/9/12 0012.
 */
public class LoginActivity extends BaseActivity<LoginActivityPresenter, LoginActivityViewController> implements LoginActivityViewController {

    private View mBt_login;
    private TextView mEt_username;  //用户名
    private TextView mEt_password;  //密码s


    final private int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124;
    private AppUpdateDialog appPermission;  //权限申请对话框
    private MyProgressDialog myProgressDialog;  //进度对话框

    @Override
    protected LoginActivityPresenter initPresenter() {
        return new LoginActivityPresenter(this);    //实例化LoginActivity的Presenter
    }

    @Override
    protected void initEvent() {
        mBt_login.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MyApplication.getInstance().outLog(TAG, "MDZZ");    //日志输出
                //调用Presenter的登录的网络请求,将用户名和密码传递过去
                mPresenter.doLogin(mEt_username.getText().toString(), mEt_password.getText().toString()); 
                


            }
        });
    }

    @Override
    protected void initView() {
        MyApplication.getInstance().addActivity(this);  //将Activity加入堆栈管理
        mEt_username = (TextView) findViewById(R.id.mEt_username);
        mEt_password = (TextView) findViewById(R.id.mEt_password);
        mBt_login = findViewById(R.id.mBt_login);
    }

    @Override
    protected void doOther() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            List<String> permissionsNeeded = new ArrayList<String>();

            final List<String> permissionsList = new ArrayList<String>();
            if (!addPermission(permissionsList, Manifest.permission.WRITE_EXTERNAL_STORAGE))
                permissionsNeeded.add("手机存储空间");
            if (!addPermission(permissionsList, Manifest.permission.READ_PHONE_STATE))
                permissionsNeeded.add("获取手机状态");
            if (!addPermission(permissionsList, Manifest.permission.CAMERA))
                permissionsNeeded.add("手机相机");
            if (!addPermission(permissionsList, Manifest.permission.ACCESS_COARSE_LOCATION))
                permissionsNeeded.add("手机位置");
//            if (!addPermission(permissionsList, Manifest.permission.WRITE_SETTINGS))
//                permissionsNeeded.add("手机设置");

            if (permissionsList.size() > 0) {
                if (permissionsNeeded.size() > 0) { //待申请的权限列表
                    // Need Rationale
                    String message = "你必须允许本APP使用:" + permissionsNeeded.get(0);
                    for (int i = 1; i < permissionsNeeded.size(); i++)
                        message = message + ", " + permissionsNeeded.get(i);
                    showMessageOKCancel(message,
                            new DialogInterface.OnClickListener() {
                                @TargetApi(Build.VERSION_CODES.M)
                                @Override
                                public void onClick(DialogInterface dialog, int which) {
                                    requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
                                            REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
                                }
                            });
                    return;
                }
                requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
                        REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
            }
        }
        super.doOther();
    }

    private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
        new AlertDialog.Builder(this)
                .setMessage(message)
                .setPositiveButton("允许", okListener)
                .setNegativeButton("拒绝", null)
                .create()
                .show();
    }

    private boolean addPermission(List<String> permissionsList, String permission) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
                permissionsList.add(permission);
                if (!shouldShowRequestPermissionRationale(permission))
                    return false;
            }
        }
        return true;
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        switch (requestCode) {
            case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS: {
                Map<String, Integer> perms = new HashMap<String, Integer>();
                // Initial
                perms.put(Manifest.permission.WRITE_EXTERNAL_STORAGE, PackageManager.PERMISSION_GRANTED);
                perms.put(Manifest.permission.READ_PHONE_STATE, PackageManager.PERMISSION_GRANTED);
                perms.put(Manifest.permission.CAMERA, PackageManager.PERMISSION_GRANTED);
                perms.put(Manifest.permission.ACCESS_COARSE_LOCATION, PackageManager.PERMISSION_GRANTED);
                // Fill with results
                for (int i = 0; i < permissions.length; i++)
                    perms.put(permissions[i], grantResults[i]);
                // Check for ACCESS_FINE_LOCATION
                if (perms.get(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
                        && perms.get(Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED
                        && perms.get(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
                        && perms.get(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
                    //经过用户授权,获得所有权限
                    if (appPermission != null) {
                        appPermission = null;
                    }
                    // All Permissions Granted
                } else {    //未得到用户授权
                    // Permission Denied
                    appPermission = new AppUpdateDialog(AppUpdateDialog.IMPORTANT, "一些权限未被允许,请在设置中授权!", getContext(), new AppUpdateDialog.NeedDoThing() {
                        @Override
                        public void mustDoThing() {
                            Uri packageURI = Uri.parse("package:" + getPackageName());
                            Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, packageURI);
                            startActivity(intent);
                        }
                    });
                }
            }
            break;
            default:
                super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        doOther();
    }

    @Override
    protected int setLayoutId() {
        return R.layout.activity_login;
    }

    @Override
    public void showDailog(String msg) {
        new MustDoThingDailog("提示", msg, getContext(), new MustDoThingDailog.NeedDoThing() {
            @Override
            public void mustDoThings() {

            }
        });
    }

    @Override
    public void showProgressD() {
        if (null == myProgressDialog)
            myProgressDialog = new MyProgressDialog("登陆", "正在登录···", getContext());
        else
            myProgressDialog.show();
    }

    @Override
    public void disProgressD() {
        if (null != myProgressDialog)
            myProgressDialog.dismiss();
    }

    @Override
    public void openHome(LoginBean bean) {

        SPHelper.setString(getContext(), getContext().getString(R.string.user), getContext().getString(R.string.username), mEt_username.getText().toString());
        SPHelper.setString(getContext(), getContext().getString(R.string.user), getContext().getString(R.string.password), mEt_password.getText().toString());
        SPHelper.setString(getContext(), getContext().getString(R.string.user), getContext().getString(R.string.userId), bean.getResult().getUserId());
        SPHelper.setString(getContext(), getContext().getString(R.string.user), getContext().getString(R.string.token), bean.getResult().getToken());

        startActivity(new Intent(getContext(), HomeActivity.class));

        finish();
    }
}

其实上面我们当中可以看到我们前台界面拿到用户数据后,调用presenter的doLogin方法,把用户名和密码传递过去,然后我们在Presenter中请求网络然后再通过调用接口实现数据回传。如下:

import com.acheng.achengutils.gsonutil.GsonUtils;
import com.acheng.achengutils.mvp.presenter.BasePresenter;
import com.acheng.achengutils.utils.CipherUtils;
import com.acheng.achengutils.utils.StringUtils;
import com.kymjs.rxvolley.RxVolley;
import com.kymjs.rxvolley.client.HttpCallback;
import com.kymjs.rxvolley.client.HttpParams;
import com.kymjs.rxvolley.http.VolleyError;

import acheng1314.cn.a3dbuild.MyApplication;
import acheng1314.cn.a3dbuild.bean.LoginBean;
import acheng1314.cn.a3dbuild.hostApi.MyApi;
import acheng1314.cn.a3dbuild.view.activity.viewcontroller.LoginActivityViewController;

/**
 * Created by pc859107393 on 2016/9/12 0012.
 */
public class LoginActivityPresenter extends BasePresenter<LoginActivityViewController> {
    /**
     * 在子类的构造函数中,设定参数为model,这时候可以presenter调用接口来实现对界面的操作。
     *
     * @param model
     */
    public LoginActivityPresenter(LoginActivityViewController model) {
        super(model);
    }

    @Override
    public void initData() {

    }

    public void doLogin(String name, String pwd) {
        //用户名和密码不能为空
        if (StringUtils.isEmpty(name) || StringUtils.isEmpty(pwd)) {
            model.showDailog("用户名或密码不能为空!"); //调用model的错误提示对话框
            return;
        }

        //密码MD5加密
        pwd = CipherUtils.small32md5(pwd);
        HttpParams params = new HttpParams();
        params.put("userName", name);
        params.put("passWord", pwd);
        RxVolley.post(MyApi.LoginApi, params, new HttpCallback() {
            @Override
            public void onSuccess(String t) {
                super.onSuccess(t);
                //数据不为空再进行数据处理
                try {
                    if (null != t) {
                        MyApplication.getInstance().outLog("输出", t);
                        LoginBean bean = new GsonUtils().toBean(t, LoginBean.class);
                        if (null != bean) {
                            if (bean.getCode() == 0) {
                                //请求成功
                                model.openHome(bean);
                            } else if (bean.getCode() == 1) {
                                model.showDailog("登录失败,帐户不存在");
                            } else if (bean.getCode() == 2) {
                                model.showDailog("登录失败,密码错误");
                            } else {
                                model.showDailog("登录失败,其他未知错误");
                            }
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    model.showDailog("登录失败,其他未知错误");
                }


            }

            @Override
            public void onFailure(VolleyError error) {
                super.onFailure(error);
                model.showDailog("登录失败,其他未知错误");
            }

            @Override
            public void onFinish() {
                super.onFinish();
                model.disProgressD();   //model的关闭对话框的接口
            }

            @Override
            public void onPreStart() {
                super.onPreStart();
                model.showProgressD();  //model的进度对话框
            }
        });


    }
}

我们上面可以看到我们现在只要把请求网络的数据传递上去就可以完成单元测试了,这样子我们就达到了我们数据流转的单元测试的标准。

既然我们都看到了Presenter对model的调用,那么我们直接贴上model再对比Activity就能明白了我们是怎么完成这个设计的。

public interface LoginActivityViewController extends BaseViewController {
    /**
     * 显示信息提示对话框
     * @param msg   message
     */
    void showDailog(String msg);

    /**
     * 显示进度对话框
     */
    void showProgressD();

    /**
     * 关闭对话框
     */
    void disProgressD();

    /**
     * 登陆成功跳转到其他界面
     * @param bean
     */
    void openHome(LoginBean bean);
}

我们看到这里,很多哥们可能又会不明白,为什么我们能控制界面呢?如下:

//我们在程序中,presenter直接调用的model,但是model是被View实现了的。
public class LoginActivity extends BaseActivity<LoginActivityPresenter, LoginActivityViewController> implements LoginActivityViewController {
    @Override
    public void showDailog(String msg) {
        //实现了model的显示对话框的方法
        new MustDoThingDailog("提示", msg, getContext(), new MustDoThingDailog.NeedDoThing() {
            @Override
            public void mustDoThings() {

            }
        });
    }

    @Override
    public void showProgressD() {
        //这是显示进度对话框的,实现了model的方法
    }

    @Override
    public void disProgressD() {
        //这是实现了moel的关闭进度对话框的方法
    }

    @Override
    public void openHome(LoginBean bean) {

        //实现了model的打开其他页面的方法
    }
}

所以我们的MVP执行的步骤其实就是:用户执行操作 -> 调用presenter(完成独立的数据处理) -> 调用model的方法控制界面 -> 展示给用户


然后应该又有哥们会问我,为什么你的基类中会有<>这种括号括起来的东西,恩恩这个是泛型,主要是用来说明他们是哪一类的东西,通过泛型来解耦就可以在基类中整合更多的东西。具体的要我来说明的话,我只能说“就不!!!”,我需要任性一回。关于MVP更好的介绍可以看下github的项目TheMvp,这个是我的偶像@张涛写的哟。

总结

  • 在mvp架构中,我们需要在基类中拿到每个界面对应的presenter和model,则我们需要让程序知道每个对应的presenter和model.
  • 为了减少不必要的代码开销,我们需要把每个activity和Fragment的公共方法抽取出来,写入基类中.
  • 在基类中,我们需要将具体的presenter和model解耦,则需要泛型进行类型转换来解除耦合.
  • 泛型解除耦合后,我们需要在每个具体的view中来持有presenter和实现model层的接口.并且通过每个view关联的presenter调用model的某个方法来控制view.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容