[譯]使用MVI打造響應(yīng)式APP(五):輕而易舉地Debug

原文:REACTIVE APPS WITH MODEL-VIEW-INTENT - PART5 - DEBUGGING WITH EASE
作者:Hannes Dorfmann
譯者:卻把清梅嗅

前文我們探討了Model-View-Intent (MVI)架構(gòu)模式及其相關(guān)特性,在 第一篇文章 中,我們談到了 單項數(shù)據(jù)流的重要性應(yīng)用狀態(tài)應(yīng)該被業(yè)務(wù)邏輯驅(qū)動。本文我們將展示這種架構(gòu)模式會怎樣回報開發(fā)者,它可以讓開發(fā)者在開發(fā)過程中更輕而易舉進行debug。

遇到過這樣的情況嘛?你得到了一個崩潰的報告,但是你無法復現(xiàn)這個BUG。聽起來似曾相識?我也是!在花了很多時間查看堆棧跟蹤和項目的源碼后,最終我選擇了放棄——關(guān)閉了這個issue,并提交了一個類似 無法復現(xiàn) 或者 某個Android生產(chǎn)商的某種特定的機型導致的特殊錯誤 的備注。

以我們的購物App舉例來說,在Home界面,用戶以某種方式進行下拉刷新,但不知道為什么,崩潰報告告訴我,當用戶執(zhí)行下拉刷新獲取最新數(shù)據(jù)的操作時,應(yīng)用拋出了一個NullPointerException。

因此,作為開發(fā)人員,您啟動App并嘗試在Home界面進行下拉刷新,但App并沒有崩潰, 它按照預(yù)期正常地運行。然后您開始仔細檢查自己的代碼,但是就是找不到哪里會導致NullPointerException的發(fā)生。你打開了debug模式,一行一行逐步執(zhí)行該界面相關(guān)的代碼,但App仍然正常的運行—— 到底怎么樣才能讓它在下拉刷新時崩潰?

問題的根本在于你不能在App崩潰發(fā)生之前復現(xiàn)狀態(tài),如果遇到崩潰的用戶可以在崩潰報告中提供他App的狀態(tài)(在崩潰發(fā)生之前)以及堆棧跟蹤,那不是很棒嗎?

通過 單向數(shù)據(jù)流Model-View-Intent ,這簡直輕而易舉。

用戶執(zhí)行所有Intent界面對Model進行渲染時,我們很方便地能夠?qū)⑺鼈冞M行打印,讓我們通過在HomePresenter中添加Log來為Home界面執(zhí)行這樣的操作(具體代碼請參考 第三節(jié),該小節(jié)我們針對狀態(tài)折疊器進行了探討)。

在以下代碼片段中,我們使用Crashlytics(譯者注:一種崩潰報告工具),使用其它的崩潰報告工具也是一樣的:

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(...); // 加載數(shù)據(jù)的業(yè)務(wù)邏輯

    Observable<PartialState> pullToRefresh = intent(HomeView::pullToRefreshIntent)
          .doOnNext(intent -> Crashlytics.log("Intent: pull-to-refresh"))
          .flatmap(...); // 加載數(shù)據(jù)的業(yè)務(wù)邏輯

    Observable<PartialState> nextPage = intent(HomeView::loadNextPageIntent)
          .doOnNext(intent -> Crashlytics.log("Intent: load next page"))
          .flatmap(...); // 加載數(shù)據(jù)的業(yè)務(wù)邏輯

    Observable<PartialState> allIntents = Observable.merge(loadFirstPage, pullToRefresh, nextPage);
    Observable<HomeViewState> stateObservable = allIntents
          .scan(initialState, this::viewStateReducer) // 對狀態(tài)進行折疊
          .doOnNext(newViewState -> Crashlytics.log( "State: "+gson.toJson(newViewState) ));

    subscribeViewState(stateObservable, HomeView::render); // 展示新的狀態(tài)
  }

  private HomeViewState viewStateReducer(HomeViewState previousState, PartialState changes){
    ...
  }
}

通過RxJava.doOnNext() 操作符,我們可以很輕松將每個intent和每個intentresult——也就是即將渲染在view層上的狀態(tài)進行打印。

我們將view的狀態(tài)序列化為json字符串,現(xiàn)在,我們的崩潰報告變成了這樣:

