Android MVP设计模式总结

MVP设计模式从提出至今也有不短的时间了,大家应该或多或少使用过MVP模式开发项目,或者至少听说过MVP设计模式,不同的人对其有不同的理解,今天就来说说我所理解的MVP设计模式。

MVC

说起MVP就不得不提MVC设计模式,MVP模式是从MVC模式中演化出来的。MVC包含以下三种组件:

  • 控制器(Controller)- 负责转发请求,对请求进行处理。
  • 视图(View) - 界面设计人员进行图形界面设计。
  • 模型(Model) - 程序员编写程序应有的功能(实现算法等等)、数据库专家进行数据管理和数据库设计(可以实现具体的功能)。

他们之间的联系为:

维基百科中的MVC

View负责显示图形界面与获取用户事件输入,事件输入之后交给Controller进行逻辑控制处理,Controller调用Model进行数据处理(如:操作数据库读取数据等),当Model中数据发生改变后会通知Controller,Controller控制View刷新界面。同时View也可以直接将数据交给Model处理,并监听Model中的数据改变来刷新界面。(详细介绍可参考维基百科中的MVC设计模式)

MVP

MVP设计模式同样包含三种组件:

  • Model 定义用户界面所需要被显示的数据模型,一个模型包含着相关的业务逻辑。
  • View 视图为呈现用户界面的终端,用以表现来自 Model 的数据,和用户命令路由再经过 Presenter 对事件处理后的数据。
  • Presenter 包含着组件的事件处理,负责检索 Model 获取数据,和将获取的数据经过格式转换与 View 进行沟通。

但他与MVC设计模式不同的是,MVP模式实现了View与Model的完全解耦。结构图如下:

MVP设计模式结构图

可以看到相比MVC模式MVP各组件之间的分工更明确,View只负责UI展示和用户事件输入,Presenter负责协调View和Model的沟通,Model负责数据操作,数据操作的结果只需要反馈给Presenter。

这样设计的优点也显而易见:

  • 分类了视图、逻辑、数据层,降低了个模块之间的耦合性,并实现了视图层和数据层的完全解耦。
  • 个组件之间通过接口实现交互,可以很方便的进行单元测试。
  • 利于代码的复用,不同的Activity可以复用同一个Presenter,同样的不同Presenter也可以复用同一个Model进行数据处理。
  • 代码更加灵活
  • 对于大项目来说,方便不同开发人员进行模块化开发协作。

代码实现

上面说了这么多还得最终落实到代码上,下面将通过MVP模式实现简单的登录功能。

效果图

UML类图(不太熟练,如有错误,望不吝赐教):

登录功能的UML类图

项目结构:

登录功能MVP模式项目结构

分别创建View、Presenter、Model三个包存放三种组件的实现类。

UserBean:

用户存放用户信息的实体类,添加一个变量用于模拟不同的登录状态。

public class UserBean {
    private String userName;
    private String password;

    //模拟不同的登录状态
    private String loginResultType = "1";
    private String token;

    public UserBean() {
    }

    public UserBean(String userName, String password) {
        this.userName = userName;
        this.password = password;
    }

    public UserBean(String userName, String password, String loginResultType, String token) {
        this.userName = userName;
        this.password = password;
        this.loginResultType = loginResultType;
        this.token = token;
    }

   //Getter和Setter代码不贴了

    @Override
    public String toString() {
        return "UserName=" + userName
                + "\n Password=" + password
                + "\n token=" + token;
    }
}

View:

ILoginView接口定义:

public interface ILoginView {
    void showLoading();
    void hideLoading();

    /**
     * 登录成功
     * @param userBean 用户类
     */
    void showLoginSuccess(UserBean userBean);

    /**
     * 显示登录失败信息
     * @param message 失败信息
     */
    void showFailureMessage(String message);

    /**
     * 显示登录错误信息
     * @param message 错误信息
     */
    void showErrorMessage(String message);
}

LoginActivity实现:

public class LoginActivity extends AppCompatActivity implements ILoginView{
    private static final String TAG = LoginActivity.class.getSimpleName();

    private ILoginPresenter loginPresenter;         //login Presenter

