一步步構(gòu)建MVP框架

MVP模式已經(jīng)是老生常談了汗销,很多程序員用過這個架構(gòu)進行開發(fā)跟束,但是從無到有搭建MVP架構(gòu)的過程,或許沒有體驗過萨咕,在此我們來一步步搭建MVP環(huán)境统抬。
先說說概念,MVP: Model View Presenter

Model:獲取數(shù)據(jù)業(yè)務(wù)邏輯
View: UI展示
Presenter: 實現(xiàn)者 Model與View相互獨立危队,Presenter負責(zé)將Model與View綁定,是一個中間者的角色聪建。

image.png

如圖:MVC模式中,Model與View相互持有對方的引用茫陆,視圖與數(shù)據(jù)邏輯交互金麸。
在實際開發(fā)中,業(yè)務(wù)邏輯基本都是在Activity / ViewController中實現(xiàn)的簿盅,導(dǎo)致Controller的臃腫和難以維護挥下。
而在MVP模式中:Model與View完全被Presenter隔離,而Presenter作為一個中間者桨醋,負責(zé)將Model與View綁定棚瘟。
假設(shè)我們的有一組數(shù)據(jù)模型叫StudentBean

public class StudentBean {
    private String name;
    private String sex;
    private String ege;
    private String phone;
    //各種set get屬性的方法...

首先我們創(chuàng)建出Model,獲取StudentBean的數(shù)據(jù)喜最;同時我們也希望在數(shù)據(jù)獲取成功時有所反饋偎蘸,于是我們在數(shù)據(jù)獲取結(jié)束時,來個回調(diào)瞬内,實現(xiàn)如下:

/**
 * Created by Shmily on 2018/1/18.
 * Model接口
 */
public interface IStudentModel {
    /**
     * 加載數(shù)據(jù)
     * @param listener
     */
    void fetchData(OnLoadListener listener);
    /**
     * 加載完成的回調(diào)
     */
    interface OnLoadListener {
        void onLoadSuccess(List<StudentBean> list);
        void onLoadFailed();
    }
}

由代碼我們可以看出Model提供了獲取數(shù)據(jù)的方式迷雪,并在數(shù)據(jù)獲取結(jié)束時給予回調(diào)方法。
現(xiàn)在我們實現(xiàn)這個Model接口

public class StudentModelImpl implements IStudentModel{
    /**
     * 獲取數(shù)據(jù),并在結(jié)束時實現(xiàn)回調(diào)
     * @param listener
     */
    @Override
    public void fetchData(OnLoadListener listener) {
        if (listener == null) {
            return;
        }
        ArrayList<StudentBean> list = loadDataFromLocal();
        if (list == null || list.isEmpty()){
            listener.onLoadFailed();
        }
        else {
            listener.onLoadSuccess(list);
        }
    }

    /**
     * 模擬加載本地數(shù)據(jù)
     * @return
     */
    private ArrayList<StudentBean> loadDataFromLocal(){
        ArrayList<StudentBean> list = new ArrayList<>();
        StudentBean student1 = new StudentBean();
        student1.setName("jason");
        student1.setSex("女");
        student1.setEge("10");
        student1.setPhone("010-1234567");
        list.add(student1);
        ...
        return list;
    }
}

在StudentModelImp中遂鹊,模擬實現(xiàn)了加載數(shù)據(jù)的方法振乏,并在加載結(jié)束時,根據(jù)結(jié)果進行對應(yīng)成功失敗的回調(diào)秉扑。
有了數(shù)據(jù)獲取慧邮,我們來實現(xiàn)數(shù)據(jù)展示View模塊
View更簡單,拿到數(shù)據(jù)就展示舟陆,沒有數(shù)據(jù)误澳,就展示其他。

public interface IStudentView {
    /**
     * 顯示進度條
     */
    void showLoading();

