淺談MVP

MVP是從MVC衍生出來的,因此,先說一下MVC,再到MVP。java

MVC

MVC分爲三個部分:android

視圖(View):用戶界面。git

控制器(Controller):業務邏輯,用於控制應用程序的流程。github

模型(Model):數據處理。即網絡請求、數據庫等一些對數據的操做處理。數據庫

它們之間的交互一般以下圖:編程

MVC

那麼在咱們android裏,如何劃分這幾層?在這裏,參考部分博文,以及根據我我的的理解,列出下面兩種可能的狀況:bash

(一)View:xml。Controller:Activity/Fragment。Model:數據處理。網絡

若是是這樣劃分的話。實際運用中,每每會出現Activity/Fragment雖然劃分爲Controller層,但看起來又像View層。架構

爲何?由於若是隻用xml做爲View層,對界面的控制能力實在太弱了,沒法動態更新UI。因此,Activity/Fragment也要擔當起一部分View層的責任,負責動態更新視圖。這就致使Activity/Fragment既有更新View控件的代碼,又有對源自Model的數據進行進一步邏輯處理的代碼,隨着迭代開發,Activity/Fragment會顯得愈來愈臃腫,分分鐘幾千行代碼,給後期維護帶來極大的困擾。若是你接手這樣的代碼,內心恐怕會不停讚歎前人真牛逼!mvc

不少博客,描述MVC時,都是這樣劃分MVC的,並指出:「View層和Model層是相互可知的,這意味着兩層之間存在耦合」。

我我的以爲這話有謬誤。View層和Model層必定是互相可知的?MVC的交互圖,V與M的箭頭,只表明它們能夠互相給對方發送消息。但並不意味着它們之間就必定互相可知。

若是View層和Model層是相互可知的,這也就意味着它們互相持有對方的引用,是經過對方的引用來給彼此發送消息。這確實意味着兩層之間存在耦合。下面是這種狀況下,View和Model交互的代碼。

class XActivity {
    public void attachModel() {
        VersionModel model = new VersionModel(this);
        model.checkUpdate();
    }
    public void onResult(String result) {
        System.out.println(this.getClass().getSimpleName() + "收到了迴應:" + result);
    }
}

class VersionModel {
    private XActivity mActivity;
    //注意:activity的引用經過構造方法傳遞了進來
    public VersionModel(XActivity activity) {
        mActivity = activity;
    }
    public void checkUpdate() {
        System.out.println("檢查更新!");
        //用activity的引用傳遞更新信息。這樣,該方法就會綁定死了這個activity。
        mActivity.onResult("暫無更新!");
    }
}

public class Couple {
    public static void main(String[] args) {
        //這個VersionModel只能提供給XActivity這個界面使用。
        XActivity xActivity = new XActivity();
        xActivity.attachModel();
    }
}/* Output:
檢查更新!
XActivity收到了迴應:暫無更新!
*/

複製代碼

但這樣的代碼,實在是太糟糕了。由於Model層的代碼,複用性是頗有必要的。好比,這個檢查版本更新的Model,也許除了閃屏頁須要,你的設置界面也須要有這個功能。像上面這樣寫,那你的Model和View綁定死了。因此,更恰當的作法,是下面這樣。Model的數據處理結果,經過接口回調給View,保證Model的複用性。

class XActivity {
    public void attachModel() {
        //跟前面不一樣,再也不是直接傳遞自身引用給Model,而是傳一個回調接口,經過回調接口,獲取數據處理結果
        VersionModel model = new VersionModel(new Callback() {
            @Override
            public void onResult(String result) {
                System.out.println(XActivity.this.getClass().getSimpleName() + "收到了迴應:" + result);
            }
        });
        model.checkUpdate();
    }
}

class YActivity {
    public void attachModel() {
        //跟前面不一樣,再也不是直接傳遞自身引用給Model,而是傳一個回調接口,經過回調接口,獲取數據處理結果
        VersionModel model = new VersionModel(new Callback() {
            @Override
            public void onResult(String result) {
                System.out.println(YActivity.this.getClass().getSimpleName() + "收到了迴應:" + result);
            }
        });
        model.checkUpdate();
    }
}

interface Callback {
    void onResult(String result);
}