    private RadioGroup loginResultRg;               //模拟登录状态的RadioGroup
    private EditText userNameEt;                    //用户名
    private EditText passwordEt;                    //密码

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

        loginResultRg = findViewById(R.id.login_result_rg);
        userNameEt = findViewById(R.id.user_name_et);
        passwordEt = findViewById(R.id.password_et);

        loginPresenter = new LoginPresenter(this);
    }

    /**
     * 登录事件
     * @param view 事件触发View
     */
    public void login(View view) {
        UserBean userBean = new UserBean();
        userBean.setUserName(userNameEt.getText().toString().trim());
        userBean.setPassword(passwordEt.getText().toString().trim());

        //通过RadioButton的选中状态模拟不同的登录状态
        switch (loginResultRg.getCheckedRadioButtonId()){
            case R.id.success_rb:
                userBean.setLoginResultType("1");
                break;
            case R.id.failure_rb:
                userBean.setLoginResultType("2");
                break;
            case R.id.error_rb:
                userBean.setLoginResultType("3");
                break;
        }
        loginPresenter.getLoginData(userBean);
    }

    @Override
    public void showLoading() {
        Log.d(TAG, "showLoading");

        Toast.makeText(LoginActivity.this, "showLoading", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void hideLoading() {
        Log.d(TAG, "hideLoading");
        Toast.makeText(LoginActivity.this, "hideLoading", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void showLoginSuccess(UserBean userBean) {
        Log.d(TAG, "showLoginSuccess user Information " + userBean.toString());
        Toast.makeText(LoginActivity.this, "showLoginSuccess userName=" + userBean.toString(), Toast.LENGTH_SHORT).show();
    }

    @Override
    public void showFailureMessage(String message) {
        Log.d(TAG, "showFailureMessage message= " + message);
        Toast.makeText(LoginActivity.this, "showFailureMessage msg=" + message, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void showErrorMessage(String message) {
        Log.d(TAG, "showErrorMessage message=" + message);
        Toast.makeText(LoginActivity.this, "showErrorMessage msg=" + message, Toast.LENGTH_SHORT).show();
    }
}

XML就不贴了需要的可以去下载demo看。

Model:

ILoginModel接口定义:

public interface ILoginModel {
    /**
     * 登录操作
     * @param param 参数
     */
    void doLogin(UserBean param, LoginCallBack loginCallBack);

    /**
     * 登录状态回调
     */
    public interface LoginCallBack{
        /**
         * 登录成功
         * @param data 返回数据
         */
        void onSuccess(UserBean data);

        /**
         * 调用登录接口时,接口调用成功,但是
         *      因用户名错误、登录失效等后台控制逻辑导致的登录失败
         * @param data 失败原因
         */
        void onFailure(String data);

        /**
         * 接口调用失败
         *      网络不通
         *      接口超时
         *      404、500等原因
         * @param error 失败原因
         */
        void onError(String error);

        /**
         * 接口请求结束,包括上面三中情况
         *     设置此方法通常是进行hideLoading等操作
         */
        void onComplete();
    }
}

LoginModel:

public class LoginModel implements ILoginModel {
    private Handler handler;

    public LoginModel(){
        handler = new Handler(Looper.getMainLooper());
    }

    @Override
    public void doLogin(final UserBean param, final LoginCallBack loginCallBack) {
        loginCallBack.onComplete();

        //模拟登录延迟操作
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {

                switch (param.getLoginResultType()){
                    case "1":
                        param.setToken("登录成功");
                        loginCallBack.onSuccess(param);
                        break;
                    case "2":
                        loginCallBack.onFailure("用户名或密码错误");
                        break;
                    case "3":
                        loginCallBack.onError("接口超时");
                        break;
                }
            }
        }, 3000);
    }
}

Presenter:

ILoginPresenter接口定义:

public interface ILoginPresenter {

    /**
     * 获取登录数据
     * @param param 参数
     */
    void getLoginData(UserBean param);
}

LoginPresenter实现:

public class LoginPresenter implements ILoginPresenter {

    private ILoginView loginView;
    private ILoginModel loginModel;

    public LoginPresenter(ILoginView loginView){
        this.loginView = loginView;
        loginModel = new LoginModel();
    }

    @Override
    public void getLoginData(UserBean userBean) {
        loginView.showLoading();

        loginModel.doLogin(userBean, new ILoginModel.LoginCallBack() {
            @Override
            public void onSuccess(UserBean data) {
                loginView.showLoginSuccess(data);
            }

            @Override
            public void onFailure(String data) {
                loginView.showFailureMessage(data);
            }

            @Override
            public void onError(String error) {
                loginView.showErrorMessage(error);
            }

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

一句话总结一下登录流程:
用户点击登录按钮触发登录操作,View也就是LoginActivity调用Presenter的getLoginData()方法,开启登录逻辑,Presenter调用Model的doLogin方法,执行具体的登录操作。Model将登录结果通过回调反馈给Presenter,Presenter控制View进行相应的UI显示。

另一种实现:

上面这种实现是最基本的实现,下面介绍另一种实现,将IView、IModel、IPresenter中的接口封装到contract中,并实现相关的基类方便其他模块扩展实现MVP模式。

UML类图:

MVP类图

项目结构

MVP项目结构图

Contract类

添加了Contract包,用于存放不同模块的协约类,用于将上一种实现方式中分散在IView、IModel、IPresenter中的接口统一归纳、统一管理。

public class LoginContract {
    /**
     * 登录View接口
     */
    public interface ILoginView {
        void showLoading();
        void hideLoading();

        /**
         * 登录成功
         * @param userBean 用户类
         */
        void showLoginSuccess(UserBean userBean);

        /**
         * 显示登录失败信息
         * @param message 失败信息
         */
        void showFailureMessage(String message);

        /**
         * 显示登录错误信息
         * @param message 错误信息
         */
        void showErrorMessage(String message);
    }

    /**
     * 登录Presenter
     */
    public interface ILoginPresenter {

        /**
         * 获取登录数据
         * @param param 参数
         */
        void getLoginData(UserBean param);
    }

    /**
     * 登录Model
     */
    public interface ILoginModel {
        /**
         * 登录操作
         * @param param 参数
         */
        void doLogin(UserBean param, LoginCallBack loginCallBack);
    }
}

View

IView:
定义了一个IView接口类,此类中抽象出所有View共同的方法,如:showLoading、hideLoading等,还有个作用就是为所有的View定义统一的接口方便之后在BasePresenter中进行泛型。

public interface IView {
    //定义统一的空接口
}

BaseActivity:
定义基类BaseActivity,封装一些通用方法便于其他模块的Activity进行扩展。注意在此类中实现了IView接口,所以在之后的Activity中不在需要实现IView接口。

public abstract class BaseActivity

extends AppCompatActivity implements IView{ //定义Presenter的泛型进行约束 protected P mPresenter; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if(initLayout() instanceof Integer){ setContentView((Integer) initLayout()); } else if(initLayout() instanceof View){ setContentView((View) initLayout()); } else{ throw new IllegalArgumentException("initLayout() 应该返回Int或者View类型对象"); } //初始化Presenter mPresenter = initPresenter(); //Presenter与View进行绑定 mPresenter.attachView(this); create(); } @Override protected void onDestroy() { super.onDestroy(); //Presenter与View解除绑定 mPresenter.detachView(); } /** 初始化Presenter的抽象方法 */ protected abstract P initPresenter(); /** 初始化布局的抽象方法 */ protected abstract Object initLayout(); /** Activity OnCreate之后的create抽象方法 */ protected abstract void create(); }

LoginActivity:
LoginActivity需要继承其父类BaseActivity并实现Login协约类中的View接口。

ublic class LoginActivity extends BaseActivity implements LoginContract.ILoginView {
    private static final String TAG = LoginActivity.class.getSimpleName();

    private RadioGroup loginResultRg;               //模拟登录状态的RadioGroup
    private EditText userNameEt;                    //用户名
    private EditText passwordEt;                    //密码

    @Override
    protected LoginPresenter initPresenter() {
        return new LoginPresenter();
    }

    @Override
    protected Object initLayout() {
        return R.layout.activity_main;
    }

    @Override
    protected void create() {
        loginResultRg = findViewById(R.id.login_result_rg);
        userNameEt = findViewById(R.id.user_name_et);
        passwordEt = findViewById(R.id.password_et);
    }

    /**
     * 登录事件
     * @param view 事件触发View
     */
    public void login(View view) {
        UserBean userBean = new UserBean();
        userBean.setUserName(userNameEt.getText().toString().trim());
        userBean.setPassword(passwordEt.getText().toString().trim());

        //通过RadioButton的选中状态模拟不同的登录状态
        switch (loginResultRg.getCheckedRadioButtonId()){
            case R.id.success_rb:
                userBean.setLoginResultType("1");
                break;
            case R.id.failure_rb:
                userBean.setLoginResultType("2");
                break;
            case R.id.error_rb:
                userBean.setLoginResultType("3");
                break;
        }

        mPresenter.getLoginData(userBean);
    }

    @Override
    public void showLoading() {
        Log.d(TAG, "showLoading");

        Toast.makeText(LoginActivity.this, "showLoading", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void hideLoading() {
        Log.d(TAG, "hideLoading");
        Toast.makeText(LoginActivity.this, "hideLoading", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void showLoginSuccess(UserBean userBean) {
        Log.d(TAG, "showLoginSuccess user Information " + userBean.toString());
        Toast.makeText(LoginActivity.this, "showLoginSuccess userName=" + userBean.toString(), Toast.LENGTH_SHORT).show();
    }

    @Override
    public void showFailureMessage(String message) {
        Log.d(TAG, "showFailureMessage message= " + message);
        Toast.makeText(LoginActivity.this, "showFailureMessage msg=" + message, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void showErrorMessage(String message) {
        Log.d(TAG, "showErrorMessage message=" + message);
        Toast.makeText(LoginActivity.this, "showErrorMessage msg=" + message, Toast.LENGTH_SHORT).show();
    }
}

Model

IModel:
统一的接口类IModel,作用同上IView接口。

public interface IModel {

}

LoginModel:
实现IModel和LoginContract.ILoginModel接口

public class LoginModel implements IModel,LoginContract.ILoginModel {
    private Handler handler;

    public LoginModel(){
        handler = new Handler(Looper.getMainLooper());
    }

    @Override
    public void doLogin(final UserBean param, final LoginCallBack loginCallBack) {
        loginCallBack.onComplete();

        //模拟登录延迟操作
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {

                switch (param.getLoginResultType()){
                    case "1":
                        param.setToken("登录成功");
                        loginCallBack.onSuccess(param);
                        break;
                    case "2":
                        loginCallBack.onFailure("用户名或密码错误");
                        break;
                    case "3":
                        loginCallBack.onError("接口超时");
                        break;
                }
            }
        }, 3000);
    }
}

Presenter

IPresenter:
统一的Presenter接口,定义了绑定和解绑View的方法。

public interface IPresenter {
    void attachView(IView view);
    void detachView();
}

BasePresenter:
定义View与Model的泛型进行约束,实现上面的接口。

public abstract class BasePresenter implements IPresenter{

    protected V mView;
    protected M mModel;

    public BasePresenter(){
        mModel = initModel();
    }

    @Override
    public void attachView(IView view) {
        mView = (V) view;
    }
    
    /**
    *初始化Moel的抽象方法
    */
    protected abstract M initModel();

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

LoginPresenter:
继承基类,实现接口,没什么好说的。

public class LoginPresenter extends BasePresenter implements LoginContract.ILoginPresenter {

    @Override
    public void getLoginData(UserBean userBean) {
        mView.showLoading();

        mModel.doLogin(userBean, new LoginContract.ILoginModel.LoginCallBack() {
            @Override
            public void onSuccess(UserBean data) {
                mView.showLoginSuccess(data);
            }

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

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

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

    @Override
    protected LoginModel initModel() {
        return new LoginModel();
    }
}

至此关于MVP就介绍完了,并扩展了一种MVP的实现方式,实现方式并不是固定的,你可以根据自己对MVP的理解和项目需要自行实现MVP设计模式。

Demo:博客中的项目Demo

[1]文中引用部分均来自中文维基百科]1

你可能感兴趣的:(Android MVP设计模式总结)