開源項目Philm的MVP架構分析
作者 : lightsky
原文鏈接前言
最近一直在研究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統一控制。
public final void init() { Preconditions.checkState(mInited == false, "Already inited"); mInited = true; onInited(); }
(2) suspend
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
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中調用。
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)方法。
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()方法統一初始化。
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中管理。
@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,進行更新。
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和狀態進行初始化
@Override protected void onInited() { super.onInited(); mState.registerForEvents(this); mUserController.init(); mMovieController.init(); mAboutController.init(); }
3.3.2 onSuspended
對所有Controller的CallBack和狀態進行清理
@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
mMainController = PhilmApplication.from(this).getMainController(); AndroidDisplay mDisplay = new AndroidDisplay(this, mDrawerLayout);
3.4.1.2 onResume
初始化所有的狀態
@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()
清空所有的狀態
@Override protected void onPause() { mMainController.suspend(); mMainController.setHostCallbacks(null); mMainController.detachDisplay(mDisplay); super.onPause(); }
3.5 實現特定Controller的UI的Fragment
3.5.1 onResume
獲取到自己的Controller進行初始化
@Override public void onResume() { super.onResume(); getController().attachUi(this); }
3.5.2 onPause
獲取到相應的Controller進行狀態清理
@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/