Android中的構架模式一直是一個很hot的topic, 近年來Architecture components推出之後, MVVM異軍突起, 風頭正在逐漸蓋過之前的MVP. 其實我覺得MVP還是有好處的, 比如靈活多變(其實只是我用起來更熟悉順手一些吧). 個人是沒有什麼偏見的, 關於項目的構架,... ...
Android中的構架模式一直是一個很hot的topic, 近年來Architecture components推出之後, MVVM異軍突起, 風頭正在逐漸蓋過之前的MVP.
其實我覺得MVP還是有好處的, 比如靈活多變(其實只是我用起來更熟悉順手一些吧).
個人是沒有什麼偏見的, 關於項目的構架, 只要找到適合的就行.
最近打算實際用一下mosby這個開源庫, 幫助構建一下mvp模式, 本文是我的心路歷程和代碼心得記錄.
關於MVP模式
前幾年MVP模式的風很大, 之前工作的項目也用的MVP模式, 所以對這個模式在team有很多討論.
可以說一千個人眼中有一千種MVP吧, 比如Presenter之後的數據邏輯, 是用Interactor呢, 還是用Repository呢, 如果用了CursorLoader, 那麼數據和View層直接耦合怎麼辦. 要不要給Presenter也定義介面呢, Presenter是註入呢還是在哪裡(比如基類Fragment里)初始化呢, P和V的attach到底是在P里做呢還是在V里做呢.
MVP的原則
儘管結合項目實際, 可能有很多變種, 但是不管怎麼變, MVP有幾個原則是要遵守的:
- Activity/Fragment實現View介面, View中的方法都只是和UI顯示相關的. View要儘可能的dummy, 不涉及業務邏輯, presenter告訴它乾什麼它乾什麼就行了.
- Presenter中沒有Android相關的類, 是一個純Java的程式. 這樣有利於解耦和測試. (所以一個檢查方法是看你的presenter的import中有沒有android的包名.)
- 註意生命周期的處理, 因為非同步任務callback返回之後View的狀態不一定還是活躍的, 所以要有一定的措施檢查View是否還在以及處理註銷等, 避免crash或記憶體泄露.
MVP的官方例子
MVP模式Google有個官方例子: android-architecture, 我之前寫了一篇解讀在這裡Google官方MVP Sample代碼解讀. (我剛看了一下官方sample代碼又更新了, 還得再看一下.)
官方的例子屬於比較正統的, 比如每個界面會定義一個Contract, 裡面分別定義View和Presenter的介面. 用Repository包裝local和remote的數據, local和remote的數據源會和repository實現相同的data source介面, 我非常喜歡RxJava版本的三級緩存處理.
我的一些小Demo
之前自己寫的一些比較完整的使用MVP的Demo:
- TodoRealm: 一個Todo任務管理器, 只有本地數據.
- ZhihuDaily: 知乎日報, 支持離線模式.
MVVM
自從Google官方推出了Android Architecture Components之後, 看起來MVVM也是一種不錯的選擇.
這是官方的例子: android-architecture-components.
我還正在學習中, 關於這個話題可能以後會單獨展開來講一下, 我先沉澱一下.
目前的心得: 這一套東西也很強大, 就是用起來不太習慣. 要遵循的套路太多, 感覺沒有使用MVP的時候那麼自由. (可能還是不太熟的緣故吧, 我還是不多說了. ==!)
所以在學習這套模式的時候我突然又懷念起MVP模式, 準備把之前一個爛尾的個人項目重新拯救一把. 就是這個: GithubClient. 這一次準備用個mvp的庫玩玩.
Mosby庫的使用和代碼分析
Mosby是一個幫你實現MVP或MVI的庫.
最近看介紹才發現它的名字是根據How I met your mother這個美劇的主角起的. (我最近才利用生病期間看完這個劇. 覺得真是巧合啊, 註定要用一用了.)
之前都是自己手動實現MVP的, 也沒什麼難的, 用這個庫會幫你解決什麼問題呢?
看看Mosby的介紹:
使用Mosby的基本步驟:
- View介面繼承
MvpView
. - Presenter: 如果有規定Presenter介面, 介面繼承
MvpPresenter<View>
, 其中View是對應的View介面, 實現類繼承MvpBasePresenter<View>
.
如果沒有Presenter的介面而直接是實現類也可以, 同樣也是實現類繼承MvpBasePresenter<View>
. - Activity或Fragment實現View介面, 繼承
MvpActivity
或MvpFragment
, 泛型參數類型傳入對應的View介面和Presenter類型即可. - Activity或Fragment實現抽象的
createPresenter()
方法, 在其中創建Presenter的實例.
好了, 所有必須的工作就做完了, mosby的類會處理初始化和實例保存等.
Activity/Fragment中不需要保存presenter的欄位, Presenter中也不需要保存View的欄位. 這些都在基類中保存了.
Mosby的實現
關於Mosby的實現可以查看它的類, 裡面有詳細的註釋.
生命周期
MvpActivity
中用了ActivityMvpDelegateImpl
, 在Activity的每一個生命周期回調中做一些事情.
在onCreate()
中創建了Presenter, 把它賦值給欄位, 並且attachView(); 在onDestroy()
中detachView()和調用presenter的destroy()來做一些清理工作.MvpFragment
中用了FragmentMvpDelegateImpl
, 在Fragment的生命周期中做一些事情: 在onCreate()
中創建Presenter, 賦值給欄位;onViewCreated()
中attachView();onDestroyView()
中detachView();onDestroy()
中調用presenter的destroy()來做一些清理工作.
所以presenter的初始化, 和view的attach/detach, 以及它們變數的保存都是mosby幫我們處理好了.- mosby還支持ViewGroup作為View, 它提供了
MvpFrameLayout
,MvpLinearLayout
和MvpRelativeLayout
以供繼承, Delegate的實現類是ViewGroupMvpDelegateImpl
, 用到的生命周期主要是onAttachedToWindow()
和#onDetachedFromWindow()
.
Presenter中調用View的方法
MvpBasePresenter
的實現沒有什麼特殊的, 主要是存了一個View的WeakReference. 新版中推薦使用ifViewAttached(ViewAction<V>)
方法來把判斷和執行一次性做了. 原來的isViewAttached()
和getView()
已經標記為deprecated了.
關於這樣做的原因, 在這裡有討論: https://github.com/sockeqwe/mosby/issues/233.
屏幕旋轉時的狀態保存
mosby是處理了屏幕旋轉時的狀態保存的, 可以看到初始化ActivityMvpDelegateImpl
時預設第三個參數是true, 即屏幕旋轉時保存狀態.
具體做法是通過PresenterManager
把presenter保存起來.
保存的時候傳了activity和一個生成的viewId:
private P createViewIdAndCreatePresenter() {
P presenter = delegateCallback.createPresenter();
if (presenter == null) {
throw new NullPointerException(
"Presenter returned from createPresenter() is null. Activity is " + activity);
}
if (keepPresenterInstance) {
mosbyViewId = UUID.randomUUID().toString();
PresenterManager.putPresenter(activity, mosbyViewId, presenter);
}
return presenter;
}
恢復狀態的時候需要把之前存的Presenter拿出來還是用activity的實例和viewId:
@Nullable public static <P> P getPresenter(@NonNull Activity activity, @NonNull String viewId) {
if (activity == null) {
throw new NullPointerException("Activity is null");
}
if (viewId == null) {
throw new NullPointerException("View id is null");
}
ActivityScopedCache scopedCache = getActivityScope(activity);
return scopedCache == null ? null : (P) scopedCache.getPresenter(viewId);
}
其中viewId是通過bundle保存和恢復出來的:
@Override public void onSaveInstanceState(Bundle outState) {
if (keepPresenterInstance && outState != null) {
outState.putString(KEY_MOSBY_VIEW_ID, mosbyViewId);
if (DEBUG) {
Log.d(DEBUG_TAG,
"Saving MosbyViewId into Bundle. ViewId: " + mosbyViewId + " for view " + getMvpView());
}
}
}
那麼問題來了:
- 1.既然我們已經有了一個viewId作為key, 為什麼還需要activity來作為查詢條件?
- 2.如果真的需要這個條件, 那麼屏幕旋轉以後activity都重建了, 如何通過新的activity實例獲得之前的Presenter呢?
首先我是在代碼中找到了第二個問題的答案, 即兩個不同的activity是如何關聯起來的:
static final Application.ActivityLifecycleCallbacks activityLifecycleCallbacks =
new Application.ActivityLifecycleCallbacks() {
@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
if (savedInstanceState != null) {
String activityId = savedInstanceState.getString(KEY_ACTIVITY_ID);
if (activityId != null) {
// After a screen orientation change we map the newly created Activity to the same
// Activity ID as the previous activity has had (before screen orientation change)
activityIdMap.put(activity, activityId);
}
}
}
@Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
// Save the activityId into bundle so that the other
String activityId = activityIdMap.get(activity);
if (activityId != null) {
outState.putString(KEY_ACTIVITY_ID, activityId);
}
}
...
@Override public void onActivityDestroyed(Activity activity) {
if (!activity.isChangingConfigurations()) {
// Activity will be destroyed permanently, so reset the cache
String activityId = activityIdMap.get(activity);
if (activityId != null) {
ActivityScopedCache scopedCache = activityScopedCacheMap.get(activityId);
if (scopedCache != null) {
scopedCache.clear();
activityScopedCacheMap.remove(activityId);
}
// No Activity Scoped cache available, so unregister
if (activityScopedCacheMap.isEmpty()) {
// All Mosby related activities are destroyed, so we can remove the activity lifecylce listener
activity.getApplication()
.unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks);
if (DEBUG) {
Log.d(DEBUG_TAG, "Unregistering ActivityLifecycleCallbacks");
}
}
}
}
activityIdMap.remove(activity);
}
};
通過Bundle存取傳遞一個activityId, 新創建的activity實例和舊的activity實例就有相同的id. 這個關係存儲在Map<Activity, String> activityIdMap
里.
這樣在新的activity中通過map查詢到activityId之後, 在Map<String, ActivityScopedCache> activityScopedCacheMap
中再通過activityId查到了ActivityScopedCache對象, 再用viewId作為key查詢到presenter.
看了onActivityDestroyed()
部分的代碼之後也終於明白了第一個問題的答案, 即這樣做的原因, 如果只用viewId, 我們是解決了存放和查詢, 但是沒有解決釋放的問題.
因為我們的需求只是在屏幕旋轉的情況下保存presenter的實例, 我們仍然需要在activity真的銷毀的時候釋放對presenter實例的保存.
這裡用了activity.isChangingConfigurations()
的條件來區分activity是真的要銷毀, 還是為了屏幕旋轉要銷毀.
PS: 說到狀態保存和恢復, 之前的一篇博客寫得很詳細, 可以參考一下: Android Fragment使用(三) Activity, Fragment, WebView的狀態保存和恢復
其他
Mosby還支持LCE(Loading-Content-Error)和ViewState, 為開發者省去更多套路化的代碼, 還有處理屏幕旋轉之後的狀態恢復.
有空的時候再寫一篇扒一扒吧.
歡迎關註微信公眾號: 聖騎士Wind