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

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

在前面的系列博客中咱們已經討論了 Model-View-Intent(MVI)模式和它的特徵。在第一部分咱們已經討論了關於單向數據流的重要性和「業務邏輯」驅動型的應用狀態的概念。在這篇博客中咱們將看到如何經過 debug 來簡化開發者的開發工做。前端

你之前有沒有收到一個崩潰報告,而且你不能復現報告中的 bug?聽起來很熟悉?我也以爲很熟悉!在花費數小時看 stacktrace 和咱們的源代碼,我選擇在 issue 跟蹤中關閉掉了這樣的報告,並且跟隨着一個小的 comment 像「不能復現這個 bug」或者「這必定是一個奇怪設備/廠商(大廠)致使的錯誤」。java

用咱們在這系列博客裏開發的購物車 app 作例子:當在 home 頁面,咱們的用戶能夠作下拉刷新,崩潰的報告顯示,因爲某種未知的緣由,當下拉刷新加載新數據的時候,會觸發 NullPointerException 異常。android

你作爲開發這開始在 home 頁面進行上拉刷新操做,可是,這個 App 並無崩潰。它像預期的那樣工做。所以,你關閉了代碼。可是,你不能看到 NullPointException 在這裏如何被拋出的。接着你開始了斷點調試,一步一步地運行相關組件的代碼,可是它仍舊是在正常工做。特喵的怎麼才能重現這個 bug 呢?ios

這個問題是你不可以重現當崩潰發生的時候的場景。若是有用戶在遇到崩潰問題時,可以給你崩潰報告,包含 App(發生崩潰前)的狀態信息和調用堆棧信息,豈不美哉?伴隨着單項數據流和 Model-View-Intent 模式那麼這種狀況將變得十分簡單。咱們簡單記錄用戶觸發的全部的 intent 和渲染到 view 上的 model(model 表明了 app 的狀態、view 的狀態)。 讓咱們在 home 頁面上這樣去作,在 HomePresenter 類上添加 log (對於更多的細節能夠看第三部分 在第三部分中咱們已經討論過狀態摺疊器的優勢)。在下面的代碼中我將貼出咱們使用 Crashlytics(相似於 Bugly) 的代碼片斷,可是它應當與其餘的 crash 報告工具的使用是相同的。git

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對象(咱們稍後來討論這個)。github

咱們能夠看一下這些 logs:sql

logs

看一下這些 log,咱們不只能夠看應用崩潰前的最新狀態,並且能夠看到用戶達到這個狀態的整個過程。爲了更好的可讀性,我已經強調了狀態過濾,而且用_[…]_替換掉「數據」(這些項將被顯示到 recycler view 上)。 所以,用戶開啓這個 app -加載第一頁的意圖。而後加載指示條顯示"loadFirstPage"。而後,真的數據就被加載進來了(data[…])。 接下來用戶滑動列表項而且到達了 recyclerView 的底部,這將觸發加載下一頁的意圖去加載更多數據(分頁),這將形成狀態轉換成"loadingNextPage":對。一旦下一頁被加載的數據(data[…])已經被更新而且"loadNextPage":錯誤已經被矯正。用戶第二次作一樣的事情。而且它開始採用下拉刷新意圖而且狀態,狀態轉變爲「loadingPullRefresh」:true。忽然 App 崩潰了(沒有更多以後的 log 信息)。json

所以如何利用這些信息幫助咱們修復這個 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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索