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

Posted JMW1407

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android:安卓学习笔记之MVP模式的简单理解和使用相关的知识,希望对你有一定的参考价值。

android MVP模式的简单理解和使用

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);
        ...
Android:安卓学习笔记之MVP模式的简单理解和使用

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

Android:安卓学习笔记之共享元素的简单理解和使用

Android:安卓学习笔记之共享元素的简单理解和使用

Android:安卓学习笔记之共享元素的简单理解和使用

Android :安卓学习笔记之事件内存泄露 的简单理解