class VersionModel {
    private Callback callback;
    //再也不是直接接受某個Activity實例作參數,這樣就不會再與某個Activity過於耦合,提升了複用性
    public VersionModel(Callback callback) {
        this.callback = callback;
    }
    public void checkUpdate() {
        System.out.println("檢查更新!");
        //用的是回調接口,傳遞更新信息。該方法,不會再是綁定死某個activity。
        callback.onResult("暫無更新!");
    }
}

public class Decouple {
    public static void main(String[] args) {
        //這個VersionModel能夠隨意提供給N個界面使用。
        XActivity xActivity = new XActivity();
        xActivity.attachModel();
        YActivity yActivity = new YActivity();
        yActivity.attachModel();
    }
}/* Output:
檢查更新!
XActivity收到了迴應:暫無更新!
檢查更新!
YActivity收到了迴應:暫無更新!
*/
複製代碼

(二)View:"xml+Activity/Fragment"視爲View層,僅僅負責控件更新。Controller:將Activity/Fragment中的邏輯控制,包括對源自Model層的數據的進一步處理的操做,抽取出來,到相似XxxController這樣命名的類裏,做爲Controller。做爲Controller。Model:數據處理。

這是我本身瞎琢磨的一種劃分方法。

若是這樣劃分,能夠極大地減輕了View層的負擔。可是,如今業務邏輯處理都被抽調到了Controller。而當Controller須要根據Model的數據處理結果,來決定View下一步作什麼的時候,只能經過View去得到(由於View層和Model層能夠互相交互。而Controller只能單方向跟Model發消息)。而View自身已經不負責業務邏輯處理了,還得夾在它們中間,簡直畫蛇添足。

MVP的寫法,其實就是基於這種劃分的。但使用MVP來實現,不會有這種尷尬。

MVP

若是MVC改一改:Controller更名爲Presenter,View和Model再也不容許交互,Controller(Presenter)和Model之間的單向通信,改成雙向。那麼這種作法,就是所謂的MVP了。

Google開源的MVP架構示例項目:android-architecture,也是基於這種劃分:

View:xml+Activity/Fragment。

Presenter:根據業務邏輯,對Model得到的結果進一步處理,最後決定View何時更新UI。

Model:數據處理。

以下圖:

MVP

android-architecture這個項目有不少分支,本文主要參考的是todo-mvp、todo-mvp-rxjava、todo-mvp-dagger三個分支。這幾個分支對MVP的劃分是一致的,只是實現細節上的差異。想學習rxjava2和dagger2的,推薦翻閱這兩個分支。

MVP的運做過程,大體能夠這麼理解:View給Presenter指派任務,而後Presenter調控一個或多個Model,給它們劃分各類小任務,配合完成任務。最後,Model經過回調接口,告訴Presenter任務結果,而後Presenter根據任務的完成狀況,通知View更新UI。

以前用於mvc討論的檢查更新功能,若是用MVP寫。關鍵的代碼以下:

public interface SplashContract {
    interface View{
        void showUpdateDialog(VersionBean versionBean);
        void jumpToMain();
    }
    interface Presenter{
        void attachView(SplashContract.View view);
        void detachView();
        void checkUpdate();
    }
}

public interface VersionCallback{
    void onUpdate(VersionBean versionBean);
    void onNoUpdate();
}

public class VersionBean{
    private int versionCode;
    private String versionName;
    private String updateUrl;
    public VersionBean(int versionCode,String versionName,String updateUrl){
        this.versionCode = versionCode;
        this.versionName = versionName;
        this.updateUrl = updateUrl;
    }
    //省略get/set方法
    ......
}

public class SplashActivity extends AppCompatActivity implements SplashContract.View{
    private SplashContract.Presenter mPresenter;
    
    @Override
    public void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        attachPresenter();
        checkUpdate();
    }
    
    public void attachPresenter(){
        //注意,使用的是Application的Context
        VersionModel versionModel = new VersionModel(getApplicationContext());
        mPresenter = new SplashPresenter(versionModel);
        mPresenter.attachView(this);
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mPresenter.detachView();
    }

    public void checkUpdate(){
        mPresenter.checkUpdate();
    }
    
    @Override
    public void showUpdateDialog(VersionBean versionBean){
        //彈更新提示dialog
    }
    
    @Override
    public void jumpToMain(){
        //跳轉到MainActivity
    }
}

public class SplashPresenter implements SplashContract.Presenter{
    private VersionModel mVersionModel;
    
