Android MVP模式深入实践探索(一)

其实MVP已经不算新东西了,写这篇文章的目的主要是为了把之前在项目重构中应用的MVP模式做一个整理、总结和记录,把实践代码做成一个可描述可理解的过程。

一篇可能写不完,总共打算分几篇来记录,这将包括且不限于以下内容:

  • MVP模式基础结构介绍
  • MVP模式中如何分类,哪些属于View层,哪些该放在Presenter层,哪些该放在Modle层
  • MVP模式中关于View层使用的数据类
  • MVP模式中View层与Presenter层如何进行交互通信,Presenter层与Model层如何进行交互通信
  • MVP模式中如何管理和划分Model层(网络、文件、数据库)
  • MVP模式中为Presenter增加生命周期
  • MVP模式中如何进行划分抽象提取各层的基类以提高复用
  • MVP模式如何结合具体的业务模型处理(理想与现实)
  • MVP模式中避免内存泄漏
  • MVP + Clean Architecture模式的探索
  • MVVM模式的了解

说起MVP就不得不提起MVC, 因为MVP的是在MVC的基础上优化而来的:

MVC角色说明:

角色 职责
View 视图界面层,与用户发生交互,接收用户输入的请求转发给Controller处理
Controller 接收View的请求, 从视图层获取数据,执行业务逻辑,并调用Model层进行数据存取
Model 执行数据存取的业务逻辑,并根据业务模型通知视图层更新(一般是通过观察者模式)

MVP角色说明:

角色 职责
View 视图界面层,与用户发生交互,接收用户输入的请求转发给Presenter处理
Presenter 接收View的请求, 从视图层获取数据,执行业务逻辑,并调用Model层进行数据存取,同时会调用View层的接口将数据更新到视图
Model 执行数据存取的业务逻辑,并将数据返回给Presenter层

可以看到MVP与MVC最大的区别就是MVP解耦了View层和Model层,两者不直接发生交互而是通过P层,而在MVC中Model还是会跟View层发生交互的。

传统的MVC模式更加适合于大型项目的开发,对于小型项目应用MVC反而显得臃肿繁琐,就像我们以前经常在Activity中最喜欢写的代码一样:UI处理、网络请求、数据存取等全部业务都放在Activity中来完成,这就像一个大杂烩,此时的Activity几乎兼顾了MVC中的所有角色,实际上在我看来它此时根本就不具备任何模式可言。而MVP模式的出现将臃肿的部分分解开来,恰好适合于小型的应用程序,比如移动端的应用,并且对单元测试也比较友好,所以大家都在提倡用这个模式进行开发。

按照惯例,还是从一个简单的登录页面来演示MVP的基本使用(几乎我看过的所有关于MVP的文章貌似都是拿登录页面来入门的。。)

布局页面:
在这里插入图片描述
布局activity_login.xml代码:

<?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:gravity="center"
    >

    <EditText
        android:id="@+id/edit_user_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="text"
        android:maxLines="1"
        android:hint="请输入用户名"
        android:textSize="16sp"
        android:textColor="@color/black" />

    <EditText
        android:id="@+id/edit_user_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="textPassword"
        android:maxLines="1"
        android:hint="请输入密码"
        android:textSize="16sp"
        android:textColor="@color/black" />

    <Button
        android:id="@+id/btn_login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:text="登录"
        android:textSize="16sp"
        android:textColor="@color/black"
        />
</LinearLayout>

LoginActivity代码:

public class LoginActivity extends Activity implements View.OnClickListener {
    private EditText mUserNameEdit;
    private EditText mUserPasswordEdit;
    private Button mLoginBtn;

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

    private void initView() {
        mUserNameEdit = (EditText) findViewById(R.id.edit_user_name);
        mUserPasswordEdit = (EditText) findViewById(R.id.edit_user_password);
        mLoginBtn = (Button) findViewById(R.id.btn_login);
        mLoginBtn.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_login:
                //TODO
                break;
            default:
                break;
        }
    }
}

如果是传统的MVC的话,到这一步基本就结束了,接下来就开始在LoginActivity中搞事情作死了。。

如果使用MVP的话,接下来我们需要建立几个package来存放对应的角色:
在这里插入图片描述
其中view包下存放activity、fragment等UI控件,iview包下存放presenter与view进行交互的接口类,我们把LoginActivity放入view/activity包中:
在这里插入图片描述
其他包下面的情况:
在这里插入图片描述
在MVP中为了解耦Presenter跟View和Model的交互都通过接口进行,所以新建一个跟LoginActivity交互的接口类ILoginView

public interface ILoginView {
    /** 获取输入框的登录用户名 */
    String getUserName();
    /** 获取输入框的用户密码 */
    String getUserPassword();
    /** 显示Toast提醒 */
    void showToast(String msg);
    /** 登录成功的UI处理 */
    void onLoginSuccess();
    /** 登录失败的UI处理 */
    void onLoginFail();
    /** 显示加载中弹窗 */
    void showProgressDialog();
    /** 隐藏加载中弹窗 */
    void hideProgressDialog();
    /** 获取当前UI页面的上下文 */
    Context getContext();
}

