Android:安卓学习笔记之MVP模式的简单理解和使用

2023-11-10

MVP模式

1、 为什么使用MVP模式?

为什么引入架构呢?引入架构的项目,必是到了一定的规模,也就是出现了一定程度的耦合与冗余,也一定意义上违反了面向对象的单一职责原则。

那么MVP解决的问题就很明显了, 那就是冗余、混乱、耦合重。此时抛开MVP不讲,如果要我们自己想办法去解决,如何来解决呢?

分而治之, 我们可能会想到,根据单一职责原则,Activity或Fragment或其他组件冗余了,那么必然要根据不同的功能模块,来划分出来不同的职责模块,这样也就遵循了单一职责的原则。站在前人的智慧上,或许很多人就想到了M(Model)V(View)C(Controller)。我们可以借鉴这一开发模式,来达到我们的目的,暂时将一个页面划分为

UI模块,也即View层

Model模块,也即数据请求模块

Logic模块, 司逻辑处理

这样划分首先职责分工就明确了,解决了混乱,冗余的问题。

  • 一个项目从分包,到分类,最后拆分方法实现,都是遵从单一职责;
  • 一个职责划分越具有原子性, 它的重用性就越好,当然这也要根据实际业务而定。比如以下代码:

1.1、实例说明

在这里插入图片描述

public class LoginActivity extends AppCompatActivity {

    EditText inputUserName;
    EditText inputPassword;
    Button btnLogin;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        btnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                final String userName = inputUserName.getText().toString();
                final String password = inputPassword.getText().toString();

                boolean isEmptyUserName = userName == null || userName.length() == 0;
                boolean isEmptyPassword = userName == null || userName.length() == 0;

                boolean isUserNameValid =Pattern.compile("^[A-Za-z0-9]{3,20}+$").matcher(userName   ).matches();
                boolean isPasswordValid = Pattern.compile("^[A-Za-z0-9]{3,20}+$").matcher(password ).matches()if (isEmptyPassword || isEmptyPassword) {
                    Toast.makeText(LoginActivity.this, "请输入帐号密码", Toast.LENGTH_SHORT).show();
                } else {
                    if (isUserNameValid && isPasswordValid) {
                        new Thread(new Runnable() {
                            @Override
                            public void run() {
                                // ...登录请求
                                boolean loginResult = false;

                                if (loginResult) {
                                    Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
                                } else {
                                    Toast.makeText(LoginActivity.this, "登录失败", Toast.LENGTH_SHORT).show();
                                }
                            }
                        }).start();
                    } else {
                        Toast.makeText(LoginActivity.this, "帐号密码格式错误", Toast.LENGTH_SHORT).show();
                    }
                }
            }
        });
    }
}

一个简单的登录, 包括点击事件获取登录信息判断是否空校验是否正确请求登录返回处理

  • 这样的代码结构混乱, 可读性差; 代码冗余,可重用性差;
  • 不同功能的代码糅合在一起, 耦合性高。这只是很简单的一个小功能。

上面说到, 面向对象的单一职责原则, 一个模块划分越具有原子性,也即划分越细,那么重用性就越高。如果我改成这样

public class LoginActivity extends AppCompatActivity {

    EditText inputUserName;
    EditText inputPassword;
    Button btnLogin;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        btnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                final String userName = getEditorText(inputUserName);
                final String password = getEditorText(inputPassword);

                if (isEmpty(userName) || isEmpty(password)) {
                    showTips("请输入帐号密码");
                } else {
                    if (isValid(userName) && isValid(password)) {
                        // 登录
                        doLogin(userName, password);
                    } else {
                        showTips("帐号密码格式错误");
                    }
                }
            }
        });
    }

    private boolean isValid(String s) {
        return Pattern.compile("^[A-Za-z0-9]{3,20}+$").matcher(s).matches();
    }

    private boolean isEmpty(String s) {
        return s == null || s.length() == 0;
    }

    private String getEditorText(EditText et) {
        return et.getText().toString();
    }

    private void showTips(String tips) {
        Toast.makeText(LoginActivity.this, tips, Toast.LENGTH_SHORT).show();
    }

    private void doLogin(String username, String password) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                // ...登录请求
                boolean loginResult = false;
                // 更新UI
                notifyLoginResult(loginResult);
            }
        }).start();
    }

    private void notifyLoginResult(boolean loginResult) {
        if (loginResult) {
            showTips("登录成功");
        } else {
            showTips("登录失败");
        }
    }

}

将源码方法进行拆分后, isEmpty, isValid, showTips…等,产生的结果有亮点:

  • 1) 方法拆分后,可重用性提高了
  • 2) 相比而言,浏览一遍,我能基本清楚onClick里做了什么,也就是架构清晰了

这就是单一职责原则的作用,提高可重用性, 减少代码冗余,开始露出清晰的思维脉络。

以上说明了单一职责的意义,以及带来的附加的益处。那么代码经过初步重构以后, 虽然更清晰了,消除了冗余,但是耦合的问题依旧。那怎么解决耦合问题呢?我们来看下半场

2、一步步让你理解MVP

MVP最难的难点之一: 如何正确划分各模块

  • Model很简单, 数据加载的界限很明确,很简单就划分出来了, 比如数据库操作, 比如文件查询, 比如网络请求,可以连带着异步操作一起拿出来,划分为单独的Model层。

View层与Presenter层交互性很频繁,很多人不清楚这一块代码算是View,还是Presenter

  • 首先, 单纯的逻辑实现必然是Presenter处理的;单纯的View初始化也必然是View处理的,如findView这些。

像登录模块,View与逻辑交错在一起,怎么区分呢 ?

首先Login功能大抵分为以下子功能:

1、取值, EditText帐号与密码(明确的View层,不涉及逻辑操作)
2、判空与校验 (Presenter但涉及View, 因为使用帐号与密码,通过传参的形式)
3、登录请求 (名副其实的Model, 处理明显在Presenter层)
4、更新UI (View层)

其实以上划分界限相对比较清晰,项目中难免遇到一些不好界限的,教你一招,难以划分的必然包含View也包含逻辑处理。那么第一步,

  • 原子性拆分,将View与逻辑处理单独拆分成不同的方法。View 的部分在View层, 处理的部分在Presenter层
  • 有一些Toast, Dialog等的划分,根据Context作区分。
    • 可以使用Application Context实现的,可以作为Presenter层; 必须使用Activity
      Context的,作为View层

那么明确了M V P的拆分,看一下拆分结果

2.1、MVP实现第一步, 将页面拆分为M/V/P三个模块

1、View 部分

public class LoginActivity extends AppCompatActivity {

    EditText inputUserName;
    EditText inputPassword;
    Button btnLogin;

    LoginPresenter presenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        presenter = new LoginPresenter(this);

        btnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                presenter.execureLogin(getEditorText(inputUserName), getEditorText(inputPassword));
            }
        });
    }

    private String getEditorText(EditText et) {
        return et.getText().toString();
    }

    public void showTips(String tips) {
        Toast.makeText(LoginActivity.this, tips, Toast.LENGTH_SHORT).show();
    }

    public void notifyLoginResult(boolean loginResult) {
        if (loginResult) {
            showTips("登录成功");
        } else {
            showTips("登录失败");
        }
    }

}

2、Model部分

public class LoginModel {
    private Handler handler;

    public LoginModel() {
        handler = new Handler();
    }

    public interface OnLoginCallback {
        void onResponse(boolean success);
    }

    public void login(String username, String password, final OnLoginCallback callback) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                // ...请求接口
                boolean result = true; // 假设这是接口返回的结果
                callback.onResponse(result);
            }
        }).start();
    }
}

Presenter部分

public class LoginPresenter {

    private LoginModel model;
    private LoginActivity activity;
    private String verifyMsg;

    public LoginPresenter(LoginActivity activity) {
        this.activity = activity;
        model = new LoginModel();
    }

    public void execureLogin(String username, String password) {
        boolean verifyBefore = verifyBeforeLogin(username, password);
        if (verifyBefore) {
            // 校验通过,请求登录
            model.login(username, password, new LoginModel.OnLoginCallback() {
                @Override
                public void onResponse(boolean success) {
                    // 登录结果
                    activity.notifyLoginResult(success);
                }
            });
        } else {
            // 校验失败,提示
            activity.showTips(verifyMsg);
        }
    }

    private boolean verifyBeforeLogin(String username, String password) {
        boolean isEmpty = isEmpty(username) || isEmpty(password);
        boolean isValid = isValid(username) && isValid(password);
        if (isEmpty) {
            verifyMsg = "请输入帐号或密码";
            return false;
        }
        if (isValid) {
            return true;
        }
        verifyMsg = "帐号或密码错误";
        return false;
    }

    private boolean isValid(String s) {
        return Pattern.compile("^[A-Za-z0-9]{3,20}+$").matcher(s).matches();
    }

    private boolean isEmpty(String s) {
        return s == null || s.length() == 0;
    }
}

通过以上代码可以看出:

  • 1、Toast提示, 更新登录状态等, 都拆分在View层;
  • 2、校验与登录则拆分在Presenter层;
  • 3、网络请求则拆分到了Model层。

这样每一层都只处理本层的业务,从大的方向上进行了单一职责拆分,从而整体符合单一职责原则。

根据MVP将页面拆分为了3层,单一职责的原则我们已经完全符合了。但是仔细看,忽然发现相互之间还存在依赖,解耦效果并不是那么理想。那我们要思考了,是什么原因导致耦合尚在?

那就是对象持有,看看我们的项目

  • Presenter持有View(Activity)对象,同时持有Model对象
  • View持有Presenter对象

MVP是怎么解决对象持有问题的?

  • 面向接口编程

2.2、 MVP实现第2步, 使用接口通信,进一步解耦

对于面向对象设计来讲, 利用接口达到解耦目的已经是人尽皆知的了。 这次改动很小,把对象持有改为接口持有即可。

  • View持有Presenter对象改为持有Presenter接口
  • Presenter持有View对象改为持有View接口

既然持有接口,肯定要在View与Presenter分别实现供外部调用的接口。

  • View供Presenter调用的方法有notifyLoginResultshowTips
  • Presenter供View调用的方法有executeLogin

那么先来实现接口如何?看代码

Presenter接口

public interface IPresenter {
    /**
     * 执行登录
     *
     * @param username
     * @param password
     */
    void executeLogin(String username, String password);
}

View接口

public interface IView {
    /**
     * 更新登录结果
     *
     * @param loginResult
     */
    void notifyLoginResult(boolean loginResult);

    /**
     * Toast提示
     *
     * @param tips
     */
    void showTips(String tips);
}

接口的作用是对外部提供一种供外部调用的规范。因此这里我们把外部需要调用的方法抽象出来,加入到接口中。接口有了,且接口代表的是View或Presenter的实现,所以分别实现它们。看代码

Presenter实现接口

public class LoginPresenter implements IPresenter {

    private LoginModel model;
    private LoginActivity activity;
    private String verifyMsg;

    public LoginPresenter(LoginActivity activity) {
        this.activity = activity;
        model = new LoginModel();
    }