    /**
     * 展示失敗結(jié)果
      */
    void showFailed();
    /**
     * 顯示獲取數(shù)據(jù)的結(jié)果,即數(shù)據(jù)
     * @param list
     */
    void showStudentList(List<StudentBean> list);
}

在View模塊中秦躯,實現(xiàn)加載進度條的方法和展示數(shù)據(jù)忆谓。
View模塊與Model模塊都有了,就差Presenter了踱承,看代碼之前倡缠,我們來分析下Presenter:
Presenter雖然隔離了Model與View哨免,但是也將二者關(guān)聯(lián)起來,也就是說要將數(shù)據(jù)獲取與數(shù)據(jù)展示關(guān)聯(lián)起來昙沦,所以琢唾,聰明的你,應(yīng)該想到了吧?

Presenter持有Model與View的引用盾饮,通過對象組合采桃,借助Model獲取數(shù)據(jù),借助View展示數(shù)據(jù)丘损,利用回調(diào)的方式普办,將這個過程串起來。
public class StudentPresenter {
    private IStudentView mStudentView;
    private IStudentModel mModel = new StudentModelImpl();
    public StudentPresenter(IStudentView studentView) {
        this.mStudentView = studentView;
    }

    public void fetch(){
        //借助view show loading
        mStudentView.showLoading();
        // 借助model 獲取數(shù)據(jù)
        mModel.fetchData(new IStudentModel.OnLoadListener() {
            @Override
            public void onLoadSuccess(List<StudentBean> list) {
                // model獲取數(shù)據(jù)成功后,由view來展示
                mStudentView.showStudentList(list);
            }

            @Override
            public void onLoadFailed() {
                mStudentView.showFailed();
            }
        });
    }
}

還差最后一步徘钥,View的實現(xiàn)衔蹲,與MVC一樣,我們用Activity作為View吏饿,實現(xiàn)IStudentView接口踪危,
并借助Presenter調(diào)用數(shù)據(jù)獲取的方法。

Presenter本身不能獲取數(shù)據(jù)猪落,但是它持有View與Model的引用贞远,通過Model獲取數(shù)據(jù),再通過回調(diào)的方式笨忌,讓View展示數(shù)據(jù)

    private void fetchData(){
        // 調(diào)用presenter加載數(shù)據(jù)
        new StudentPresenter(this).fetch();
    }

    /**
     * 展示進度條
     */
    @Override
    public void showLoading() {
        Toast.makeText(this,getString(R.string.loading),Toast.LENGTH_SHORT).show();
    }

    /**
     * 展示數(shù)據(jù)
     */
    @Override
    public void showStudentList(List<StudentBean> list) {
        myAdapter = new MyAdapter(this,list);
        listview.setAdapter(myAdapter);
    }