view接口类中定义的基本都是一些UI数据或者显示UI控件的方法,然让LoginActivity实现这个接口:

public class LoginActivity extends Activity implements ILoginView, View.OnClickListener {
    private EditText mUserNameEdit;
    private EditText mUserPasswordEdit;
    private Button mLoginBtn;
    public ProgressDialog mProgressDialog;

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

    private void initView() {
        mUserNameEdit = (EditText) findViewById(R.id.edit_user_name);
        mUserPasswordEdit = (EditText) findViewById(R.id.edit_user_password);
        mLoginBtn = (Button) findViewById(R.id.btn_login);
        mLoginBtn.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_login:
                //TODO
                break;
            default:
                break;
        }
    }

    @Override
    public String getUserName() {
        return mUserNameEdit.getText().toString();
    }

    @Override
    public String getUserPassword() {
        return mUserPasswordEdit.getText().toString();
    }

    @Override
    public void showToast(String msg) {
        ToastUtils.showToast(this, msg);
    }

    @Override
    public void onLoginSuccess() {
        ToastUtils.showToast(this, "登录成功");
        //跳转首页
        gotoHomeActivity();
        finish();
    }

    @Override
    public void onLoginFail() {
        ToastUtils.showToast(this, "登录失败");
    }

    @Override
    public void showProgressDialog() {
        if (mProgressDialog == null) {
            mProgressDialog = DialogUtils.showSpinningProgressDialog(this, "正在登录中...", false);
        } else {
            if (!mProgressDialog.isShowing()) {
                mProgressDialog.show();
            }
        }
    }

    @Override
    public void hideProgressDialog() {
        if (mProgressDialog != null) {
            mProgressDialog.dismiss();
        }
    }
    
    @Override
    public Context getContext() {
        return this;
    }
    
    /** 跳转首页 */
    private void gotoHomeActivity() {
        Intent intent = new Intent(this, MainActivity.class);
        startActivity(intent);
    }
}

View层到此就完事了,接下来实现PresenterModel角色的接口和实现类

Presenter接口和实现类:

public interface ILoginPresenter {
    /** 登录操作逻辑处理 */
    void login();
    /** 登录成功逻辑处理 */
    void onLoginSuccess();
    /** 登录失败逻辑处理 */
    void onLoginFail(String errMsg);
}
public class LoginPresenter implements ILoginPresenter {
    private ILoginView mLoginView;
    private ILoginModel mLoginModel;

    public LoginPresenter(ILoginView loginView) {
        mLoginView = loginView;
        mLoginModel = new LoginModelImpl(this);
    }

    @Override
    public void login() {
        // 对用户名和密码的校验逻辑,这里只简单判空,实际可以加更多校验
        if (TextUtils.isEmpty(mLoginView.getUserName())) {
            mLoginView.showToast("请输入用户名");
            return;
        }
        if (TextUtils.isEmpty(mLoginView.getPassword())) {
            mLoginView.showToast("请输入密码");
            return;
        }
        //判断网络是否可用
        if (!NetUtils.checkNetState(mLoginView.getContext())) {
            mLoginView.showToast("当前无网络连接,请检查网络");
            return;
        }
        //显示登录进度弹窗
        mLoginView.showProgressDialog();
        //调用model层发起登录请求
        mLoginModel.sendLoginRequest(mLoginView.getUserName(), mLoginView.getPassword());
    }

    /** 登陆成功 */
    @Override
    public void onLoginSuccess() {
    	//隐藏登录进度弹窗
        mLoginView.hideProgressDialog();
        //回调View层接口
        mLoginView.onLoginSuccess();
    }

    /** 登陆失败 */
    @Override
    public void onLoginFail(String errMsg) {
   	    //隐藏登录进度弹窗
        mLoginView.hideProgressDialog();
        //回调View层接口
        mLoginView.onLoginFail(errMsg);
    }
}

可以看到在LoginPresenter的实现类当中分别持有了ILoginViewILoginModel两个接口,LoginPresenter通过这两个接口分别与LoginActivity和LoginModel进行交互。其中ILoginView变量是通过构造函数传递进来的,而ILoginModel则是在构造函数内部创建的。

Model接口和实现类:

public interface ILoginModel {
    /** 发起登录请求 */
    void sendLoginRequest(String userName, String password);
}
public class LoginModelImpl implements ILoginModel {
    private static final String API_LOGIN = "/mobile/login";
    private ILoginPresenter mPresenter;

    public LoginModelImpl(ILoginPresenter mPresenter) {
        this.mPresenter = mPresenter;
    }

