[譯]使用 MVI 編寫響應式 APP — 第五部分 — 簡單的除錯

pcdack發表於2018-03-19

使用 MVI 編寫響應式 APP — 第 5 部分 — 簡單的除錯

在前面的系列部落格中我們已經討論了 Model-View-Intent(MVI)模式和它的特徵。在第一部分我們已經討論了關於單向資料流的重要性和“業務邏輯”驅動型的應用狀態的概念。在這篇部落格中我們將看到如何通過 debug 來簡化開發者的開發工作。

你以前有沒有收到一個崩潰報告,並且你不能復現報告中的 bug?聽起來很熟悉?我也覺得很熟悉!在花費數小時看 stacktrace 和我們的原始碼,我選擇在 issue 跟蹤中關閉掉了這樣的報告,而且跟隨著一個小的 comment 像“不能復現這個 bug”或者“這一定是一個奇怪裝置/廠商(大廠)導致的錯誤”。

用我們在這系列部落格裡開發的購物車 app 做例子:當在 home 頁面,我們的使用者可以做下拉重新整理,崩潰的報告顯示,由於某種未知的原因,當下拉重新整理載入新資料的時候,會觸發 NullPointerException 異常。

你做為開發這開始在 home 頁面進行上拉重新整理操作,但是,這個 App 並沒有崩潰。它像預期的那樣工作。因此,你關閉了程式碼。但是,你不能看到 NullPointException 在這裡如何被丟擲的。接著你開始了斷點除錯,一步一步地執行相關元件的程式碼,但是它仍舊是在正常工作。特喵的怎麼才能重現這個 bug 呢?

這個問題是你不能夠重現當崩潰發生的時候的場景。如果有使用者在遇到崩潰問題時,能夠給你崩潰報告,包含 App(發生崩潰前)的狀態資訊和呼叫堆疊資訊,豈不美哉?伴隨著單項資料流和 Model-View-Intent 模式那麼這種情況將變得十分簡單。我們簡單記錄使用者觸發的所有的 intent 和渲染到 view 上的 model(model 代表了 app 的狀態、view 的狀態)。 讓我們在 home 頁面上這樣去做,在 HomePresenter 類上新增 log (對於更多的細節可以看第三部分 在第三部分中我們已經討論過狀態摺疊器的優點)。在下面的程式碼中我將貼出我們使用 Crashlytics(類似於 Bugly) 的程式碼片段,但是它應當與其他的 crash 報告工具的使用是相同的。

class HomePresenter extends MviBasePresenter<HomeView, HomeViewState> {

  private final HomeViewState initialState; // Show loading indicator

  public HomePresenter(HomeViewState initialState){
    this.initialState = initialState;
  }

  @Override protected void bindIntents() {

    Observable<PartialState> loadFirstPage = intent(HomeView::loadFirstPageIntent)
          .doOnNext(intent -> Crashlytics.log("Intent: load first page"))
          .flatmap(...); // business logic calls to load data

    Observable<PartialState> pullToRefresh = intent(HomeView::pullToRefreshIntent)
          .doOnNext(intent -> Crashlytics.log("Intent: pull-to-refresh"))
          .flatmap(...); // business logic calls to load data

    Observable<PartialState> nextPage = intent(HomeView::loadNextPageIntent)
          .doOnNext(intent -> Crashlytics.log("Intent: load next page"))
          .flatmap(...); // business logic calls to load data

    Observable<PartialState> allIntents = Observable.merge(loadFirstPage, pullToRefresh, nextPage);
    Observable<HomeViewState> stateObservable = allIntents
          .scan(initialState, this::viewStateReducer) // call the state reducer
          .doOnNext(newViewState -> Crashlytics.log( "State: "+gson.toJson(newViewState) ));

    subscribeViewState(stateObservable, HomeView::render); // display new state
  }

  private HomeViewState viewStateReducer(HomeViewState previousState, PartialState changes){
    ...
  }
}
複製程式碼

應用RxJava的 .doOnNext() 操作符,在每個 intent、每個 intent 的結果和之後渲染到 view 上的狀態上新增日誌,我們序列化 view 狀態為json物件(我們稍後來討論這個)。

我們可以看一下這些 logs:

logs

看一下這些 log,我們不僅可以看應用崩潰前的最新狀態,而且可以看到使用者達到這個狀態的整個過程。為了更好的可讀性,我已經強調了狀態過濾,並且用_[…]_替換掉“資料”(這些項將被顯示到 recycler view 上)。 因此,使用者開啟這個 app -載入第一頁的意圖。然後載入指示條顯示"loadFirstPage"。然後,真的資料就被載入進來了(data[…])。 接下來使用者滑動列表項並且到達了 recyclerView 的底部,這將觸發載入下一頁的意圖去載入更多資料(分頁),這將造成狀態轉換成"loadingNextPage":對。一旦下一頁被載入的資料(data[…])已經被更新並且"loadNextPage":錯誤已經被矯正。使用者第二次做同樣的事情。並且它開始採用下拉重新整理意圖並且狀態,狀態轉變為“loadingPullRefresh”:true。突然 App 崩潰了(沒有更多之後的 log 資訊)。

