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