    private SplashContract.View mView;
    
    public SplashPresenter(VersionModel versionModel){
        mVersionModel = versionModel;
    }
    
    @Override
    public void attachView(SplashContract.View view){
        mView = view;
    }
    
    public void onDetachView() {
        //能夠在此取消全部的異步訂閱
    }
    
    @Override
    public void checkUpdate(){
        mVersionModel.checkUpdate(new VersionCallback(){
            @Override
            public void onUpdate(VersionBean versionBean){
                //異步任務回來後,先判斷View是否處於正確的狀態
                if(mView != null){
                    mView.showUpdateDialog(versionBean);
                }
            }
            
            @Override
            public void onNoUpdate(){
                //異步任務回來後,先判斷View是否處於正確的狀態
                if(mView != null){
                    mView.jumpToMain();
                }
            }
        });
    }
}

public class VersionModel{
    private Context mContext;
    public VersionModel(Context context){
        mContext = context;
    }
    
    public void checkUpdate(VersionCallback callback){
        //經過網絡獲取更新信息
        VersionBean versionBean = fromServer();
        if(BuildConfig.VERSION_CODE < versionBean.getVersionCode()){
            callback.onUpdate(versionBean);
        }else{
            callback.onNoUpdate();
        }
    }
}
複製代碼

區區一個檢測更新而已,這代碼量會不會有點多了?

不要緊。注意事項更多。

注意事項

基於todo-mvp的mvp寫法,我的概括的注意事項以下:

1.View只負責簡單的視圖更新、界面跳轉的代碼。

你能夠理解爲,View是很懶很懶的,不肯意動腦子,幾乎全部的邏輯處理,哪怕只是簡單的「1+1=?」的問題,都推給Presenter。我以爲,除了實現視圖和邏輯的分離,還有一部分緣由是:若是是Presenter太臃腫,你徹底能夠根據功能不一樣,輕易拆分紅兩個甚至更多的Presenter。可是若是Activity/Fragment(View)太臃腫的話,可能就很差拆分了。

因此,View只負責初始化自身,根據須要,給Presenter指派任務,而後進入「瞌睡」狀態。只有兩種狀況纔會被從新喚醒:

1)當接受來自外界的刺激時,好比:點擊事件;

2)當Presenter有處理返回:嗨,孫子,醒醒,起來倒茶了(更新UI)。

2.View和Presenter的交互,是用接口來實現的。

下面是接口寫法的示例。

public interface XxxContract {
    interface View{
    }
    interface Presenter{
    }
}
複製代碼

Activity/Fragment實現XxxContract.View接口,Presenter實現XxxContract .Presenter接口。二者在使用對方的實例時,一般都會向上轉型爲對應的接口,也就是說,暴露給對方的方法,都會寫在接口裏面。

討論:這個Contract接口到底有沒有存在的必要?

其實,縱觀谷歌的demo,View和Presenter之間的耦合度一般都是很高的,Presenter是爲對應的View量身定製的,複用的可能性不大,一般也沒有這個必要。

而這個接口,如其名,更像是一個契約接口。裏面兩個關鍵的子接口,在這種狀況下,徹底失去了接口存在的最大意義:"多繼承"。

  「肯定接口是理想選擇,於是應該老是選擇接口而不是具體的類。」這實際上是一種引誘。固然,對於建立類,幾乎在任什麼時候刻,均可以替代爲建立一個接口和一個工廠。許多人都掉進了這種陷阱,只要有可能就去建立接口和工廠。這種邏輯看起來好像是由於須要使用不一樣的具體實現,所以老是應該添加這種抽象性。這實際上已經變成了一種草率的設計優化。任何抽象性都應該是應真正的需求而產生的。若是沒有足夠的說服力,只是爲了以防萬一添加新接口,並由此帶來了額外的複雜性。那麼應該質疑一下這樣的設計了。恰當的原則,應該是優先選擇類而不是接口。接口是一種重要工具,但它們容易被濫用。」                 ——摘自《Java編程思想》第9章 接口 第188-189頁

尤爲是MVP用得多了,這個契約接口,總讓人感到彆扭。好比,想在Presenter添加一個方法,除了在Presenter裏寫它一次,還得在契約接口裏面聲明一次。它給我帶來的麻煩,彷佛多於給個人便利。

這個Contract接口有沒有存在的必要,真的有待商榷。

