android:MVP架构模式的优雅封装

简介

关于Android程序的构架, 主流的不外乎以下几种:MVC、MVP和MVVM。
MVC:相对于较为落后,耦合度太高、职责不明确,不易于维护。
MVVM:使用DataBinding,普及性不如MVP。

此外,Google官方提供了Sample代码来展示MVP模式的用法,因此主流还是选择MVP架构。

因本文主要讲的是MVP模式的优雅封装,MVVM模式在此就不作赘述,后续文章会讲到。

MVC:

提到MVP就不得不提到MVC,关于MVC架构,可以看下面这张图

image.png
MVC工作原理:

MVC即Model View Controller,简单来说就是通过controller的控制去操作model层的数据,并且返回给view层展示,具体见上图。当用户出发事件的时候,view层会发送指令到controller层,接着controller去通知model层更新数据,model层更新完数据以后直接显示在view层上,这就是MVC的工作原理。

这种原理就会造成一个一个致命的缺陷:当我们把很多业务逻辑写在activity中时,activity既充当了View层,又充当了Controller层。因此,耦合性极高,各种业务逻辑代码和View代码混合在一起你中有我我中有你,如果要修改一个需求,改动的地方可能相当多,维护起来十分不便。

作为一个追求优雅的程序猿,这种架构必然要被抛弃。

MVP:

image.png
概念

MVP即Model、View、Presenter
View:负责视图部分展示、视图事件处理。Activity、Fragment、Dialog、ViewGroup等呈现视图的组件都可以承担该角色。
Model:负责数据的请求、解析、过滤等数据层操作。
Presenter:View和Model交互的桥梁。

优势

单一职责
Model、View、Presenter只处理某一类逻辑

解耦

Model层修改和View层修改互不影响
面向接口编程,依赖抽象
Presenter和View互相持有抽象引用,对外隐藏内部实现细节

可能存在的问题

1、Model进行异步操作,获取结果通过Presenter回传到View时,出现View引用的空指针异常
2、Presenter和View互相持有引用,解除不及时造成的内存泄漏。

因此,在进行MVP架构设计时需要考虑Presenter对View进行回传时,View是否为空?
Presenter与View何时解除引用即Presenter能否和View层进行生命周期同步?

好了,说了这么多废话,总之一句话,MVP好。下面我们来看看具体如何优雅的实现MVP的封装。

MVP架构优雅的封装

1、首先,我们定义一个BaseView

/**
 * 视图基类
 */
public interface BaseView {
    
}

上面说过,

如果Presenter与View不及时解除引用关系,那么内存泄漏乃至内存溢出就是必然。

具体来说,

当Presenter对象持有一个或多个大型Activity的引用,如果该对象(P)不能被系统回收,那么当这些Activity不再使用时,这个Activity也不会被系统回收,这样一来便出现了内存泄漏的情况。在应用中内出现一次两次的内存泄漏或许不会出现什么影响,但是在应用长时间使用以后,若是这些占据大量内存的Activity无法被GC回收的话,最终会导致OOM的出现,就会直接Crash应用。

我们当然不会坐视这种情况的发生,解决的思路就是,

我们将Presenter的生命周期和View层的生命周期绑定在一起,给Presenter定义两个方法,一个绑定View层,一个解绑View层,在需要的时候进行绑定,不需要的时候进行解绑就可以了。

于是就有了下面这个定义。

2、将Presenter的生命周期和View层的生命周期绑定

/**
 * 控制器接口:
 * 定义P层生命周期与 V层同步
 */
public interface IPresenter<V extends BaseView> {

    void onMvpAttachView(V view, Bundle savedInstanceState);

    void onMvpStart();

    void onMvpResume();

    void onMvpPause();

    void onMvpStop();

    void onMvpSaveInstanceState(Bundle savedInstanceState);

    void onMvpDetachView(boolean retainInstance);

    void onMvpDestroy();
}

为了代码的优雅性,我们对它进行一次封装

/**
 * 控制器基类:
 * Presenter生命周期包装、View的绑定和解除,P层实现的基类
 */
public class BasePresenter<V extends BaseView> implements IPresenter<V> {

    private WeakReference<V> viewRef;

    protected V getView() {
        return viewRef.get();
    }

    protected boolean isViewAttached() {
        return viewRef != null && viewRef.get() != null;
    }

    private void _attach(V view, Bundle savedInstanceState) {
        viewRef = new WeakReference<V>(view);
    }

    @Override
    public void onMvpAttachView(V view, Bundle savedInstanceState) {
        _attach(view, savedInstanceState);
    }

    @Override
    public void onMvpStart() {

    }

    @Override
    public void onMvpResume() {

    }

    @Override
    public void onMvpPause() {

    }

    @Override
    public void onMvpStop() {

    }

    @Override
    public void onMvpSaveInstanceState(Bundle savedInstanceState) {

    }

    private void _detach(boolean retainInstance) {
        if (viewRef != null) {
            viewRef.clear();
            viewRef = null;
        }
    }

    @Override
    public void onMvpDetachView(boolean retainInstance) {
        _detach(retainInstance);
    }

