从0到1搭建MVP框架

相信大多处在Android进阶阶段的朋友都了解过Android框架方面的知识,要开发一款优秀的app,自然少不了优秀的应用框架。好的框架能够让你的代码变得更加简洁易读,也更有利于后续开发和维护。

MVP框架即Model、View、Presenter,其优点是将View和Model解耦,在View层只需要执行Presenter提供的请求数据的方法,并给Presenter提供一个更新视图的方法;Presnter执行请求方法,从Model层获取需要的数据,处理完成后就通知View层更新视图(如果需要的话)。这其中View完全没有与Model交互,仅仅只是发送请求,请求之后的具体流程View层一概不知。这样做的好处一是让View层的代码更加清晰,因为只剩下UI方面的业务逻辑代码;二是当Model层发生变动时,仅需修改Presenter的请求逻辑,而View层不用作出改变。

关于MVP框架理论方面的知识网上有很多,大多都异曲同工,本质上都是讲它的分层解耦,在此就不再赘述。然而在实际应用时,可能部分MVP框架的初学者就会变得云里雾里——怎么每个人的MVP框架实现都不一样呢?

其实,MVP框架只是一种将业务逻辑代码分层解耦的概念,若只是简单的将每个功能模块的代码都分割成Model、View和Presenter三个部分,那项目将会变得十分庞大且出现大量的冗余代码,这就与使用MVP框架的初衷背道而驰了。所以,在构建MVP框架时就要在分层的基础上对代码再进行封装和抽象,让代码变得更加清晰易懂,而这些不同的封装抽象思路,就造就了多种多样的MVP框架实现。

下面,我们就亲自动手来实现一个MVP框架吧

1、MVP初建

从最简单的例子着手,如图所示,Activity中有一个TextView和一个Button,点击Button开始请求数据,数据返回后展示在TextView中

image

首先分析业务层次,View层即Activity,Model层是getData的数据源,Presenter连接View和Model,在Presenter中实现getData的请求逻辑,并将返回结果通知给View更新TextView

写出最基础的雏形,就是最基本的MVP框架

我们以分层结构建包,如果项目中的功能模块很多的话,可以以功能模块来建包

初建MVP包结构

model层包括一个MyModel类和一个Callback回调接口,这里需要注意遵守面型对象设计的原则(如果不了解的同学建议去了解一下,可能让你对代码的读写产生新的理解),从Model获取数据时,不能直接在Model中获取数据的方法内返回数据,必须通过接口回调的方式将数据传给Presenter层,这样做可以降低层次间的耦合。

Model层代码

/**

* 数据请求回调接口

*/

public interface Callback {

    void onSuccess(String data);

    void onError(String errorMsg);

}
public class MyModel {

    public static void getData(final String token, final Callback callback) {
        //不要在主线程执行耗时操作
        new Handler().post(new Runnable() {
            @Override
            public void run() {
                switch (token) {
                    case "local":
                        getLocalData(callback);
                        break;
                    case "net":
                        getNetData(callback);
                        break;
                }
            }
        });
    }

    private static void getLocalData(Callback callback) {
        try {
            Thread.sleep(1000);
            if (new Random().nextBoolean()) {
                callback.onSuccess("This is local data#" + new Random().nextInt(100));
            }else {
                //模拟本地数据不存在
                callback.onError("local data not found!");
            }
        } catch (InterruptedException e) {
            callback.onError(e.getMessage());
        }
    }

    private static void getNetData(Callback callback) {
        try {
            Thread.sleep(1500);
            if (new Random().nextBoolean()) {
                callback.onSuccess("This is Internet data#" + new Random().nextInt(100));
            }else {
                //模拟网络请求失败
                callback.onError("request internet data failed!");
            }
        } catch (InterruptedException e) {
            callback.onError(e.getMessage());
        }
    }
}

这里的model层代码简单模拟了从本地获取数据和从网络获取数据,两者都是耗时操作且都可能报错,调用获取数据的方法需要传入一个Callback回调接口,以便通知请求结果。

View层

public interface IMyView {
    void updateText(String text);

    void showToast(String msg);
}

View层需要对Presenter提供一个操作对象,用于通知其更新视图,这里也要用接口来实现

public class MainActivity extends AppCompatActivity implements IMyView {

    private MyPresenter mPresenter;

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

        mPresenter = new MyPresenter(this);