因此,我的更傾向於:去除Contract接口、Presenter接口,只保留View接口。Activity/Fragment則直接使用Presenter的實例時,再也不將其向上轉型爲接口。在調用Presenter的attachView方法的時候,將Activigty/Fragment的實例向上轉型爲接口,避免Presenter濫用該實例引用。

以上僅是我的觀點,也許鄙人目光短淺,有所謬誤。歡迎指正。

3.Presenter異步任務回來後,通知View更新UI以前,要先判斷Activity是否爲null,或者Fragment是否已經從activity中移除。

由於Presenter的生命週期,一般與Activity/Fragment是不相同的。因此Presenter在執行異步操做後,在結束的時候,都要判斷View是否還處於正確的狀態。這樣,就能有效得避免了在異步任務完成時,Activity/Fragment卻已經被銷燬而致使的空指針等問題。固然,同步任務一般不須要這種判斷。

有趣的是,todo-mvp、todo-mvp-dagger、todo-mvp-rxjava這三個分支對上述操做的作法都不一樣,但有殊途同歸之妙。下面給出它們作法的關鍵代碼。

todo-mvp

public class TasksFragment extends Fragment implements TasksContract.View {
   /**
    * Return true if the fragment is currently added to its activity.
    */
    @Override
    public boolean isActive() {
        return isAdded();
    }
}

public class TasksPresenter implements TasksContract.Presenter {
    @Override
    public void loadTasks(boolean forceUpdate) {
        //異步任務
        mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() {
            @Override
            public void onTasksLoaded(List<Task> tasks) {
                //異步任務結束後,再調View以前,必須先判斷
                //不過這種作法,其實只適合實現View接口的是Fragment的時候
                if (!mTasksView.isActive()) {
                    return;
                }
                mTasksView.setLoadingIndicator(false);
            }
        });
    }
}
複製代碼

todo-mvp-dagger

final class TasksPresenter implements TasksContract.Presenter {
    @Override
    public void loadTasks(boolean forceUpdate) {
        //異步任務
        mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() {
            @Override
            public void onTasksLoaded(List<Task> tasks) {
                //異步任務結束後,再調View以前,必須先判斷
                if (mTasksView == null) {
                    return;
                }
                mTasksView.setLoadingIndicator(false);
            }
        });
    }

    //在View解除綁定時,會被調用(須要你本身在onDestroy/onDestroyView裏面手動調用)
    @Override
    public void dropView() {
        mTasksView = null;
    }
}
複製代碼

todo-mvp-rxjava

public class TasksPresenter implements TasksContract.Presenter {
    @Override
    public void loadTasks(boolean forceUpdate) {
        //異步任務
        Disposable disposable = mTasksRepository
                .getTasks()
                .subscribe(tasks -> mTasksView.setLoadingIndicator(false));
        //將能取消異步任務的disposable添加進CompositeDisposable
        mCompositeDisposable.add(disposable);
    }

    //在View解除綁定時,會被調用(須要你本身在onDestroy/onDestroyView裏面手動調用)
    // 取消mCompositeDisposable裏面添加的全部RxJava的異步任務。
    @Override
    public void unsubscribe() {
        mCompositeDisposable.clear();
    }
}
複製代碼

4.Model,做爲數據源,要確保它的可複用性。

就像我上面講MVC時舉的例子,Model不該該持有Presenter的引用,它不該該知道會是哪一個Presenter來調用它。它的數據處理結果,都應該經過回調接口來交給調用它的Presenter。若是,你以爲寫這些回調接口很煩,那麼你能夠考慮學習一波RxJava了。

反面示例:

//不該該將Presenter的引用傳給Model
public MainModel(MainContract.Presenter mainPresenter) {
        this.mainPresenter= mainPresenter;
}
複製代碼

這是個很典型的反面例子...我也曾經這麼寫過Model的代碼,直到有一天,我須要在其餘地方複用某個Model的時候...笑哭.jpg。

5.Context。若是你要在Presenter和Model裏面使用Context,那麼你應該使用Application的Context,而不是Activity的Context。

緣由以下:

1)避免Context在子線程使用時,因爲Activity忽然被銷燬,致使的空指針。