因此如何利用這些資訊幫助我們修復這個 bug?顯然,我們知道那個意圖使用者觸發了,因此我們可以人工去復現 bug。此外,我們可以將我們的 app 的狀態快照成 json。我們可以簡單的將最後一個狀態反序列化 json,並且成為我們的初始狀態去修復這個 Bug:

String json ="  {\"data\":[...],\"loadingFirstPage\":false,\"loadingNextPage\":false,\"loadingPullToRefresh\":false} ";
HomeViewState stateBeforeCrash = gson.fromJson(json, HomeViewState.class);
HomePresenter homePresenter = new HomePresenter(stateBeforeCrash);
複製程式碼

然後,我們開啟除錯工具,觸發下拉重新整理的意圖(intent)。它將出現在如果使用者已經向下滑第二次滑到第二頁沒有更多的資料存在,並且,我們的 app 沒有正確的處理,因此下拉重新整理造成了崩潰。

總結

製作 app 的狀態"快照"讓我們的開發工作更加輕鬆。不僅我們可以容易的復現崩潰場景,另外,我們可以序列化狀態去寫迴歸測試,不用額外消耗任意程式碼。記住這僅僅適用於如果 app 的狀態遵循單項資料流(被業務邏輯驅動),不變性和純函式的原則。Model-View-Intent 帶領我們去正確的方向,因此我們構建“可快照”的 app 是非常好和十分有用,這就是這種架構的“副作用”。

"可快照的" app 有什麼缺點?顯然我們序列化 app 的狀態(例如:使用 Gson)。這將新增額外的計算時間。在我的一般大小的 app 中,首次使用 Gson 序列化需要大約 30 毫秒。因為 Gson 需要使用反射來掃描類去決定需要序列化的欄位。隨後的狀態序列化在 Nexus 4 中平均需要花費 6 毫秒。當序列化執行在 .doOnNext() 這是一般執行在其他執行緒,但是,我 app 的使用者不得不等 6 毫秒比那些沒有快照的 app。我的觀點是等 6 毫秒使用者是很難察覺到。無論如何,關於快照狀態的一個討論是當崩潰發生時,從使用者的裝置通過崩潰日誌工具向伺服器上傳的資料量是十分巨大的。如果使用者連線著 wifi 沒什麼大不了的,但可能對於在使用手機流量的使用者確實是一個問題。最後但是也很重要的一點,你也許洩露了伴隨著狀態的敏感資料的崩潰日誌。要麼就不要在上傳的崩潰報告中去序列化那些敏感的資料(因此報告可能不完整並且幾乎沒啥用),要麼就將這敏感資料加密(這可能需要一些額外的CPU時間)。

總結一下:就我個人而言,在給我的 app 做快照處理時我發現了很多益處,然而,你也不得不做一些權衡.也許你可以在內部版本或者 beta 版本上啟用快照功能,看看在你自己的 app 上工作得如何。

紅利:時間旅行

在開發時,如果可以擁有時間旅行的選擇項,豈不美哉。也許嵌入一個除錯側邊欄像 Jake Wharton 的 u2020 dome app。

所有我們需要類似於除錯側邊欄只需要兩個按鈕“前一個狀態”和“後一個狀態”因此我們可以一步一步地從一個狀態及時的到前一個狀態(或下一個狀態)。例如:如果我們已經做了一個 HTTP 請求作為狀態變化的一部分,可以確定的是,在往前回溯時,我們並不想再次進行真正的 http 請求,因為與此同時後端的資料也可能會發生變化。

時間旅行要求一些額外的層,像一個代理層在一個 app 的邊界部分。因此我們可以“錄製”和“回放”狀態像 http 請求(同理 sqlite等等)。對這類事情十分的感興趣?這就像我的朋友 Felipe 為OKHttp做類似的事情。可以隨意聯絡他來得到他正在寫的庫的更多細節。

Snipaste_2018-03-07_11-40-30.png

你是否正在找一個十分有用的安卓庫,可以錄製和回放 OkHttp 網路互動,比如說 Espresso 測試?

— Felipe Lima (@felipecsl) 28. Februar 2017

這篇部落格是使用 MVI 開發響應式 APP 的一部分。 這裡是內容表:

這是中文翻譯:


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章