    @Override
    public void onMvpDestroy() {

    }
}

3、对于View层,我们一般都会写一个BaseActivity

/**
 * @description 在此类中添加自己的基类功能
 */

 public class BaseActivity extends FragmentActivity {

   protected void openActivity(String action) {
          openActivity(action, null);
   }

   public void showEnsureDialog(String message) {
       
        
   }
}

4、我们再写一个绑定生命周期的BaseMvpActivity包装类

/**
 * MVP的Activity基类:
 * 纯粹的 MVP 包装,不要增加任何View层基础功能
 * 如果要添加基类功能,请在{@link BaseActivity} 中添加
 */
public abstract class BaseMvpActivity<P extends IPresenter> extends BaseActivity implements BaseView {

    protected P mPresenter;

    protected abstract P createPresenter();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mPresenter = createPresenter();
        if (mPresenter == null) {
            throw new NullPointerException("Presenter is null! Do you return null in createPresenter()?");
        }
        mPresenter.onMvpAttachView(this, savedInstanceState);
    }

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

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

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

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

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        if (mPresenter != null) {
            mPresenter.onMvpSaveInstanceState(outState);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mPresenter != null) {
            mPresenter.onMvpDetachView(false);
            mPresenter.onMvpDestroy();
        }
    }

}

5、我们以登录为例,定义一个契约类

/**
 * 契约接口类:
 * P层与 V层接口定义
 */
public class LoginContract {

    public interface ILoginView extends BaseView {
        /**
         * 登录成功
         */

        void LoginSuccess();

        /**
         * 登录失败
         *
         * @param msg
         */
        void LoginFailed(String msg);
    }

    public interface ILoginPresenter extends IPresenter<ILoginView> {

        /**
         * 登录
         */
        void login(String username, String password);
    }
}

6、我们再定义一个登录的Presenter的实现类,在这个类中,完成互相访问。

/**
 * 控制器实现类
 */
public class LoginPresenterImpl extends BasePresenter<LoginContract.ILoginView> implements LoginContract.ILoginPresenter {

    @Override
    public void login(String username, String password) {
        //先进行非空判断
        if (isViewAttached()) {
            handleLogin(getView(), username, password);
        }
    }

    private void handleLogin(LoginContract.ILoginView view, String username, String password) {
        if (username.isEmpty() || password.isEmpty()) {
            view.LoginFailed("账号和密码不能为空");
        } else if (password.length() < 6 || password.length() > 20) {
            view.LoginFailed("密码须在6-20位之间");
        } else {
            if (username.equals("mvp")) {
                if (password.equals("123456")) {
                    view.LoginSuccess();
                } else {
                    view.LoginFailed("密码错误");
                }
            } else {
                view.LoginFailed("用户名错误");
            }
        }
    }

    @Override
    public void onMvpAttachView(LoginContract.ILoginView view, Bundle savedInstanceState) {
        super.onMvpAttachView(view, savedInstanceState);
    }

    /**
     * 重写P层需要的生命周期,进行相关逻辑操作
     */
    @Override
    public void onMvpResume() {
        super.onMvpResume();
    }

}

7、到这里,我们的封装基本完成,我们现在来看看我们的LoginActivity是怎么样的。

public class LoginActivity extends BaseMvpActivity<LoginContract.ILoginPresenter> implements LoginContract.ILoginView {
    @BindView(R.id.et_username)
    EditText etUsername;
    @BindView(R.id.et_password)
    EditText etPassword;
    @BindView(R.id.btn_login)
    Button btnLogin;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvp);
        ButterKnife.bind(this);
    }

    @Override
    protected LoginContract.ILoginPresenter createPresenter() {
        return new LoginPresenterImpl();
    }

    @OnClick({R.id.btn_login})
    public void onViewClicked(View view) {
        switch (view.getId()) {
            case R.id.btn_login:
                String username = etUsername.getText().toString().trim();
                String password = etPassword.getText().toString().trim();
                mPresenter.login(username, password);
                break;
        }
    }

    @Override
    public void LoginSuccess() {
        Toast.makeText(this, "LoginSuccess", Toast.LENGTH_SHORT).show();
    }

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

这样,是不是看起来,清爽多了。

8、最后,总结:

Mvp模式很好的将View层和Presenter解耦,View层和Presenter层的修改互不影响,并且符合软件设计原则之单一职责,提高了代码的灵活性和可扩展性。最后二者之间通过抽象进行关联,使之可以互相访问。
从MVC到最简单的MVP架构,我们解决了MVC的数据层和视图层耦合的问题;随之而来的是内存泄露的问题,通过设置对应的绑定解绑方法来解决这个问题;之后又是代码冗余的问题,于是利用Java的多态性,我们将重复性工作交由基类去完成,子类继承基类重写对应方法即可。而实际上我们只需要修改上面Presenter中的构造代码,不需要在构造中传递V层了,然后再写一个绑定和解绑的方法,最后修改Activity创建Presenter时进行绑定,在onDestroy中进行解绑。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容