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

来源:https://www.cnblogs.com/mengdd/archive/2018/09/25/android-mvp-pattern-and-mosby.html
-Advertisement-
Play Games

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介面, 繼承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是真的要銷毀, 還是為了屏幕旋轉要銷毀.

PS: 說到狀態保存和恢復, 之前的一篇博客寫得很詳細, 可以參考一下: Android Fragment使用(三) Activity, Fragment, WebView的狀態保存和恢復

其他

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

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一. 概述 本篇介紹在linux上見到的各種包管理系統(package management system,PMS)用來進行軟體安裝,管理,刪除的命令行工具。PMS是利用一個資料庫來記錄各種相關內容,包括: linux系統上已安裝了什麼軟體包,每個包安裝了什麼文件,每個已安裝軟體包的版本。 軟體包通 ...
  • 更多目錄知識 http://blog.51cto.com/yangrong/1288072 /etc/fstab 機自動掛載分區/磁碟,規定哪個分區/設備,掛載到哪裡 /etc/resolv.conf DNS臨時配置文件 /etc/hosts 主機名解析文件: hosts文件里存放ip地址與功能變數名稱的對 ...
  • 旭日Follow_24 的CSDN 博客 ,全文地址請點擊: https://blog.csdn.net/xuri24/article/details/81455449 一,前言 本文章是讀了“深入理解java虛擬機”一書的筆記記錄和心得。作為一名Java的開發從業者或愛好者,想要在這條路繼續和深入 ...
  • 一、前言 MySQL :是用於管理數據的軟體 MySQL是一種關係資料庫管理系統,關係資料庫將數據保存在不同的表中,而不是將所有數據放在一個大倉庫內,這樣就增加了速度並提高了靈活性。 分為服務端和客戶端(也是基於C/S架構的程式) 服務端: socket服務端 本地文件操作 解析指令(SQL語句) ...
  • pymysql的下載和使用 該模塊本質就是一個套接字客戶端軟體,使用前需要事先安裝,能夠讓我們在 Python程式中操作資料庫. pymysql模塊的下載: 在Python安裝文件中找到scripts文件 shift+右鍵打開powershell,接著如下圖: pymysql的使用 (數據均已存在) ...
  • 索引rebuild與rebuild online區別 1.0目的,本篇文檔探討索引rebuild 與 rebuild online的區別 2.0猜測:已有的知識 2.1對索引rebuild重建會對錶申請TM4級表鎖,將會影響業務修改數據,而對索引進行rebuild online則不影響業務修改數據, ...
  • 旭日Follow_24 的CSDN 博客 ,全文地址請點擊: https://blog.csdn.net/xuri24/article/details/80963801 慢查詢日誌概念 MySQL的慢查詢日誌是MySQL提供的一種日誌記錄,它用來記錄在MySQL中響應時間超過閥值的語句,具體指運行時 ...
  • 旭日Follow_24 的CSDN 博客 ,全文地址請點擊: https://mp.csdn.net/postedit/80910082 索引概念: 索引是關係資料庫中用於存放每一條記錄的一種對象,主要目的是加快數據的讀取速度和完整性檢查。建立索引是一項技術性要求高的工作。一般在資料庫設計階段的與數 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...