    @Override
    public void executeLogin(String username, String password) {
        boolean verifyBefore = verifyBeforeLogin(username, password);
        if (verifyBefore) {
            // 校验通过,请求登录
            model.login(username, password, new LoginModel.OnLoginCallback() {
                @Override
                public void onResponse(boolean success) {
                    // 登录结果
                    activity.notifyLoginResult(success);
                }
            });
        } else {
            // 校验失败,提示
            activity.showTips(verifyMsg);
        }
    }

    private boolean verifyBeforeLogin(String username, String password) {
        boolean isEmpty = isEmpty(username) || isEmpty(password);
        boolean isValid = isValid(username) && isValid(password);
        if (isEmpty) {
            verifyMsg = "请输入帐号或密码";
            return false;
        }
        if (isValid) {
            return true;
        }
        verifyMsg = "帐号或密码错误";
        return false;
    }

    private boolean isValid(String s) {
        return Pattern.compile("^[A-Za-z0-9]{3,20}+$").matcher(s).matches();
    }

    private boolean isEmpty(String s) {
        return s == null || s.length() == 0;
    }
}

View实现接口

public class LoginActivity extends AppCompatActivity implements IView{

    EditText inputUserName;
    EditText inputPassword;
    Button btnLogin;

    LoginPresenter presenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        presenter = new LoginPresenter(this);

        btnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                presenter.executeLogin(getEditorText(inputUserName), getEditorText(inputPassword));
            }
        });
    }

    private String getEditorText(EditText et) {
        return et.getText().toString();
    }

    @Override
    public void showTips(String tips) {
        Toast.makeText(LoginActivity.this, tips, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void notifyLoginResult(boolean loginResult) {
        if (loginResult) {
            showTips("登录成功");
        } else {
            showTips("登录失败");
        }
    }

}

在接口中提供对外部调用的方法,然后分别在View和Presenter中实现它们。接口与实现都有了,还记得我们的目的是什么吗?是把持有的对象替换为接口,撸起来,看代码

// 这是View持有的接口,在onCreate中初始化的对象由原来的LoginPresenter改为了IPresenter。
public class LoginActivity extends AppCompatActivity implements IView{

    EditText inputUserName;
    EditText inputPassword;
    Button btnLogin;

    IPresenter presenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        presenter = new LoginPresenter(this);
        ...
}

// 这是Presenter持有的接口,在构造中由原来的LoginActivity改为了IView

public class LoginPresenter implements IPresenter {

    private LoginModel model;
    private String verifyMsg;

    private IView activity;

    public LoginPresenter(IView activity) {
        this.activity = activity;
        model = new LoginModel();
    }

    ...
2.2.1、MVP遵从的面向对象原则

1) 单一职责

  • 每个模块只负责该模块的本职工作,不越界。 如View负责UI初始化与更新, Model负责数据查询与异步,至于逻辑判断,业务实现,放心的扔给Presenter中就好了。

2) 面向接口通信

  • 对象的持有是造成耦合的本质原因之一,因此要达到解耦的目的,替换为接口持有最是合适不过。

2.3、 从头到尾封装一个完整的MVP框架

2.3.1、业务分析

这里以登录为例。先来分析下业务:

1、EditText取得输入的UserName与Password

2、校验(包括判断是否是空的, 是否符合输入规范比如不允许输入特殊字符)

3、校验通过, 执行登录请求; 不通过,直接提示错误

4、登录请求

5、根据登录结果,提示登录成功或失败

6、伴随着登录结果更新UI

2.3.2、BaseInterface 与 Contract的概念

MVP引入了BaseInterface 与Contract的概念。如果单纯的mvp,可能很多人都理解,但是加上这两个概念,加深了理解难度。

base-interface 就是我们常用的base的概念,目的就是规范统一的操作。比如显示一个Toast, 判断网络是否连接,跳转动画等,我们都放在BaseActivity中,因为所有的Activity都需要这些。接口的继承也是这个目的。如登录功能

  • 1) 我们需要一个Presenter,于是有了LoginPresenter
  • 2)我们需要一个LoginPresenter的接口,为View层提供调用,于是有了ILoginPresenter
  • 3)无论登录,还是注册,还有其他功能,所有的Presenter都需要一个功能start, 于是有了IPresenter
    • IPresenter提供了一个所有Presenter接口共有的操作,就是start,也即初始化的加载 Contract的概念

这个概念的引入只是为了统一管理一个页面的View和Presenter接口。每个页面对应

  • 1个View(Activity或Fragment), 一个IView(View接口)
  • 1个Presenter,一个IPresenter(Presenter接口)
  • 1个Contract(一个包含View接口和Presenter接口的接口)

public interface LoginContract {

    interface View {
        void notifyLoginResult();
    }

    interface Presenter {
        void login(String username, String password);
    }

}
2.3.3、从头到尾封装一个完整的MVP框架。

1) 首先来思考,我们最先定义的应该是什么? 当然是公共接口。

View的公共接口(MVP-Samples中的IView)没有公共的操作,我们定义一个空的接口,用于统一规范。

public interface IView {
}

Presenter的公共接口(MVP-Samples中的IPresenter)也没有公共的操作,在mvp提供的samples中是带了一个start的,但是这里不需要。

  • 为什么呢?因为我们还要来一个BasePresenter。所以我们还是定义一个空的接口,用于统一规范。
public interface IPresenter {
}

以上两个接口,是用于给View与Presenter的接口继承的,注意,不是View或Presenter本身继承。因为它定义的是接口的规范, 而接口才是定义的类的规范。

2)定义的接口要继承IView与IPresenter, 而且由Contract统一管理

public interface LoginContract {

    interface View extends IView {

        // View中的2个功能:
        // 1) 取得登录需要的username, password # 不需要对Presenter层提供调用
        // 2) 提示错误信息, 提示登录结果 # 需要Presenter层调用,因为校验和登录都是在Presenter层的
        // 因此2)是View层提供的对外方法,需要在接口中定义

