1. 程式人生 > >MVP模式, 開源庫mosby的使用及程式碼分析

MVP模式, 開源庫mosby的使用及程式碼分析

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工作管理員, 只有本地資料.

MVVM

我還正在學習中, 關於這個話題可能以後會單獨展開來講一下, 我先沉澱一下.

目前的心得: 這一套東西也很強大, 就是用起來不太習慣. 要遵循的套路太多, 感覺沒有使用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介面, 繼承MvpActivityMvpFragment, 泛型引數型別傳入對應的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, MvpLinearLayoutMvpRelativeLayout以供繼承, 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是真的要銷燬, 還是為了螢幕旋轉要銷燬.

其他

Mosby還支援LCE(Loading-Content-Error)和ViewState, 為開發者省去更多套路化的程式碼, 還有處理螢幕旋轉之後的狀態恢復.
有空的時候再寫一篇扒一扒吧.

歡迎關注微信公眾號: 聖騎士Wind
微信公眾號