Android中MVP设计模式

前言

开始写这篇这前确实挺纠结的,纠结到底要不要写。一个是这种设计模式的内容网上都已经很多了,都是前辈写的,我写的可能也不如他们好。二是设计模式这种东西很大,高大上的原理就够你喝一壶的了,光一篇博客想要理解还是挺难的。纠结到最后还是决定写了,就当给自己这几天的学习做下笔记,也省的自己再去纠结了,行动就是最好的解决办法。

原理

MVP是有MVC演变而来的,因为在Android中用MVC来写的时候,Activity到底是View层还是Controller层真的是傻傻分不去清楚,所以基本它就把这两层的活都自己一个人干了,这样一来Activity上的代码量会非常大,Controller层和View层没有实现解耦分离开来,如果后面这两层代码其中一层出了问题你就可能两层都的修改,代码是没什么重用性可言的。而MVP的出现就是为了解决这些问题的,MVP模式将Controller的工作抽取出来交给Presenter层,Presenter层负责控制处理业务逻辑,作为中间层建立起Model层和View层的联系,从而实现三层交互。这么说确实挺难让人理解的,下面就通过一个例子,在例子中夹杂着解释让我们掌握MVP的用法吧。


MVP.png
  • 这个图很简单,当 View 需要更新数据时,首先去找 Presenter,然后 Presenter 去找 Model 请求数据,Model 获取到数据之后通知 Presenter,Presenter 再通知 View 更新数据,这样 Model 和 View 就不会直接交互了,所有的交互都由 Presenter 进行,Presenter 充当了桥梁的角色。很显然,Presenter 必须同时持有 View 和 Model 的对象的引用,才能在它们之间进行通信。

例子

  • 该例子就是一个模拟登陆的例子,一看效果图你就立马明白了,效果图如下:


    mvp.gif
Model层
  • 创建用于保存用户信息的实体类User
public class User {
    private String username;
    private String password;

    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}
  • 结果回调接口
public interface OnLoginListener {
    //登录成功的回调
    void loginSuccess(User user);
    //登录失败的回调
    void loginFailed();
}
  • 定义业务接口,这里抽出一个接口的目的是为了降低一层耦合和便于复用。比如同一个网络请求业务你可以有OkHttp的实现,同时也可以有Retrofit的实现,这样一来就可以方便的做到网络框架的替换,当然你还可以有不同内容的实现。
public interface IUserBiz {
    //登录方法
    public void login(String username, String password, OnLoginListener loginListener);
}
  • 具体Model的实现类
public class UserBiz implements IUserBiz {
    @Override
    public void login(final String username, final String password, final OnLoginListener loginListener) {
        //模拟网络请求耗时操作
        new Thread() {
            @Override
            public void run() {
                //模拟了耗时                
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //模拟登录成功
                if ("luoqiang".equals(username) && "108".equals(password)) {
                    User user = new User();
                    user.setUsername(username);
                    user.setPassword(password);
                    loginListener.loginSuccess(user);
                } else {
                    loginListener.loginFailed();
                }
            }
        }.start();
    }
}
View层
  • 定义View层接口,View层不关心数据,不关心逻辑处理!只关心和用户的交互,那么这个登录界面应该有的操作就是:从输入框获取用户名,获取密码,清除用户名,清除密码,登录网络请求时显示进度条,隐藏进度条,登录成功跳转到对应界面,登录失败提示。接下来定义接口如下:
public interface IUserLoginView {
    String getUserName();
    String getPassword();
    void clearUserName();
    void clearPassword();
    void showLoading();
    void hideLoading();
    void toMainActivity(User user);
    void showFaileTips();
}
  • 为什么要写一个 View 的接口,直接把 Activity 本身传入到 Presenter 层不行吗?这当然是可行的,这里使用接口的目的和Model层使用接口的目的是一样的,主要是为了代码的复用,试想一下,如果直接传入 Activity,那么这个 Presenter 就只能为这一个 Activity 服务。举个例子,假设有个 App 已经开发完成了,可以在手机上正常使用,现在要求做平板上的适配,在平板上的界面显示效果有所变化,TextView 并不是直接在 Activity 中的,而是在 Fragment 里面,如果没有使用 View 的接口的话,那就需要再写一个针对 Fragment 的 Presenter,然后把整个过程再来一遍。但是使用 View 的接口就很简单了,直接让 Fragment 实现这个接口,然后复写接口里面的方法,Presenter 和 Model 层都不需要做任何改动。
  • View的具体实现类,其实就是Activity。这里需要注意内存泄漏的风险。试想一下,如果在点击登录按钮之后,Model 获取到数据之前,退出了 Activity,此时由于 Activity 被 Presenter 引用,而 Presenter 正在进行耗时操作,会导致 Activity 的对象无法被回收,造成了内存泄漏,解决的方式很简单,在 Activity 退出的时候,把 Presenter 中 View 的引用置为空即可。
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Toast;