現(xiàn)在來看看這些日志,我們不僅能看到崩潰發(fā)生之前的最后一個狀態(tài),而且還能看到用戶達到這個狀態(tài)所經(jīng)歷的完整歷史記錄——為了保證可讀性,我將data字段內(nèi)的內(nèi)容替換為了[...]:

  • 1.用戶啟動了App,通過加載首頁數(shù)據(jù)的intent,這樣loadingFirstPage的值為true,使得加載指示器展示了出來,同時數(shù)據(jù)也被加載完畢(data[…])。

  • 2.接下來用戶滾動列表,并達到了列表的底部,這觸發(fā)了加載下一頁數(shù)據(jù)的intent,并開始加載更多的數(shù)據(jù)(分頁),這也導致了loadingNextPage狀態(tài)的改變,它的值變成了true。

  • 3.一旦分頁數(shù)據(jù)被加載成功,loadingNextPage狀態(tài)改變成了false,用戶再次重復操作達到了列表的底部,并又一次出發(fā)了觸發(fā)了加載下一頁數(shù)據(jù)的intent。

  • 4.接下來用戶開始嘗試下拉刷新的intent,這導致loadingPullToRefresh狀態(tài)變更為了true,然后,App突然發(fā)生了崩潰—— 這之后就沒有更多日志了。

這些信息如何幫助我們解決這個bug呢?顯然,我們知道用戶觸發(fā)了哪些操作,因此我們完全可以手動復現(xiàn)這個崩潰。此外,因為我們將App的狀態(tài)用json進行表現(xiàn),因此我們可以簡單地使用最后一個狀態(tài),反序列化json并將此狀態(tài)作為我們的初始狀態(tài)來修復該錯誤:

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

接下來我們打開了Debug調(diào)試工具,并嘗試觸發(fā)下拉刷新的intent,事實證明,如果用戶向下滾動頁面2次,則沒有更多數(shù)據(jù)可用,并且我們的App并沒有進行相應(yīng)的處理,因此后續(xù)的下拉刷新操作導致了崩潰。

結(jié)語

一個應(yīng)用狀態(tài)隨時隨地 可快照App可以使我們開發(fā)人員的生活更加輕松。我們不僅能夠輕松的 復現(xiàn)崩潰,而且可以將狀態(tài)進行序列化來 編寫回歸測試,并且這幾乎沒有什么成本。

請記住,這些便利只有在App的狀態(tài)遵循 單項數(shù)據(jù)流 、不可變純函數(shù) 的原則的情況下才能享受到(即被業(yè)務(wù)邏輯驅(qū)動),Model-View-Intent讓我們偏向了這種思想流派,而這個架構(gòu)模式中有一個非常棒并且有效的額外的效果,那就是本文所提到的構(gòu)建了一個 可快照App。

可快照 的應(yīng)用有什么缺陷呢?顯然我們正在將App的狀態(tài)序列化(比如通過Gson).這增加了一些額外的計算資源的負荷,平均來算的話,狀態(tài)第一次被Gson序列化大約需要30毫秒,因為Gson必須使用反射來掃描類,以確定必須序列化的字段。

Nexus 4上,狀態(tài)的連續(xù)序列化平均需要大約6毫秒。由于序列化在.doOnNext()中運行,雖然這通常在后臺線程上運行,但的確是這樣:我的App用戶必須比其它應(yīng)用的用戶多等待6毫秒,才能在屏幕上看到新的狀態(tài)。

我的觀點是,這對于用戶來說也許并不明顯,但是對狀態(tài)進行 快照 的一個問題是,在崩潰時,崩潰報告工具從用戶設(shè)備上傳到其服務(wù)器的數(shù)據(jù)量要大得多—— 如果用戶通過wifi連接,這無關(guān)痛癢,但如果用戶處于移動網(wǎng)絡(luò)下則可能會有一定的爭議。

最后,將狀態(tài)附加在崩潰報告中時,您可能會泄漏用戶的一些敏感的數(shù)據(jù)。針對這個問題,一個方案是不序列化敏感數(shù)據(jù),但這可能導致連接到崩潰報告的狀態(tài)不完整(因此這些報告可能幾乎無用),另外一個方案則是將敏感數(shù)據(jù)進行加密——但這可能需要一些額外的CPU占用。

總結(jié)一下:我個人認為這樣 可快照App有很多優(yōu)點,但是,你可能需要做出一些權(quán)衡。也許您開始為內(nèi)部版本或beta版本啟用App快照,以衡量它其產(chǎn)生的作用。


系列目錄

《使用MVI打造響應(yīng)式APP》原文

《使用MVI打造響應(yīng)式APP》譯文

《使用MVI打造響應(yīng)式APP》實戰(zhàn)


關(guān)于我

Hello,我是卻把清梅嗅,如果您覺得文章對您有價值,歡迎 ??,也歡迎關(guān)注我的博客或者Github。

如果您覺得文章還差了那么點東西,也請通過關(guān)注督促我寫出更好的文章——萬一哪天我進步了呢?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容