    /**
     * 展示加載失敗
     */
    @Override
    public void showFailed() {
        Toast.makeText(this,getString(R.string.loadfailed),Toast.LENGTH_SHORT).show();
    }

在Activity中蓝仲,完全看不到Model,但卻借助Presenter調(diào)用Model間接獲取了數(shù)據(jù)官疲,再通過View本身展示袱结。

Model與View二者完全隔離,卻被Presenter利用對象組合回調(diào)的方式相關(guān)聯(lián)途凫。

代碼的耦合度降低垢夹,在業(yè)務(wù)需求發(fā)生變化時,eg:需要從網(wǎng)絡(luò)獲取數(shù)據(jù)维费。 基于目前的代碼果元,我們不需要修改什么,只要再實現(xiàn)一個StudentImlInternet即可犀盟《梗可見實現(xiàn)了開閉原則Open Closed Principle
忘了什么是開閉原則?貼下概念:

定義: Softeware entities like classes,modules and functions should be open for extension but closed for modifications阅畴。
一個軟件實體如類倡怎、模塊和函數(shù)應(yīng)該對擴展開放,對修改關(guān)閉。
即通過擴展來實現(xiàn)變化监署,而不是通過修改已有代碼來實現(xiàn)變化颤专。

以上就是MVP模式。有沒有一種so easy! 的感覺钠乏。那么上面的寫法會不會有問題呢?

某一個Activity(View)在內(nèi)存不足時血公,已經(jīng)銷毀, 但是Model獲取數(shù)據(jù)(子線程耗時操作)
那么Presenter還持有View的引用缓熟。但Model完成后,Presenter去通知View更新摔笤,則出現(xiàn)內(nèi)存泄漏(生命周期不#####同步够滑,上下文丟失等問題)

舉例:開啟一個DownActivity 點擊頁面下載功能 那么model就開啟子線程獲取數(shù)據(jù)
然后點擊DownActivity的某按鈕去其他頁面 AnimationActivity 播放動畫或視頻,DownActivity是如果此時內(nèi)存不足>DownActivity被銷毀吕世,那么當model完成數(shù)據(jù)獲取彰触,由于P還持有V的引用,則Presenter就會通過DownActivity去更>新UI命辖,此時會發(fā)生內(nèi)存泄漏况毅。

image.png

解決方案:View被銷毀時,解除與Presenter的關(guān)聯(lián)尔艇。

So 我們需要兩個方法:attachView(view);  detachView(); (像Fragment的生命周期的用法)
在View創(chuàng)建onCreate與銷毀onDestory的時候調(diào)用尔许。

但是如果每個實現(xiàn)View的Activity都在onCreate與onDestory的時候調(diào)用綁定與解綁的方法,這未免太傻了吧终娃。于是你想到了在BaseActivity中實現(xiàn)味廊,于是我們要把代碼優(yōu)化為擴展的方式: 配合泛型提取父類

/**
 * Created by Shmily on 2017/6/2.
 */
public abstract class BaseActivity<V,T extends  BasePresenter<V>> extends Activity{
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 創(chuàng)建Presenter 將在view中創(chuàng)建presenter提取到Base類
        mPresenter = createPresenter();
        // p與v的關(guān)聯(lián)
        mPresenter.attachView((V)this);
    }

    public T mPresenter;

    /**
     * create presenter
     * @return 模版方法,具體實現(xiàn)到子類
     */
    public abstract T createPresenter();

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // p與v解除關(guān)聯(lián) 解決內(nèi)存泄漏的問題
        mPresenter.detachView();
    }
}

解釋下泛型這里:

由于業(yè)務(wù)未來的擴展棠耕,Presenter實現(xiàn)可能有多種類型余佛,可以提取到父類BaseActivity中創(chuàng)建:采用模版
方法public T mPresenter; public abstract T createPresenter();
而T要在BaseActivity中聲明,So BaseActivity<T>
Presenter的父類 BasePresenter(持有View的引用)
使用泛型V 在BasePresenter聲明 BasePresenter<V>
public abstract class BasePresenter<T>
而BaseActivity持有BasePresenter的子類的引用(要在這里創(chuàng)建) So BaseActivity中的T是繼承
BasePresenter的一個子類窍荧。那么 abstract class BaseActivity<V辉巡,T extends BasePresenter<V>>

基于上文內(nèi)存泄漏的可能性,我們對Presenter的擴展時蕊退,讓presenter對view的持有為弱引用
public abstract class BasePresenter<T> {
    //View接口類型弱引用
    public WeakReference<T>  mViewRefer;

    /**
     * bind view with presenter
     * @param view
     */
    protected void attachView(T view) {
        //建立關(guān)聯(lián)
        mViewRefer = new WeakReference<T>(view);
    }

    //可以通過此方法,判斷是否與View建立了關(guān)聯(lián)
    protected boolean isViewAttached() {
        return mViewRefer != null && mViewRefer.get() != null;
    }

    /**
     * unbind view with presenter
     */
    protected void detachView(){
        if (mViewRefer != null) {
            mViewRefer.clear();
            mViewRefer = null;
        }
    }
    /**
     * base 類提供view的引用
     * @return
     */
    public T getView(){
        return mViewRefer.get();
    }
}