        /**
         * 提示一个Toast
         *
         * @param msg
         */
        void showToast(String msg);

    }

    interface Presenter extends IPresenter {

        // Presenter中的2个功能:
        // 1) 校验 # 看你怎么写,既可以在View层中调用校验方法,也可以在Presenter层中,这里定义为直接在Presenter中校验,彻底和View解耦
        // 2) 登录 # 先执行校验,再执行登录,需要在View层点击登录时调用
        // 因此2)是Presenter对外层提供的方法,需要在接口中定义

        /**
         * 登录操作
         *
         * @param username
         * @param password
         */
        void login(String username, String password);

    }
}

以上Contract(称之为功能仓库)分别定义了View与Presenter接口,并添加了接口的定义过程分析。

3) 接口定义完成了,下一步是什么呢? 肯定是实现接口,加入功能吧。定义Presenter与View分别实现接口,加入对应功能。

public class LoginActivity extends AppCompatActivity implements LoginContract.View {

    LoginContract.Presenter mPresenter;

    Button loginBtn;
    EditText etUser, etPwd;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mPresenter = new LoginPresenter(this);

        loginBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mPresenter.login(etUser.getText().toString(), etPwd.getText().toString());
            }
        });
    }

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

*********************************************************************************************************
public class LoginPresenter implements LoginContract.Presenter {

    LoginContract.View mView;

    public LoginPresenter(LoginContract.View mView) {
        this.mView = mView;
    }

    @Override
    public void login(String username, String password) {
        // 校验直接放在登录流程
        boolean isVerifySuccessully = verifyLoginInfo();
        if (isVerifySuccessully) {
            // 请求登录
            LoginModel.requestLogin(username, password, new LoginModel.RequestCallback() {
                @Override
                public void onResponse(boolean result) {
                    if (result) {
                        // 提示登录成功
                        mView.showToast("登录成功");
                    } else {
                        // 提示登录失败
                        mView.showToast("登录失败");
                    }
                }
            });
        } else {
            // 校验失败,提示错误
            mView.showToast("无效的帐号或密码");
        }
    }

    private boolean verifyLoginInfo() {
        // 这里校验登录信息
        // 校验帐号,密码是否为空
        // 校验帐号,密码是否符合要求
        return true;
    }
}

注意:LoginActivity在相对的生命周期中需要销毁Presenter引用,由于后面会封装,这里没加。

  • 可能出现内存泄漏,如果model层数据没有回调完成,activity退出,以为presenter持有当前activity,所以gc不了,出现内存泄漏

走到这一步基本就是一个完整的MVP开发模式了,从划分层次到接口通信,其实还是挺简单的,不是么?下面继续来优化这个框架,我们考虑以下几个问题:

  • 每个Activity或者Fragment都要初始化或管理Presenter,累不累?
  • 同样的,每个Presenter都要管理View,累不累?

2.4、 最终优化版

Presenter基类抽取,公共元素有哪些 :

  • Presenter公共元素,其实主要有两个: ContextView接口。
    • 注意:Presenter不要传入Activity的Context;
      • 如果需要用到Activity的Context,那么Presenter层就不单纯了。那么只能是Application的Context。
      • 我们获取Application Context的方式有两种,AppContext(你的Application)的静态获取 和 Activity的getApplicationContext。这里使用传入的Application Context吧!

很多网上View的获取是定义一个AttachView的方法, 这里使用在构造中直接传入。


public abstract class BasePresenter<AttachView extends IView> {
    private Context mContext;
    private AttachView mView;

    public BasePresenter(Context context, AttachView view) {
        if (context == null) {
            throw new NullPointerException("context == null");
        }
        mContext = context.getApplicationContext();
        mView = view;
    }

    /**
     * 获取关联的View
     *
     * @return
     */
    public AttachView getAttachedView() {
        if (mView == null) {
            throw new NullPointerException("AttachView is null");
        }
        return mView;
    }

    /**
     * 获取关联的Context
     *
     * @return
     */
    public Context getContext() {
        return mContext;
    }

    /**
     * 清空Presenter
     */
    public void clearPresenter() {
        mContext = null;
        mView = null;
    }

    /**
     * View是否关联
     *
     * @return
     */
    public boolean isViewAttached() {
        return mView != null;
    }

    /**
     * 网络是否连接
     *
     * @return
     */
    public boolean isNetworkConnected() {
        if (mContext == null) {
            throw new NullPointerException("mContext is null");
        }
        return NetworkHelper.isNetworkConnected(mContext);
    }

    public abstract void start();

    public abstract void destroy();
}

以上是我们抽取的Presenter基类。实现了:

1、初始化时绑定View接口,并在clear时清除接口
2、自动获取ApplicationContext(还是建议不这样,直接传Application的Context) 3、View状态判定
4、网络连接判断(因为Presenter中执行网络请求比较频繁,你可以根据业务自定义多个方法)
5、satrt初始化方法与destroy销毁方法(结合后面的MVPCompatActivity自动销毁)

注意:在使用View时,请先判断View状态,否则View异常销毁时会报NullPoiterException如果有线程或者Handler一定要在destroy中销毁,避免造成内存泄漏

for Activity

/**
 * MVP - Activity基类
 */
public abstract class MVPCompatActivity<T extends BasePresenter> extends RootActivity {
    protected T mPresenter;

    @Override
    protected void onStart() {
        super.onStart();
        if (mPresenter == null) {
            mPresenter = createPresenter();
        }
        mPresenter.start();
    }

    @Override
    protected void onStop() {
        super.onStop();
        mPresenter.clearPresenter();
        mPresenter = null;
    }