public class UserLoginActivity extends AppCompatActivity implements IUserLoginView {

    private EditText mEtUsername, mEtPassword;
    private Button mBtnLogin, mBtnClear;
    private ProgressBar mPbLoading;

    private UserLoginPresenter mUserLoginPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mUserLoginPresenter = new UserLoginPresenter(this);
        initViews();
    }

    private void initViews() {
        mEtUsername = (EditText) findViewById(R.id.et_username);
        mEtPassword = (EditText) findViewById(R.id.et_password);
        mBtnClear = (Button) findViewById(R.id.btn_clear);
        mBtnLogin = (Button) findViewById(R.id.btn_login);
        mPbLoading = (ProgressBar) findViewById(R.id.pb_loading);
        mBtnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mUserLoginPresenter.login();
            }
        });
        mBtnClear.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mUserLoginPresenter.clear();
            }
        });
    }

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

    @Override
    public String getPassword() {
        return mEtPassword.getText().toString();
    }

    @Override
    public void clearUserName() {
        mEtUsername.setText("");
    }

    @Override
    public void clearPassword() {
        mEtPassword.setText("");
    }

    @Override
    public void showLoading() {
        mPbLoading.setVisibility(View.VISIBLE);
    }

    @Override
    public void hideLoading() {
        mPbLoading.setVisibility(View.GONE);
    }

    @Override
    public void toMainActivity(User user) {
        Toast.makeText(this, "跳转到登录成功页面", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void showFaileTips() {
        Toast.makeText(this, "登录失败", Toast.LENGTH_SHORT).show();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //为了防止内存泄漏,解绑Presenter层对View层的引用
        mUserLoginPresenter.detachView();
    }
}

对于在Activity中实现我们上述定义的接口,是一件很容易的事,毕竟接口引导我们去完成。

Presenter层
  • Presenter的作用就是从View层获取用户的输入,传递到Model层进行处理,然后回调给View层,输出给用户!
import android.os.Handler;

public class UserLoginPresenter {
    private IUserBiz userBiz;
    private IUserLoginView userLoginView;
    private Handler mHandler = new Handler();
    //对应视图页面销毁的标志位,当视图销毁后回调就不需要处理了
    private boolean destroyFlag;

    //Presenter必须要能拿到View和Model的实现类
    public UserLoginPresenter(IUserLoginView userLoginView) {
        this.userLoginView = userLoginView;
        this.userBiz = new UserBiz();
    }

    public void login() {
        userLoginView.showLoading();
        userBiz.login(userLoginView.getUserName(), userLoginView.getPassword(), new OnLoginListener() {
            @Override
            public void loginSuccess(final User user) {
                if (!destroyFlag) { //View层销毁后不需要处理的判断
                    //需要在UI线程执行
                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            userLoginView.toMainActivity(user);
                            userLoginView.hideLoading();
                        }
                    });
                }
            }

            @Override
            public void loginFailed() {
                if (!destroyFlag) { //View层销毁后不需要处理的判断
                    //需要在UI线程执行
                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            userLoginView.showFaileTips();
                            userLoginView.hideLoading();
                        }
                    });
                }
            }
        });
    }

    public void clear() {
        userLoginView.clearUserName();
        userLoginView.clearPassword();
    }

    //解绑视图
    public void detachView() {
        destroyFlag = true;
        this.userLoginView = null;
    }
}
  • presenter完成二者的交互,那么肯定需要二者的实现类。大致就是从View中获取需要的参数,交给Model去执行业务方法,执行的过程中需要的反馈,以及结果,再让View进行做对应的显示。整个过程中,View 和 Model 并没有直接交互,所有的交互都是在 Presenter 中进行的。

感谢

谈谈我理解的Android应用架构
浅谈 MVP in Android
一个小例子彻底搞懂 MVP
MVC、MVP、MVVM,我到底该怎么选?
爱吖妹纸(含 Kotlin 分支版本)——Retrofit + RxJava + MVP 架构 APP 体验代码家的干货集中营 Gank.io,福利多多,不容错过
如何构建Android MVVM 应用框架

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容