開源專案Philm的MVP架構分析

黑風S發表於2015-10-14

前言

最近一直在研究ChrisBannes的開源專案Philm,其整體架構是一套MVP的實現,因為自己也確實沒有遇到過整個專案利用MVP搭建的架構,看到的更多是一些程式碼片段,這裡就探討Philm是如何結合Android實際問題來實現一種MVP架構,如有分析不準確的地方,歡迎指出,大家一起探討。

1.簡單談一談MVP

在無任何模式下的開發時,Activity與Model層的關係太緊密了,做了所有的操作,不易維護,擴充套件性較差。比如我們後期的需求可能不是從資料庫獲取資料了,而是從網路,又或者有一個版本要對所有的UI進行大改版,(隨著MaterialDesign的出現,我覺得這個還是有可能的),如果所有的邏輯都在Activity中,那麼如此臃腫的程式碼,怎麼修改都費勁吧,又或者你要適應多套UI,比如平板,維護起來也很麻煩吧。

1.2 MVP

MVP是MVC的一種衍生,MVP模式中不容許View直接訪問Model,這是MVP與MVC最大的不同之處。View中應該只有UI邏輯,捕捉使用者輸入以及檢視的渲染。這樣將其它複雜的邏輯抽離出來放到Presenter中去,這樣就出現了MVP。這種模式和傳統的軟體工程思想一樣,降低了耦合度,模組化,更方便維護。Presenter通常是通過定義好的介面與View進行互動,那麼開發的時候,只要寫一個測試類去實現該介面即可模擬使用者的各種操作進行測試,而不需要使用自動化測試工具。甚至可以不用再每次在手機上重新執行應用了,測試也更有效率。

簡單的說,就是將View中的複雜工作抽取到Presenter中,降低了耦合度,便於維護和測試,也增強了複用性。

在MVP模式裡通常包含4個要素
(1) View: 負責繪製UI元素、與使用者進行互動(Activity或Fragment);
(2) View interface: View需要實現的介面,View通過View interface與Presenter進行互動,降低耦合,方便進行單元測試
(3) Model: 負責業務Bean的操作。
(4) Presenter: 作為View與Model互動的紐帶,承載了大部分的複雜邏輯。

MVP的優點
1、Model與View完全分離,它們通過介面進行互動,便於維護和測試。
2、可以更高效地使用Model,因為所有對Model的操作都在Presenter內部。
3、我們可以將一個Presener用於多個檢視,只需要在Presenter中為不同的View定義View Interface即可,具體的View實現自己的View Interface,即可使用Presenter中的Model操作等。

關於MVP的更多資料和討論可以看文章結尾的相關連結

2. MVP架構的實現

MVP的具體實現是沒有標準的,因為一個專案要考慮的因素很多,你可以按照自己的習慣和需求進行具體的實現。下面我們來分析Philm中實現的MVP架構。

2.1 Philm的總體設計

Philm使用了Controller來統一管理Model、View,按照上面的MVP理解以及Controller實際所做的工作,這個Controller其實相當於上面的Presenter,但這個Controller更加複雜,在Controller內部,直接定義了MVP中View與Presenter的互動介面Callback,另外該專案中引入了State的概念(具體見下面的介紹),統一管理了Model和業務中所需的Event,所有的Activity和Fragment的跳轉以及TitleBar和Drawer的管理使用了一個Display來實現。

類關係圖

基本呼叫流程圖

2.2 核心概念

2.2.1 Controller

控制中心,簡單的可認為是MVP中的Presenter,但是其更復雜。統一管理介面狀態的初始化和狀態清理,為所有的UI進行渲染並新增CallBack,統一排程業務相關的後臺任務執行緒,訂閱State中定義的Event,進行檢視的更新。並統一定義了View與Presenter的View Interface,一個Controller可以為多個View定義View Interface,因此Controller可以被多個View所共用。

2.2.2 State

儲存介面使用到的業Bean,定義業務中所用到Event事件。

每一個介面擁有自己的State介面,ApplicationState負責統一實現所有State介面,ApplicationState扮演了UI和後臺執行緒的通訊者,實現儲存業務Bean的分發和事件的分發。

使用Controller進行populate時,會從state中獲取業務Bean,如果State中沒有,Controller則會啟動後臺執行緒請求資料,成功獲取資料後通過state.set()分發儲存到State中,同時會post一個Event通知Controller進行相應的處理。