    @Override
    public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
        super.onSaveInstanceState(outState, outPersistentState);
        mPresenter.clearPresenter();
        mPresenter = null;
    }

    /**
     * 创建一个Presenter
     *
     * @return
     */
    protected abstract T createPresenter();

}

for Fragment


public abstract class MVPCompatFragment<T extends BasePresenter> extends RootFragment {
    protected T mPresenter;

    @Override
    public void onStart() {
        super.onStart();
        if (mPresenter == null) {
            mPresenter = createPresenter();
        }
        mPresenter.start();
    }

    @Override
    public void onStop() {
        super.onStop();
        if (mPresenter != null) {
            mPresenter.clearPresenter();
            mPresenter = null;
        }
    }

    protected abstract T createPresenter();

}

for Layout


public abstract class MVPCompatLayout<T extends BasePresenter> extends RootLayout {

    protected T mPresenter;

    public MVPCompatLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        mPresenter = createPresenter();
    }

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

    protected abstract T createPresenter();
}

for Adapter


public abstract class MVPCompatRecyclerAdapter<T, P extends BasePresenter> extends RootRecyclerAdapter<T> {
    protected P mPresenter;

    public MVPCompatRecyclerAdapter(Context context, List data) {
        super(context, data);
    }

    protected abstract P createPresenter();


    @Override
    public void onViewAttachedToWindow(RecyclerViewHolder holder) {
        super.onViewAttachedToWindow(holder);
        mPresenter = createPresenter();
    }

    @Override
    public void onViewDetachedFromWindow(RecyclerViewHolder holder) {
        super.onViewDetachedFromWindow(holder);
        if (mPresenter != null) {
            mPresenter.clearPresenter();
            mPresenter = null;
        }
    }
}

通过继承以上View的Base, 可以自由实现初始化以及销毁。轻松实现MVP。

最后,补充的RootActivity, 作为一个Base的Activity,是根据不同的业务决定里面的内容的,因此这里很少


public abstract class RootActivity extends AppCompatActivity {
    protected Context mContext;
    protected Context mAppContext;

    private View mContentView;

    private Bundle mBundleObj;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mAppContext = getApplicationContext();
        mContext = this;
        mContentView = getLayoutInflater().inflate(getLayoutRes(), null);
        setContentView(mContentView);
        ButterKnife.bind(this);
        init();
    }

    protected abstract int getLayoutRes();

    protected abstract void init();

    /**
     * findViewById
     *
     * @param resId
     * @param <T>
     * @return
     */
    protected <T extends View> T $(int resId) {
        return (T) findViewById(resId);
    }

    /**
     * Toast
     *
     * @param toast
     */
    protected void showToast(String toast) {
        Toast.makeText(this, toast, Toast.LENGTH_SHORT).show();
    }

    /**
     * get a bundle from reuse.
     *
     * @return
     */
    protected Bundle obtainBundle() {
        if (mBundleObj == null) {
            mBundleObj = new Bundle();
        } else {
            mBundleObj.clear();
        }
        return mBundleObj;
    }


}

3、具体实例

在这里插入图片描述

3.1、MyUser

public class MyUser extends BmobUser {

    private String image;

    private String gender;

    private String age;

    public String getImage() {
        return image;
    }

    public void setImage(String image) {
        this.image = image;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }
}

3.2、BaseContract

public interface BaseContract {

    interface BasePresenter<T> {

        void attachView(T view);

        void detachView();
    }

    interface BaseView {

        void onSuccess();

        void onFailure(Throwable e);


    }
}

3.3、LandContract

public interface LandContract extends BaseContract {

    interface View extends BaseContract.BaseView {

        void landSuccess(MyUser user);

    }

    interface Presenter extends BaseContract.BasePresenter<View>{
        /**
         * 用户登陆
         */
        void login(String username, String password);

        /**
         * 用户注册
         */
        void signup(String password, String mail);
    }
}

3.4、BaseMVPActivity

public abstract class BaseMVPActivity<T extends BaseContract.BasePresenter> extends BaseActivity{

    protected T mPresenter;

    protected abstract T bindPresenter();

    @Override
    protected void processLogic() {
        attachView(bindPresenter());
    }

    private void attachView(T presenter){
        mPresenter = presenter;
        mPresenter.attachView(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mPresenter.detachView();
    }
}

3.5、LoginActivity

public class LoginActivity extends BaseMVPActivity<LandContract.Presenter>
        implements LandContract.View, View.OnFocusChangeListener, View.OnClickListener {

    private OwlView mOwlView;
    private EditText usernameET;
    private EditText passwordET;
    private EditText rpasswordET;
    private TextView signTV;
    private TextView forgetTV;
    private Button loginBtn;

    //是否是登陆操作
    private boolean isLogin = true;
    
    @Override
    protected int getLayoutId() {
        return R.layout.activity_user_land;
    }

    @Override
    protected void initWidget() {
        super.initWidget();
        mOwlView=findViewById(R.id.land_owl_view);
        usernameET=findViewById(R.id.login_et_username);

        passwordET=findViewById(R.id.login_et_password);
        rpasswordET=findViewById(R.id.login_et_rpassword);
        signTV=findViewById(R.id.login_tv_sign);
        forgetTV=findViewById(R.id.login_tv_forget);
        loginBtn=findViewById(R.id.login_btn_login);
    }

    @Override
    protected void initClick() {
        super.initClick();
        passwordET.setOnFocusChangeListener(this);
        rpasswordET.setOnFocusChangeListener(this);
        signTV.setOnClickListener(this);
        forgetTV.setOnClickListener(this);
        loginBtn.setOnClickListener(this);
    }

    @Override
    public void onFocusChange(View v, boolean hasFocus) {
        if (hasFocus) {
            mOwlView.open();
        } else {
            mOwlView.close();
        }
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.login_btn_login:  //button
                if (isLogin) {
                    login();  //登陆
                } else {
                    sign();  //注册
                }
                break;
            case R.id.login_tv_sign:  //sign
                if (isLogin) {
                    //置换注册界面
                    signTV.setText("登录");
                    loginBtn.setText("注册");
                    rpasswordET.setVisibility(View.VISIBLE);
                    usernameET.setVisibility(View.GONE);
                } else {
                    //置换登陆界面
                    signTV.setText("注册");
                    loginBtn.setText("登录");
                    usernameET.setVisibility(View.VISIBLE);
                    rpasswordET.setVisibility(View.GONE);
                }
                isLogin = !isLogin;
                break;
            case R.id.login_tv_forget:  //忘记密码
                showForgetPwDialog();
                break;
            default:
                break;
        }
    }