對于弱引用不了解的童鞋郊楣,一定要自己學(xué)習(xí)下。
最后在Activity調(diào)用

  private void fetchData(){
        mPresenter.requestData();
    }
    /**
     * 子類實現(xiàn)父類的方法
     * @return
     */
    @Override
    public StudentPresenterIml createPresenter() {
        return new StudentPresenterIml();
    }

在子類中創(chuàng)建自己想要的presenter對象咕痛,通過引用父類的mPresenter直接獲取數(shù)據(jù)痢甘。
MVP是安卓研發(fā)編碼時常用的代碼框架,本篇先是以最精簡的方式茉贡,搭建MVP框架塞栅。了解了它的核心與本質(zhì)后,再用擴展的方式,再將Presenter的實現(xiàn)提到抽象層放椰,具體實現(xiàn)延遲到子類作烟,并解決可能存在的內(nèi)存泄漏的問題。
代碼中有充分的注釋砾医,在此獻上代碼
[精簡版] https://github.com/Shmily701/SimpleMVP
[擴展版] https://github.com/Shmily701/ExtendedMVP
喜歡學(xué)習(xí)拿撩,樂于分享,不麻煩的話如蚜,給個?鼓勵下吧压恒!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市错邦,隨后出現(xiàn)的幾起案子探赫,更是在濱河造成了極大的恐慌,老刑警劉巖撬呢,帶你破解...
    沈念sama閱讀 212,294評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伦吠,死亡現(xiàn)場離奇詭異,居然都是意外死亡魂拦,警方通過查閱死者的電腦和手機毛仪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,493評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來芯勘,“玉大人箱靴,你說我怎么就攤上這事『摄担” “怎么了刨晴?”我有些...
    開封第一講書人閱讀 157,790評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長路翻。 經(jīng)常有香客問我狈癞,道長,這世上最難降的妖魔是什么茂契? 我笑而不...
    開封第一講書人閱讀 56,595評論 1 284
  • 正文 為了忘掉前任蝶桶,我火速辦了婚禮,結(jié)果婚禮上掉冶,老公的妹妹穿的比我還像新娘真竖。我一直安慰自己,他們只是感情好厌小,可當我...
    茶點故事閱讀 65,718評論 6 386
  • 文/花漫 我一把揭開白布恢共。 她就那樣靜靜地躺著,像睡著了一般璧亚。 火紅的嫁衣襯著肌膚如雪讨韭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,906評論 1 290
  • 那天,我揣著相機與錄音透硝,去河邊找鬼狰闪。 笑死,一個胖子當著我的面吹牛濒生,可吹牛的內(nèi)容都是我干的埋泵。 我是一名探鬼主播,決...
    沈念sama閱讀 39,053評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼罪治,長吁一口氣:“原來是場噩夢啊……” “哼丽声!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起觉义,我...
    開封第一講書人閱讀 37,797評論 0 268
  • 序言:老撾萬榮一對情侶失蹤恒序,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后谁撼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,250評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡滋饲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,570評論 2 327
  • 正文 我和宋清朗相戀三年厉碟,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屠缭。...
    茶點故事閱讀 38,711評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡箍鼓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出呵曹,到底是詐尸還是另有隱情款咖,我是刑警寧澤,帶...
    沈念sama閱讀 34,388評論 4 332
  • 正文 年R本政府宣布奄喂,位于F島的核電站铐殃,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏跨新。R本人自食惡果不足惜富腊,卻給世界環(huán)境...
    茶點故事閱讀 40,018評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望域帐。 院中可真熱鬧赘被,春花似錦、人聲如沸肖揣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,796評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽龙优。三九已至羊异,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背球化。 一陣腳步聲響...
    開封第一講書人閱讀 32,023評論 1 266
  • 我被黑心中介騙來泰國打工秽晚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人筒愚。 一個月前我還...
    沈念sama閱讀 46,461評論 2 360
  • 正文 我出身青樓赴蝇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親巢掺。 傳聞我的和親對象是個殘疾皇子句伶,可洞房花燭夜當晚...
    茶點故事閱讀 43,595評論 2 350