State中定義的事件型別
非同步請求完成的通知、資料變更、NetWork的狀態變化、LoadingProgress的展示與隱藏,這也是State被單獨抽離出來的原因吧,統一儲存了Model並定義了各種State,注意State並不對Model做複雜的操作,只是簡單的Set和Get,複雜的操作全部由Controller處理。

2.2.3 Display

統一控制TitleBar、Drawer以及所有Activity和Fragment跳轉,沒有業務邏輯操作。

3. 核心類分析

3.1 BaseController

3.1.1 核心方法

(1) init()
所有Controller初始化發起的方法,在BasePhilmActivity中通過mMainController.init()發起,所有的Controller由MainCtroller統一控制。

1
2
3
4
5
public final void init() {
       Preconditions.checkState(mInited == false, "Already inited");
       mInited = true;
       onInited();
   }

(2) suspend

1
2
3
4
5
public final void suspend() {
       Preconditions.checkState(mInited == true, "Not inited");
       onSuspended();
       mInited = false;
}

3.2 BaseUiController

統一定義管理某個介面的所有的UI,事件,介面。
在View(這裡是Activity或Fragment)中使用的時候獲取相應的Controller,然後在onResume中呼叫getController().attachUI(),為View設定CallBack,然後進行populate,在onPause中呼叫 getController().detachUi(this)清空UI的所有CallBack以及其它狀態的清理。

每一個Controller都擁有一個自己的UiCallBacks,以及一個繼承自BaseUiController.Ui的UI。

Controller是可以共用的
在Controller內部也可同時為不同的View定義相應的UICallBack,因為不同的View可能會使用到相同的Model,State等事件,不同的View只需實現Controller中定義的與自己相關的CallBack即可。當然該UICallBack需要繼承已實現BaseUiController.Ui的UI,因為最終都會傳入到BaseUIController中進行setCallbacks(UC)。

3.2.1 重要成員變數

mUis:用於儲存所有的UI
mUnmodifiableUis:mUis的拷貝,但不可修改
UI<UC>介面
所有Controller的子類的UI都需要實現該UI介面,擁有CallBack的能力,在Controller attachUi的時候會為傳入的UI設定CallBack

1
2
3
4
5
6
public interface Ui<UC> {

       void setCallbacks(UC callbacks);

       boolean isModal();
   }

3.2.2 重要方法

3.2.2.1 attachUi(U ui)

在Activity或者Fragment的onResume中呼叫,進行新增回撥,檢視渲染等工作。
final 型別,不可複寫,只是為了Controller的實現類進行狀態的管理,如果需要更多的操作,可以實現onUiAttached(UI)方法,該方法會在attachUI中呼叫。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public synchronized final void attachUi(U ui) {
        Preconditions.checkArgument(ui != null, "ui cannot be null");
        Preconditions.checkState(!mUis.contains(ui), "UI is already attached");

        mUis.add(ui);

        ui.setCallbacks(createUiCallbacks(ui));

        if (isInited()) {
            if (!ui.isModal() && !(ui instanceof SubUi)) {
                updateDisplayTitle(getUiTitle(ui));
            }

            onUiAttached(ui);
            populateUi(ui);
        }
    }

3.2.2.2 detachUI(U ui)

在Activity或者Fragment的onResume中呼叫,清空CallBack。
final 型別,不可複寫,只是為了實現類進行狀態的管理,同上如果有更多的操作,可以實現onUiDetached(ui)方法。

1
2
3
4
5
6
7
8
public synchronized final void detachUi(U ui) {
       Preconditions.checkArgument(ui != null, "ui cannot be null");
       Preconditions.checkState(mUis.contains(ui), "ui is not attached");
       onUiDetached(ui);
       ui.setCallbacks(null);

       mUis.remove(ui);
   }

3.2.2.3 onInited()

各個Controller進行初始化的時候呼叫,不用明確的呼叫,因為MainController統一管理了所有的Controller,在BasePhilmActivity的onResume中通過呼叫MainController.init()方法統一初始化。

1
2
3
4
5
6
7
8
protected void onInited() {
        if (!mUis.isEmpty()) {
            for (U ui : mUis) {
                onUiAttached(ui);
                populateUi(ui);
            }
        }
    }

各個Controller自身的onInited方法做一些事件的註冊等簡單的初始化。

3.2.2.4 onSuspended()

清理CallBacks並取消所有註冊事件 unregisterForEvents。
也是由MainController統一在BasePhilmActivity的onPause中管理。

1
2
3
4
5
6
7
@Override
  protected void onPause() {
     mMainController.suspend();        		  
     mMainController.setHostCallbacks(null);
     mMainController.detachDisplay(mDisplay);
     super.onPause();
  }