    /**
     * 执行登陆动作
     */
    public void login() {
        String username = usernameET.getText().toString();
        String password = passwordET.getText().toString();
        if (username.length() == 0 || password.length() == 0) {
            SnackbarUtils.show(mContext, "用户名或密码不能为空");
            return;
        }

        ProgressUtils.show(this, "正在登陆...");

        mPresenter.login(username, password);
    }

    /**
     * 执行注册动作
     */
    public void sign() {
        String email = usernameET.getText().toString();
        String password = passwordET.getText().toString();
        String rpassword = rpasswordET.getText().toString();
        if (email.length() == 0 || password.length() == 0 || rpassword.length() == 0) {
            SnackbarUtils.show(mContext, "请填写必要信息");
            return;
        }
        if (!StringUtils.checkEmail(email)) {
            SnackbarUtils.show(mContext, "请输入正确的邮箱格式");
            return;
        }
        if (!password.equals(rpassword)) {
            SnackbarUtils.show(mContext, "两次密码不一致");
            return;
        }

        ProgressUtils.show(this, "正在注册...");
        usernameET.setText(email);
        mPresenter.signup(password,email);

    }

    /***********************************************************************/

    @Override
    protected LandContract.Presenter bindPresenter() {
        return new LandPresenter();
    }

    @Override
    public void landSuccess(MyUser user) {
        ProgressUtils.dismiss();
        if (isLogin) {
            setResult(RESULT_OK, new Intent());
            Intent intent = new Intent(this, MainActivity1.class);
            startActivity(intent);
            finish();
        } else {
            Toast.makeText(this, "注册成功", Toast.LENGTH_SHORT).show();
            //置换登陆界面
            signTV.setText("注册");
            loginBtn.setText("登录");
            rpasswordET.setVisibility(View.GONE);
            usernameET.setVisibility(View.VISIBLE);
            isLogin=true;

        }
        Log.i(TAG,user.toString());
    }

    @Override
    public void onSuccess() {
        ProgressUtils.dismiss();
    }

    @Override
    public void onFailure(Throwable e) {
        ProgressUtils.dismiss();
        SnackbarUtils.show(mContext, e.getMessage());
        Log.e(TAG,e.getMessage());
    }

    /**
     * 显示忘记密码对话框
     */
    public void showForgetPwDialog() {
                new MaterialDialog.Builder(this)
                        .title("找回密码")
                        .inputType(InputType.TYPE_CLASS_TEXT)
                        .input("请输入注册邮箱", null, (dialog, input) -> {
                            String inputStr = input.toString();
                            if (input.equals("")) {
                                SnackbarUtils.show(mContext, "内容不能为空!");
                            } else if(!StringUtils.checkEmail(inputStr)) {
                                Toast.makeText(LoginActivity.this,
                                        "请输入正确的邮箱格式", Toast.LENGTH_LONG).show();
                            } else {
                                //找回密码
                                BmobUser.resetPasswordByEmail(input.toString(), new UpdateListener() {
                                    @Override
                                    public void done(BmobException e) {
                                        if (e == null) {
                                            ToastUtils.show(mContext, "重置密码请求成功,请到邮箱进行密码重置操作");
                                        } else {
                                            ToastUtils.show(mContext, "重置密码请求失败,请确认输入邮箱正确!");
                                        }
                                    }
                        });
                    }
                })
                .positiveText("确定")
                .negativeText("取消")
                .show();
    }

}

3.6、LandPresenter

public class LandPresenter extends RxPresenter<LandContract.View> implements LandContract.Presenter{
    private String TAG="LandPresenter";
    @Override
    public void login(String username, String password) {
        MyUser.loginByAccount(username, password, new LogInListener<MyUser>() {
            @Override
            public void done(MyUser myUser, BmobException e) {
                if(e == null) {
                    mView.landSuccess(myUser);
                }else {
                    String error=e.toString();
                    if(error.contains("incorrect")){
                        Toast.makeText(getApplicationContext(), "账号或者密码错误!", Toast.LENGTH_SHORT).show();
                        mView.onSuccess();
                    }else {
                        mView.onFailure(e);
                    }
                }
            }
        });
    }
    @Override
    public void signup(String password, String mail) {
        MyUser myUser =new MyUser();
        myUser.setPassword(password);
        myUser.setEmail(mail);
        myUser.signUp(new SaveListener<MyUser>() {
            @Override
            public void done(MyUser myUser, BmobException e) {
                if(e == null)
                    mView.landSuccess(myUser);
                else{
                    String error = e.toString();
                    if(error.contains("already")){
                        Toast.makeText(getApplicationContext(), "邮箱已经被注册,请重新填写!", Toast.LENGTH_SHORT).show();
                        mView.onSuccess();
                    }else {
                        mView.onFailure(e);
                    }
                }
            }
        });
    }
}

参考