2)避免本身偷懶,在Presenter和Model執行更新UI的操做。實際上,這也是很不該該的操做。我在不少項目裏面,常常看到這樣的錯誤操做:在Presenter裏更新UI、把Adapter的代碼放進Presenter裏面等等。MVP的劃分,不只僅是爲了代碼的解耦、複用,也有很大一部分緣由是由於Android類不能直接在JVM上運行,會影響咱們寫單元測試。而儘可能隔離、減小使用Android類的Presenter、Model層,會更便於咱們寫相關單元測試。

3)Application的Context足夠應付Presenter和Model裏面的需求了。看下錶,Context的應用場景:

Context的應用場景
有一些NO上添加了一些數字。

*數字1:啓動Activity在這些類中是能夠的,可是須要建立一個新的task。通常狀況不推薦。

*數字2:在這些類中去layout inflate是合法的,可是會使用系統默認的主題樣式,若是你自定義了某些樣式可能不會被使用。

*數字3:在receiver爲null時容許,在4.2或以上的版本中,用於獲取黏性廣播的當前值。(能夠無視) ContentProvider、BroadcastReceiver之因此在上述表格中,是由於在其內部方法中都有一個context用於使用。

你能夠看見,Activity的Context能作的,Application的Context基本都能作。若是你非要在Presenter、Model裏面使用Activity的Context,我想你更應該考慮是否是該把這段代碼放到Activity/Fragment裏面。

6.Presenter、Model的構造方法,要使用依賴注入。

所謂的依賴注入,就是A類裏面須要使用到B類,而B類的實例,在一開始,就先在外面建立好,而後經過A類的構造方法傳遞進來的。先前的MVP示例裏的這段代碼,就完成了一個簡單的依賴注入。

VersionModel versionModel = new VersionModel(getApplicationContext());
mPresenter = new SplashPresenter(versionModel);
複製代碼

如今流行的Dagger2這個依賴注入框架只是能極大地簡化了你本身去手動new這些實例,甚至建立這些實例的單例,再注入的過程。

全局的Context,若是要在Presenter、Model裏面使用,也要經過構造方法,將Context傳遞進來。很大的一部分緣由也是:方便寫單元測試代碼。

由於單元測試裏面有一個mock的概念。依賴注入,能方便你mock相應的類。若是對單元測試有興趣的話,到時能夠參考個人相關測試文章。

代碼示例: todo-mvp裏的某個Presenter構造方法的部分代碼

public TasksPresenter(@NonNull TasksRepository tasksRepository) {
        /**
         * 至關於mTasksRepository = tasksRepository,並檢測tasksRepository是否爲null。
         * 若是爲空,拋空指針異常。
         */
        mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
}
複製代碼

注:checkNotNull是谷歌的guava裏面的方法。雖然谷歌demo的各個分支,都有用到guava庫,一個工具類庫。但實際項目中並不推薦適用它,由於android裏64k方法的限制。這個庫它有1w+方法...若是你喜歡用相關方法,建議把相關類複製出來便可。

7.關於Presenter、Model的劃分

前面提到,Model是「數據處理」,Presenter是「根據業務邏輯,對Model得到的結果進一步處理」。不少人也許會對Presenter、Model的劃分產生疑問:一樣都會進行數據處理,怎麼劃分才更恰當?

其實MVP三層的關係,從另外一個角度看的話,是層層遞進的關係,以下圖。

而前面咱們已經知道Model的複用性是很重要。假設,若是有多個Presenter都對Model中得到的數據,進行一樣的處理,那麼咱們就應該考慮,把這一段處理代碼,「下沉」到Model層裏。若是Model裏的一個方法,有一段代碼只針對某個Presenter作了特殊處理,那麼咱們就應該考慮,把這段處理代碼,「上浮」到那個特定的Presenter裏。

總結

相比MVC的各類尷尬,MVP之間的分工合做明顯更加合理,便於維護、測試。箇中好處,只能你理解和熟練使用MVP後,才能深有感觸。

建議認真翻閱谷歌的官方架構demo:android-architecture

我我的也提供了一個用MVP寫的訊飛語音交互demo:TalkDemo

若是你對dagger、rxjava熟悉,也能夠參閱個人另外一個基於mvp+dagger2(dagger.android)+rxjava2+retrofit2的架構demo:ArchitectureDemo

以上內容,均基於我的的理解和谷歌的架構demo總結的。

若有謬誤,請及時提醒,不勝感激!

相關文章
相關標籤/搜索