3.2.2.5 populate(UI)

對View進行渲染。

3.2.2.6 createUiCallbacks(U ui)

abstract型別,用於Controller建立自己的UICallBack,該方法已經在attachUi(U ui)的時候呼叫ui.setCallbacks(createUiCallbacks(ui)),為所有UI設定自己的UI回撥事件。

3.2.2.7 getId(U ui)

為每一個後臺執行緒和Event設定一個ID。當需要渲染的時候,遍歷所有的UI,通過findUi找到目標UI,進行更新。

1
2
3
protected int getId(U ui) {
       return ui.hashCode();
   }

3.2.2.8 findUi(final int id)

與getId(U ui)相對應,獲取後臺執行緒或者事件的ID。

3.2.2.9 populateUiFromEvent

populateUiFromEvent(BaseState.UiCausedEvent event)
當某些資料更新的時候,需要更新檢視時,呼叫該方法,傳送一個事件,通知Controller進行UI更新。

3.3 MainController

3.3.1 onInited()

對所有Controller的CallBack和狀態進行初始化

1
2
3
4
5
6
7
8
@Override
 protected void onInited() {
     super.onInited();
     mState.registerForEvents(this);
     mUserController.init();
     mMovieController.init();
     mAboutController.init();
 }

3.3.2 onSuspended

對所有Controller的CallBack和狀態進行清理

1
2
3
4
5
6
7
8
9
10
@Override
    protected void onSuspended() {
        mAboutController.suspend();
        mUserController.suspend();
        mMovieController.suspend();
        mDbHelper.close();
        mState.unregisterForEvents(this);

        super.onSuspended();
    }

3.4 BasePhilmActivity

整個專案的MainController的初始化的發起:

3.4.1 重要方法

3.3.1.1 onCreate

1
2
mMainController = PhilmApplication.from(this).getMainController();
AndroidDisplay  mDisplay = new AndroidDisplay(this, mDrawerLayout);

3.4.1.2 onResume

初始化所有的狀態

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 @Override
   protected void onResume() {
       super.onResume();
       mMainController.attachDisplay(mDisplay);
       mMainController.setHostCallbacks(this);
       mMainController.init();
   }

public void attachDisplay(Display display) {
       Preconditions.checkNotNull(display, "display is null");
       Preconditions.checkState(getDisplay() == null, "we currently have a display");
       setDisplay(display);
   }

 @Override
   protected void setDisplay(Display display) {
       super.setDisplay(display);
       mMovieController.setDisplay(display);
       mUserController.setDisplay(display);
       mAboutController.setDisplay(display);
   }

3.4.1.5 onPause()

清空所有的狀態

1
2
3
4
5
6
7
@Override
  protected void onPause() {
      mMainController.suspend();
      mMainController.setHostCallbacks(null);
      mMainController.detachDisplay(mDisplay);
      super.onPause();
  }

3.5 實現特定Controller的UI的Fragment

3.5.1 onResume

獲取到自己的Controller進行初始化

1
2
3
4
5
@Override
   public void onResume() {
       super.onResume();
       getController().attachUi(this);
   }

3.5.2 onPause

獲取到相應的Controller進行狀態清理

1
2
3
4
5
6
7
@Override
  public void onPause() {
      saveListViewPosition();
      cancelToast();
      getController().detachUi(this);
      super.onPause();
  }

總結

Philm中實現的MVP的架構,遵循了MVP的關鍵原則:將View和Model隔離。因為整個專案是基於該架構的,所以考慮的更全面,比如狀態的統一管理,所有Controller的統一管理,統一的CallBack的管理,使用Otto驅動事件實現元件間的解耦等。當你希望自己的整個專案完全使用MVP的架構時,Philm的框架無疑是一種非常值得參考的實現,你也可以根據自己的需求進行擴充套件。
另外Philm也使用了最新的MaterialDesign設計,有一些自定義的View,也是不錯的學習資料。

相關文章

http://blog.csdn.net/vector_yi/article/details/24719873

http://antonioleiva.com/mvp-android/
https://github.com/antoniolg/androidmvp

http://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/
https://github.com/android10/Android-CleanArchitecture

http://magenic.com/BlogArchive/AnMVPPatternforAndroid

http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html

http://blog.csdn.net/xijiaohuangcao/article/details/7925641

https://github.com/pedrovgs/EffectiveAndroidUI/


轉自:http://www.lightskystreet.com/2015/02/10/philm_mvp/

相關文章