1、一步步带你精通MVP
2、移动架构这么多,如何一次搞定所有
3、Android 架构MVC MVP MVVM+实例

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android:安卓学习笔记之MVP模式的简单理解和使用 的相关文章

  • 添加监听器与设置监听器

    添加监听器和设置监听器有什么区别 e g addTextChangedListener textWatcher setOnClickListener clickListener Answer 在 aioobe 的回答之后 我在我的项目中对此
  • 旋转对话框的自定义主题

    我想自定义当我单击 spinnermode dialog 中的微调器时出现的对话框的外观 我想使用对话框片段 但没有找到文档 是否可以 不会 外观是固定的 给人一种操作系统的感觉 您必须创建自己的微调器才能执行此操作 这并不容易 但如果微调
  • 在 docker 容器内创建 android 模拟器时出现“sh: 1: file: not found”

    我正在尝试在 docker 容器内创建一个 android 模拟器 但遇到了一些问题 SDK 更新和 AVD 创建成功 我尝试创建模拟器 出现以下错误 emulator avd test 22 no skin no audio no win
  • 使用缩略图作为毕加索的占位符

    从用户体验的角度来看 首先向用户显示缩略图 直到真实图像完成加载 然后向他显示 这会很棒 但是Picasso https github com square picasso仅使用资源文件作为占位符 例如 Picasso with conte
  • 如何突出显示在 Textview android 中单击的单词

    我正在创建一个应用程序 在其中我从 EditText 中的用户获取输入 现在 单击按钮后 输入的字符串将显示在 TextView 中 现在我的问题是 当用户单击任何单个单词时 我会使该单词在 TextView 中突出显示 例如 用户输入 你
  • 如何在谷歌地图中使用latlng字符串数组绘制多边形

    在我的应用程序中 我有包含 imagview 的 recyclerview 并且该 imageview 通过使用我存储在 sqlite 中的坐标包含静态地图图像 当我单击该图像时 我将该字符串数组格式的坐标传递给其他地图活动 然后使用该字符
  • 如何以编程方式启用编辑文本的垂直滚动条

    我正在尝试实施android scrollbars vertical 通过 Java 代码在 XML 中 我尝试过方法setVerticalScrollBarEnabled true 但它不起作用 有人可以给我一个建议吗 提前致谢 您可以使
  • 在 Phonegap 3.x CLI 上构建 android 发布 apk

    如何使用 Phonegap 3 x CLI 在本地构建 Android 应用程序并准备发布 我检查了项目的platforms android目录中生成的bin文件夹 并且只有 debug APK 顺便说一句 我使用这个命令 phonegap
  • 对超类方法的调用应该是第一个语句吗?

    语音识别的结果可以在onActivityResult int requestCode int resultCode Intent data 方法 如图这个例子 http developer android com resources sam
  • Android 反向地理编码不适用于华为设备

    我正在尝试通过这段代码反转地理编码纬度 经度 Geocoder geocoder new Geocoder context Locale ENGLISH try List
  • 免费和付费版本 Android 应用程序的最佳方法?

    我开发了一个 Android 应用程序 我希望它可以作为免费版本和付费版本提供 最好的方法是什么 我可以想到三种解决方案 将项目分成两个分支并维护它们 创建一个库项目并有两个附加项目 一个 免费 版本和一个 付费 版本 使用应用内结算 问
  • 如何在具有多种字体大小的 TextView 中调整行高?

    我有一个包含 Spannable 字符串的 TextView 该字符串包含一堆文本 其中第一个单词的字体大小是字符串其余部分的两倍 问题在于 由于第一个字的大小增加 第一行和第二行之间的行间距比后续行之间的行间距大得多 http img s
  • 如何在 Android 上通过 RTMP 进行流式传输?

    我正在尝试在远程服务器上播放视频文件 视频格式为flv 服务器为Flash Media Server3 5 我将通过 RTMP 连接到服务器并使用 Android Media Player 实现视频文件的播放 真的吗 可能吗 任何帮助都是我
  • Osmdroid:如何从我自己的位图(图块)创建和加载地图?

    我开始使用 Osmdroid 我想使用这项技术来显示有关 F1 赛道的地图 我有一张大图片 我可以将它切割成更小的图块 我可以修改osmdroid库来上传这些图片吗 我想将这些位图 图块 保存在我的资产文件夹中 我非常不知道如何做到这一点
  • 如果我清理了反向引用,我是否可以观察 ViewModel?

    建议的实施方式ViewModel是通过使用来公开变化的数据LiveData活动 片段和视图的对象 有一些情况 当LiveData不是一个理想的答案或根本没有答案 自然的选择是将观察者模式应用于ViewModel 使其成为可观察的 注册观察员
  • Android SSH 示例代码

    我想创建一个 android 活动 用于与远程设备 通过 Wifi 建立 SSH 会话并在远程设备上执行一些 Linux 命令 任何人都可以获得一个快速 简短的示例 用于使用以下命令进行连接 身份验证和发送远程命令Trilead https
  • Xamarin.Android JmDNS 绑定问题

    我开始研究 Xamarin Android 的 JmDNS 绑定 我设法构建了绑定 但无法从代码中引用它 https github com ytn3rd monodroid bindings tree master JmDNS https
  • Pebble 应用程序设置中的开发人员选项

    I am following this manual http www sitepoint com pebble watch development javascript and I don t have the Developers Op
  • 在 Android 中将列表传递给另一个 Activity

    我已经创建了一个列表 并希望将该列表传递给另一个活动 但当我创建意图时 我在 putExtra 语句上收到错误 只是想知道是否有任何简单的方法来传递字符串列表而不是单个字符串 Thanks private List
  • 什么是 Android DecorView?

    http developer android com reference android view Window html getDecorView http developer android com reference android