    @Override
    public void sendLoginRequest(String userName, String password) {
        String url = BuildConfig.BASE_IP + API_LOGIN;
        StringHashMap requestParams = new StringHashMap();
        requestParams.put("userName", userName);
        requestParams.put("password", password);
        HttpDataManager.post(url, requestParams, new HttpCallback() {
            @Override
            public void onSuccess(String result, Object tag) {
                LoginResultBean loginResult = JsonUtils.jsonToObject(result, LoginResultBean.class);
                if (loginResult != null) {
                    if (loginResult.getErrCode() == 200) {
                        mPresenter.onLoginSuccess();
                    } else {
                        mPresenter.onLoginFail(loginResult.getErrMsg());
                    }
                } else {
                    mPresenter.onLoginFail("登录失败,服务不可用");
                }

            }

            @Override
            public void onError(String result, Object tag) {
                mPresenter.onLoginFail("登录失败:"+result);
            }
        }, 100);
    }
}

LoginModelImpl实现类中持有了ILoginPresenter接口,通过它与LoginPresenter实现类进行交户。在LoginModelImpl实现类中发起请求调用的是HttpDataManager类的post方法,这个类实际上就是你的网络请求框架组件,这里用的是基于开源库的一个简单封装(内部其实还是用的OkHttpClient)。

HttpDataManager类的代码:

public class HttpDataManager {

    /** 发起get请求 */
    public static void get(String url, StringHashMap params, HttpCallback callback, int tag) {
        RequestParams requestParams = new RequestParams();
        requestParams.setAllStrParams(params);
        BaseHttpRequest request = new BaseHttpRequest(url, requestParams, callback, tag);
        HttpClient.getInstance().get(request);
    }

    /** 发起post请求 */
    public static void post(String url, StringHashMap params, HttpCallback callback, int tag) {
        RequestParams requestParams = new RequestParams();
        requestParams.setAllStrParams(params);
        BaseHttpRequest request = new BaseHttpRequest(url, requestParams, callback, tag);
        HttpClient.getInstance().post(request);
    }
}

在这里插入图片描述
这里建了两个类HttpDataManagerLocalDataManager分别用来管理网络请求和本地数据存取(File、SharePrefer、DataBase),这里你可以使用流行的开源网络请求库或者其他数据管理库(如Retrofit、GreenDao等)。当然对于一个好的架构模式不应该受限于这些库,你可以随意选取。

最后就是在LoginActivity当中调用LoginPresenter的方法进行登录操作了:

 	@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        mLoginPresenter = new LoginPresenter(this);
        initView();
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_login:
                //开始登录
                mLoginPresenter.login();
                break;
            default:
                break;
        }
    }

到此,一个简单登录页面的MVP模式的简单实现就完成了。

如果是第一次接触MVP,你会明显的发现类文件比传统的写法陡增啊!是的,没错,这也是MVP的缺点之一,一个页面要想用MVP模式做的比较完美至少要建立五、六个Java类,因为Model层和Presenter层要交互,Presenter层和View层要交互,每个交互都需要接口来进行,即便你使用一些依赖注入框架,可能减少new对象的代码,但是Java类文件数量依然是少不了的,这是由MVP的角色和角色职责功能决定的。

所以你可能感觉原来在一个Activity当中就能完成的工作,现在要分这么多文件,但是这样做的代价就是换来了业务逻辑的解耦和职责的划分清晰,别忘了我们当初是为了什么目的来用这个模式的!任何一个模式都会有利有弊,有奉献就会有牺牲,你不可能做到只利用它的优点而不接受它的缺点影响。

这也是为什么前面提到MVP模式不适合大型项目的开发,因为大型项目本来类文件就多,你再用MVP类文件要翻好几倍,顿时就炸了,所以说对移动端的应用比较适合,因为移动端的app程序一般都比较小。

MVP的优点和缺点:

优点:

  • 解耦View层和业务层,职责清晰,便于维护,从此不用再担心Activity中的代码臃肿了,现在的View就像是一个只有setter和getter的控件而已。
  • View层可以进行组件化拆分,因为没有业务的掺杂,可以抽取高度可复用的封装。
  • 在View层个性化多变的情况下,业务层的代码也可以复用。
  • 对单元测试友好,View层和业务层解耦相互无感知,所以可以分开单独测试。

缺点:

  • 类文件随之增加,无疑也会增加维护成本,增加或修改一个业务方法,对应的每一层的接口都要增加或者修改
  • 调用链变长,增加bug调试的工作量,在定位问题上会花费更多的时间成本

当然上面的例子是比较理想的状态,实际项目中可能不一定按照完美的实现过程来做,例如,假如一个页面更新/获取view的地方掺杂了业务逻辑特别的多超过了50个,那么你要对应的去写一套接口的话估计要疯掉,这就是理想与现实的一个的差距,后面的文章中单独来分析如何在理想与现实之间做一个妥协。

在实际项目中MVP的每一层其实可以做纵向的复用代码提取,在每一层中也可以再进行分类,比如根据业务模型再去划分等等,具体后面文章再说吧。
在这里插入图片描述
下一篇将记录总结:MVP模式中如何分类,哪些属于View层,哪些该放在Presenter层,哪些该放在Modle层



发布了96 篇原创文章 · 获赞 57 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/lyabc123456/article/details/93910593