        Button btn = findViewById(R.id.button);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //请求数据
                mPresenter.getData();
            }
        });
    }

    @Override
    public void updateText(String text) {
        ((TextView)findViewById(R.id.text)).setText(text);
    }

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

可以看到,在View层中,我们除了创建了一个Presenter对象并调用其请求数据的方法,其余就只剩UI相关的操作了

Presenter层

public class MyPresenter {

    //View层操作对象
    private IMyView mView;

    //创建Presenter时绑定View
    public MyPresenter(IMyView view){
        mView = view;
    }

    /**
     * 请求数据的方法
     */
    public void getData(){
        mView.updateText("I'm working, please wait...");
        MyModel.getData("local", new Callback() {
            @Override
            public void onSuccess(String data) {
                mView.updateText(data);
            }

            @Override
            public void onError(String errorMsg) {
                mView.showToast(errorMsg);
                mView.updateText("get data failed!");
            }
        });
    }
}

运行结果


开始执行
获取数据成功
获取数据失败

执行结果与代码逻辑一致,运行正常,到这里我们就完成了一个最基本的MVP框架了,然而,我们仔细观察就会发现,这个框架还有不少问题:
1、Presenter通知视图更新需要调用View的方法,但是并不知道View的生命周期,由于请求数据是一项耗时的操作,很可能在执行请求时View已经销毁了,造成通知UI更新时发生空指针异常
2、Model的请求回调接口Callback具有很强的针对性(onSuccess只返回一个字符串类型的数据),每当需要获取一种新类型的数据,就要创建一个新的回调接口

让我们先来解决这两个问题

2、第一次修改

第一个问题,我们需要让Presenter绑定View的生命周期,在通知视图更新时判断mView是否为空,修改Presenter的代码

public class MyPresenter {

    //View层操作对象
    private IMyView mView;

    //创建Presenter时不绑定View
    public MyPresenter() {

    }

    /**
     * 绑定View,一般在View的onCreate或onResume中调用
     *
     * @param view
     */
    public void attachView(IMyView view) {
        mView = view;
    }

    /**
     * 解绑View,一般在View的onDestroy或onStop中调用
     */
    public void detach() {
        mView = null;
    }

    /**
     * 判断View对象是否存在
     *
     * @return
     */
    private boolean isViewAttached() {
        return mView != null;
    }

    /**
     * 封装更新视图的方法,增加空值判断
     *
     * @param text
     */
    private void updateText(String text) {
        if (isViewAttached()) {
            mView.updateText(text);
        }
    }

    private void showToast(String msg) {
        if (isViewAttached()) {
            mView.showToast(msg);
        }
    }

    /**
     * 请求数据的方法
     */
    public void getData() {
        updateText("I'm working, please wait...");
        MyModel.getData("local", new Callback<String>() {
            @Override
            public void onSuccess(String... data) {
                updateText(data[0]);
            }

            @Override
            public void onError(String errorMsg) {
                showToast(errorMsg);
                updateText("get data failed!");
            }
        });
    }
}

修改后的Presenter不需要在构造函数中传入View对象,而是在View中自由地通过Presenter的attachView方法和detachView方法绑定和解绑View对象,除了attachView和detachView,我们还可以另外声明onResume和onStop方法,这样更加贴近View的声明周期,具体需要绑定哪些声明周期,则视需求而定了,这里仅绑定onCreate和onDestroy的生命周期。
同样Activity的代码也作出修改

public class MainActivity extends AppCompatActivity implements IMyView {

    private MyPresenter mPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        mPresenter = new MyPresenter();
        //绑定View
        mPresenter.attachView(this);

        Button btn = findViewById(R.id.button);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //请求数据
                mPresenter.getData();
            }
        });
    }
......
    @Override
    protected void onDestroy() {
        //解绑View
        mPresenter.detach();
        super.onDestroy();
    }
}

在绑定了生命周期后,我们已经解决了通知视图更新的空指针异常问题,而另一个请求回调接口则更好解决了,修改Callback的代码

/**
 * 数据请求回调接口
 */
public interface Callback<T> {
    /**
     * 请求数据成功
     *
     * @param data 返回的数据,数量不定
     */
    void onSuccess(T... data);

    /**
     * 请求失败
     *
     * @param errorMsg 错误信息
     */
    void onError(String errorMsg);
}

使用泛型代替了原本指定的String类型的数据,并且把单个参数变为了数组型参数,这样数据回传的类型和数量都没有限制了。