随机推荐

  • ❀OSPF协议面试题总结❀

    文章目录 一 简单介绍下ospf 二 ospf的骨干区域有什么用 为什么要划分一个骨干区域 三 ospf的状态机 四 ospf的lsa有几种 五 ospf路由的生成过程 六 介绍ospf的虚链路 一 简单介绍下ospf 开放式最短路径优先协
  • 力扣2594.修车的最少时间

    题目描述 给你一个整数数组 ranks 表示一些机械工的 能力值 ranksi 是第 i 位机械工的能力值 能力值为 r 的机械工可以在 r n2 分钟内修好 n 辆车 同时给你一个整数 cars 表示总共需要修理的汽车数目 请你返回修理所
  • Android ApiDemos示例解析(87):Media->MediaPlayer

    本例介绍了如何使用MediaPlayer类来播放声音或是视频 涉及的Activity有三个 MediaPlayerDemo 主Activity 显示示例列表 MediaPlayerDemo Audio 子Activity 用于播放声音 在L
  • ConcurrentHashMap1.8总结

    Java8 ConcurrentHashMap结构基本上和Java8的HashMap一样 不过保证线程安全性 在JDK8中ConcurrentHashMap的结构 由于引入了红黑树 使得ConcurrentHashMap的实现非常复杂 我们
  • FPGA学习专栏-串口通信(xinlinx)

    FPGA学习专栏 串口通信 本系列文章基于开发板黑金A309 FPGA芯片为Xilinx公司的spartan6 本系列文章记录FPGA学习历程 文章目录 FPGA学习专栏 串口通信 一 串口通信原理 二 硬件设计 三 verilog代码编写
  • react之装饰器报错:This experimental syntax requires enabling one of the following parser plugin(s): “decor

    在学习mobx时 遇到了 This experimental syntax requires enabling one of the following parser plugin s decorators decorators legac
  • STM32F103ZET6【标准库函数开发】------17 DMA实验

    STM32F103ZET6有2个DMA控制器 DMA1有7个通道 DMA2有5个通道 各个通道对应的外设如下
  • Caused by: org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed解决方法

    前言 Android Studio 升级到最新版本后 构建项目时 构建失败 出现错误 Caused by org codehaus groovy control MultipleCompilationErrorsException star
  • MySQL的null与not null

    相信很多用了mysql很久的人 对这两个字段属性的概念还不是很清楚 一般会有以下疑问 我字段类型是not null 为什么我可以插入空值 为毛not null的效率比null高 判断字段不为空的时候 到底要 select from tabl
  • IDEA报错:Cannot start compilation: the output path is not specified for module “testSvnKit“.Specify th

    IDEA报错Cannot start compilation the output path is not specified for module testSvnKit Specify the output path in the Pro
  • List分组的两种方式

    java8之前List分组 假设有个student类 有id name score属性 list集合中存放所有学生信息 现在要根据学生姓名进行分组 public Map
  • 精确径向基(matlab工具箱)

    原文地址 精确径向基 matlab工具箱 作者 神经网络之家 作者 梁小h 日期 2015 10 26 09 30 47 0 lt 文档仅供查阅和简单了解 深入了解请关注神经网络之家发布的 神经网络教学视频 gt 精确径向基神经网络在mat
  • HTML详解连载(5)

    HTML详解连载 5 专栏链接 link http t csdn cn xF0H3 下面进行专栏介绍 开始喽 行高 设置多行文本的间距 属性名 属性值 行高的测量方法 行高 垂直居中 技巧 字体族 属性名 属性值 示例 扩展 font 复合
  • 学期总结-2018年上

    从现在开始 我需要养成一个写作的好习惯 之所以培养这个习惯 是因为 我开始发现我的一个重大缺陷 语言表达能力的欠缺 这种能力 在一般生活中并不会有太大的作用 而且很多时候 大部分人都体会不到其所带来的 破坏 这种破坏 会让你的交际陷入阻塞
  • 不能向服务器考文件,如何往云服务器考文件

    如何往云服务器考文件 内容精选 换一换 华为云帮助中心 为用户提供产品简介 价格说明 购买指南 用户指南 API参考 最佳实践 常见问题 视频帮助等技术文档 帮助您快速上手使用华为云服务 无法正常使用Cloud init 弹性云服务器获取M
  • 关于python爬虫逆向RPC的基础使用

    makeRequest function a b c d rpc使用的代码 function 防止重复创建websocket if window flagLX else window weiboLX makeRequest var ws n
  • egg初始化搭建swagger项目

    步骤 安装node 安装你喜欢的编辑器 初始化项目 输入安装 egg 命令 输入安装 egg dev 命令 修改 package json 基本目录结构 需手动创建 输入安装 egg sequelize 命令 数据库选择 配置 sequel
  • Android移动开发-调用摄像头进行拍照的实现

    现在Android智能手机的像素都会提供照相的功能 大部分的手机的摄像头的像素都在1000万以上的像素 有的甚至会更高 它们大多都会支持光学变焦 曝光以及快门等等 下面的程序Demo实例示范了使用Camera v2来进行拍照 当用户按下拍照
  • Windows修改MySQL数据库密码(修改或忘记密码)

    今天练习远程访问数据库时 为了方便访问 就想着把数据库密码改为统一的 以后我们也会经常遇到MySQL需要修改密码的情况 比如密码太简单 忘记密码等等 在这里我就借鉴其他人的方法总结几种修改MySQL密码的方法 我就以实际操作修改root密码
  • Android:安卓学习笔记之MVP模式的简单理解和使用

    Android MVP模式的简单理解和使用 MVP模式 1 为什么使用MVP模式 1 1 实例说明 2 一步步让你理解MVP 2 1 MVP实现第一步 将页面拆分为M V P三个模块 2 2 MVP实现第2步 使用接口通信 进一步解耦 2