原文:REACTIVE APPS WITH MODEL-VIEW-INTENT - PART5 - DEBUGGING WITH EASE
作者:Hannes Dorfmann
譯者:卻把清梅嗅
前文我們探討了Model-View-Intent (MVI)
架構模式及其相關特性,在 第一篇文章 中,我們談到了 單項資料流的重要性 和 應用狀態應該被業務邏輯驅動。本文我們將展示這種架構模式會怎樣回報開發者,它可以讓開發者在開發過程中更輕而易舉進行debug。
遇到過這樣的情況嘛?你得到了一個崩潰的報告,但是你無法復現這個BUG
。聽起來似曾相識?我也是!在花了很多時間檢視堆疊跟蹤和專案的原始碼後,最終我選擇了放棄——關閉了這個issue
,並提交了一個類似 無法復現 或者 某個Android生產商的某種特定的機型導致的特殊錯誤 的備註。
以我們的購物App
舉例來說,在Home
介面,使用者以某種方式進行下拉重新整理,但不知道為什麼,崩潰報告告訴我,當使用者執行下拉重新整理獲取最新資料的操作時,應用丟擲了一個NullPointerException
。
因此,作為開發人員,您啟動App
並嘗試在Home
介面進行下拉重新整理,但App
並沒有崩潰, 它按照預期正常地執行。然後您開始仔細檢查自己的程式碼,但是就是找不到哪裡會導致NullPointerException
的發生。你開啟了debug
模式,一行一行逐步執行該介面相關的程式碼,但App
仍然正常的執行—— 到底怎麼樣才能讓它在下拉重新整理時崩潰?
問題的根本在於你不能在App
崩潰發生之前復現狀態,如果遇到崩潰的使用者可以在崩潰報告中提供他App
的狀態(在崩潰發生之前)以及堆疊跟蹤,那不是很棒嗎?
通過 單向資料流 和 Model-View-Intent ,這簡直輕而易舉。
在 使用者執行所有Intent 和 介面對Model進行渲染時,我們很方便地能夠將它們進行列印,讓我們通過在HomePresenter
中新增Log
來為Home
介面執行這樣的操作(具體程式碼請參考 第三節,該小節我們針對狀態摺疊器進行了探討)。
在以下程式碼片段中,我們使用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(...); // 載入資料的業務邏輯
Observable<PartialState> pullToRefresh = intent(HomeView::pullToRefreshIntent)
.doOnNext(intent -> Crashlytics.log("Intent: pull-to-refresh"))
.flatmap(...); // 載入資料的業務邏輯
Observable<PartialState> nextPage = intent(HomeView::loadNextPageIntent)
.doOnNext(intent -> Crashlytics.log("Intent: load next page"))
.flatmap(...); // 載入資料的業務邏輯
Observable<PartialState> allIntents = Observable.merge(loadFirstPage, pullToRefresh, nextPage);
Observable<HomeViewState> stateObservable = allIntents
.scan(initialState, this::viewStateReducer) // 對狀態進行摺疊
.doOnNext(newViewState -> Crashlytics.log( "State: "+gson.toJson(newViewState) ));
subscribeViewState(stateObservable, HomeView::render); // 展示新的狀態
}
private HomeViewState viewStateReducer(HomeViewState previousState, PartialState changes){
...
}
}
複製程式碼
通過RxJava
的 .doOnNext() 操作符,我們可以很輕鬆將每個intent
和每個intent
的result
——也就是即將渲染在view
層上的狀態進行列印。
我們將view
的狀態序列化為json字串,現在,我們的崩潰報告變成了這樣:
現在來看看這些日誌,我們不僅能看到崩潰發生之前的最後一個狀態,而且還能看到使用者達到這個狀態所經歷的完整歷史記錄——為了保證可讀性,我將data
欄位內的內容替換為了[...]:
- 1.使用者啟動了
App
,通過載入首頁資料的intent
,這樣loadingFirstPage
的值為true
,使得載入指示器展示了出來,同時資料也被載入完畢(data[…])。 - 2.接下來使用者滾動列表,並達到了列表的底部,這觸發了載入下一頁資料的
intent
,並開始載入更多的資料(分頁),這也導致了loadingNextPage
狀態的改變,它的值變成了true
。 - 3.一旦分頁資料被載入成功,
loadingNextPage
狀態改變成了false
,使用者再次重複操作達到了列表的底部,並又一次出發了觸發了載入下一頁資料的intent
。 - 4.接下來使用者開始嘗試下拉重新整理的
intent
,這導致loadingPullToRefresh
狀態變更為了true
,然後,App
突然發生了崩潰—— 這之後就沒有更多日誌了。
這些資訊如何幫助我們解決這個bug呢?顯然,我們知道使用者觸發了哪些操作,因此我們完全可以手動復現這個崩潰。此外,因為我們將App
的狀態用json
進行表現,因此我們可以簡單地使用最後一個狀態,反序列化json並將此狀態作為我們的初始狀態來修復該錯誤:
String json =" {\"data\":[...],\"loadingFirstPage\":false,\"loadingNextPage\":false,\"loadingPullToRefresh\":false} ";
HomeViewState stateBeforeCrash = gson.fromJson(json, HomeViewState.class);
HomePresenter homePresenter = new HomePresenter(stateBeforeCrash);
複製程式碼
接下來我們開啟了Debug
除錯工具,並嘗試觸發下拉重新整理的intent
,事實證明,如果使用者向下滾動頁面2次,則沒有更多資料可用,並且我們的App
並沒有進行相應的處理,因此後續的下拉重新整理操作導致了崩潰。
結語
一個應用狀態隨時隨地 可快照 的App
可以使我們開發人員的生活更加輕鬆。我們不僅能夠輕鬆的 復現崩潰,而且可以將狀態進行序列化來 編寫回歸測試,並且這幾乎沒有什麼成本。
請記住,這些便利只有在App
的狀態遵循 單項資料流 、不可變、純函式 的原則的情況下才能享受到(即被業務邏輯驅動),Model-View-Intent
讓我們偏向了這種思想流派,而這個架構模式中有一個非常棒並且有效的額外的效果,那就是本文所提到的構建了一個 可快照 的App
。
可快照 的應用有什麼缺陷呢?顯然我們正在將App
的狀態序列化(比如通過Gson
).這增加了一些額外的計算資源的負荷,平均來算的話,狀態第一次被Gson
序列化大約需要30毫秒,因為Gson
必須使用反射來掃描類,以確定必須序列化的欄位。
在Nexus 4
上,狀態的連續序列化平均需要大約6毫秒。由於序列化在.doOnNext()
中執行,雖然這通常在後臺執行緒上執行,但的確是這樣:我的App
使用者必須比其它應用的使用者多等待6毫秒,才能在螢幕上看到新的狀態。
我的觀點是,這對於使用者來說也許並不明顯,但是對狀態進行 快照 的一個問題是,在崩潰時,崩潰報告工具從使用者裝置上傳到其伺服器的資料量要大得多—— 如果使用者通過wifi連線,這無關痛癢,但如果使用者處於行動網路下則可能會有一定的爭議。
最後,將狀態附加在崩潰報告中時,您可能會洩漏使用者的一些敏感的資料。針對這個問題,一個方案是不序列化敏感資料,但這可能導致連線到崩潰報告的狀態不完整(因此這些報告可能幾乎無用),另外一個方案則是將敏感資料進行加密——但這可能需要一些額外的CPU佔用。
總結一下:我個人認為這樣 可快照 的App
有很多優點,但是,你可能需要做出一些權衡。也許您開始為內部版本或beta版本啟用App
快照,以衡量它其產生的作用。
系列目錄
《使用MVI打造響應式APP》原文
《使用MVI打造響應式APP》譯文
《使用MVI打造響應式APP》實戰
關於我
Hello,我是卻把清梅嗅,如果您覺得文章對您有價值,歡迎 ❤️,也歡迎關注我的部落格或者Github。
如果您覺得文章還差了那麼點東西,也請通過關注督促我寫出更好的文章——萬一哪天我進步了呢?