当然仅仅做到这样还是不够的,再查看和思考现有代码可以发现,我们只有一个View和一个Presenter时,需要写一次绑定生命周期的操作,但是如果有多个时,我们就要写多次绑定操作,如此一来就产生了大量的冗余代码。所以,我们可以把Presetner和View中的基础操作提取出来,封装到一个基础类当中去。

3、第二次修改

新建IBaseView接口,在接口中声明基础方法(也可以让IBaseView为空,仅作标记用)

public interface IBaseView {
    void showToast(String msg);
}

修改IMyView接口继承自IBaseView

public interface IMyView extends IBaseView {
    void updateText(String text);
}

新建BasePresenter类,将View的类型设为泛型,封装绑定View生命周期的方法

public abstract class BasePresenter<T extends IBaseView> {

    protected T mView;

    public void attachView(T view){
        mView = view;
    }

    public void detachView(){
        mView = null;
    }

    protected boolean isViewAttached(){
        return mView != null;
    }
}

新建BaseActivity类,封装基础方法以及绑定生命周期,将绑定的Presenter的类型设置为泛型

public abstract class BaseActivity<T> extends AppCompatActivity implements IBaseView {

    protected T mPresenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mPresenter = createPresenter();
        //绑定生命周期
        if (mPresenter != null){
            mPresenter.attachView(this);
        }
    }

    //子类必须重写绑定Presenter的方法(如果View不需要绑定Presenter,就不会继承这个类)
    protected abstract T createPresenter();

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

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

修改MyPresenter的代码,继承自BasePresenter,泛型为IMyView

public class MyPresenter extends BasePresenter<IMyView> {

    /**
     * 封装更新视图的方法,增加空值判断
     *
     * @param text
     */
    private void updateText(String text) {
        if (isViewAttached()) {
            mView.updateText(text);
        }
    }

    private void showToast(String msg) {
        if (isViewAttached()) {
            mView.showToast(msg);
        }
    }

    /**
     * 请求数据的方法
     */
    public void getData() {
        updateText("I'm working, please wait...");
        MyModel.getData("local", new Callback<String>() {
            @Override
            public void onSuccess(String... data) {
                updateText(data[0]);
            }

            @Override
            public void onError(String errorMsg) {
                showToast(errorMsg);
                updateText("get data failed!");
            }
        });
    }
}

修改MainActivity的代码,继承自BaseActivity,泛型为MyPresenter

public class MainActivity extends BaseActivity<MyPresenter> implements IMyView {

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

        Button btn = findViewById(R.id.button);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //请求数据
                mPresenter.getData();
            }
        });
    }

    @Override
    protected MyPresenter createPresenter() {
        return new MyPresenter();
    }

    @Override
    public void updateText(String text) {
        ((TextView)findViewById(R.id.text)).setText(text);
    }
}

通过代码可以很直观的看出来,经过一层封装之后,MyPresenter和MainActivity的代码都变得简洁了许多,基础的操作交由基类处理,具体功能模块的类只需要负责相关的业务逻辑,这样代码又变得清晰了。

经过两次修改,这个MVP框架已经基本可以使用了,如果还需要再进行优化,可以针对Model层进行优化,比如增加一个Model层管理类,统一管理Model,这里由于我们没有真正的数据源,因此没有进一步优化。

关于MVP框架,还有很多各种各样的思路来实现,谷歌官方也推出了自己的MVP框架案例todo-mvp,大家也可以参考官方的实现方式来继续优化,项目地址:https://github.com/googlesamples/android-architecture
除了谷歌官方以外,也可以参考一些第三方的实现思路,比如TheMVP,使用Activity作为Presenter层来处理代码逻辑,在Activity中包含一个ViewDelegate对象来间接操作View层提供的方法,从而完全解耦视图层。

第三方的MVP框架有很多,在此不一一举例了,MVP框架不是一个固定的死板的套路,要根据具体项目来指定不同的实现方式,在不恰当的项目中使用MVP框架反而会适得其反,比如当前的这个例子,我们仅仅只是想要获取一条字符串数据来更新TextView,却写出了将近十个类,这就充分说明了MVP框架不适合用于小型项目。

MVP框架不是万能的,想法才是。要想设计出适用的项目框架,必须要有扎实的基本功,要理解掌握面向对象设计的原则和各种各样的设计模式,熟练运用这些设计原则和设计模式,再加上一点奇思妙想,才能打造出千变万化的应用框架。

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

推荐阅读更多精彩内容