原文: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拄氯。
遇到過這樣的情況嘛渡蜻?你得到了一個崩潰的報告满俗,但是你無法復(fù)現(xiàn)這個BUG
菩鲜。聽起來似曾相識腻菇?我也是胳螟!在花了很多時間查看堆棧跟蹤和項目的源碼后昔馋,最終我選擇了放棄——關(guān)閉了這個issue
,并提交了一個類似 無法復(fù)現(xiàn) 或者 某個Android生產(chǎn)商的某種特定的機型導(dǎo)致的特殊錯誤 的備注糖耸。
以我們的購物App
舉例來說秘遏,在Home
界面,用戶以某種方式進行下拉刷新嘉竟,但不知道為什么邦危,崩潰報告告訴我,當(dāng)用戶執(zhí)行下拉刷新獲取最新數(shù)據(jù)的操作時舍扰,應(yīng)用拋出了一個NullPointerException
倦蚪。
因此,作為開發(fā)人員边苹,您啟動App
并嘗試在Home
界面進行下拉刷新陵且,但App
并沒有崩潰, 它按照預(yù)期正常地運行。然后您開始仔細(xì)檢查自己的代碼个束,但是就是找不到哪里會導(dǎo)致NullPointerException
的發(fā)生滩报。你打開了debug
模式,一行一行逐步執(zhí)行該界面相關(guān)的代碼播急,但App
仍然正常的運行—— 到底怎么樣才能讓它在下拉刷新時崩潰脓钾?
問題的根本在于你不能在App
崩潰發(fā)生之前復(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
和每個intent
的result
——也就是即將渲染在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ù)(分頁),這也導(dǎo)致了loadingNextPage
狀態(tài)的改變独郎,它的值變成了true
踩麦。3.一旦分頁數(shù)據(jù)被加載成功,
loadingNextPage
狀態(tài)改變成了false
,用戶再次重復(fù)操作達到了列表的底部氓癌,并又一次出發(fā)了觸發(fā)了加載下一頁數(shù)據(jù)的intent
谓谦。4.接下來用戶開始嘗試下拉刷新的
intent
,這導(dǎo)致loadingPullToRefresh
狀態(tài)變更為了true
,然后贪婉,App
突然發(fā)生了崩潰—— 這之后就沒有更多日志了反粥。
這些信息如何幫助我們解決這個bug呢?顯然疲迂,我們知道用戶觸發(fā)了哪些操作才顿,因此我們完全可以手動復(fù)現(xiàn)這個崩潰。此外尤蒿,因為我們將App
的狀態(tài)用json
進行表現(xiàn)郑气,因此我們可以簡單地使用最后一個狀態(tài),反序列化json并將此狀態(tài)作為我們的初始狀態(tài)來修復(fù)該錯誤:
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ù)的下拉刷新操作導(dǎo)致了崩潰。
結(jié)語
一個應(yīng)用狀態(tài)隨時隨地 可快照 的App
可以使我們開發(fā)人員的生活更加輕松奏属。我們不僅能夠輕松的 復(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
).這增加了一些額外的計算資源的負(fù)荷愧驱,平均來算的話慰技,狀態(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ù)宰睡,但這可能導(dǎo)致連接到崩潰報告的狀態(tài)不完整(因此這些報告可能幾乎無用),另外一個方案則是將敏感數(shù)據(jù)進行加密——但這可能需要一些額外的CPU占用溯乒。
總結(jié)一下:我個人認(rèn)為這樣 可快照 的App
有很多優(yōu)點夹厌,但是,你可能需要做出一些權(quán)衡裆悄。也許您開始為內(nèi)部版本或beta版本啟用App
快照矛纹,以衡量它其產(chǎn)生的作用。
系列目錄
《使用MVI打造響應(yīng)式APP》原文
《使用MVI打造響應(yīng)式APP》譯文
- [譯]使用MVI打造響應(yīng)式APP(一):Model到底是什么
- [譯]使用MVI打造響應(yīng)式APP[二]:View層和Intent層
- [譯]使用MVI打造響應(yīng)式APP[三]:狀態(tài)折疊器
- [譯]使用MVI打造響應(yīng)式APP[四]:獨立性UI組件
- [譯]使用MVI打造響應(yīng)式APP[五]:輕而易舉地Debug
- [譯]使用MVI打造響應(yīng)式APP[六]:恢復(fù)狀態(tài)
- [譯]使用MVI打造響應(yīng)式APP[七]:掌握時機(SingleLiveEvent問題)
- [譯]使用MVI打造響應(yīng)式APP[八]:導(dǎo)航
《使用MVI打造響應(yīng)式APP》實戰(zhàn)
關(guān)于我
Hello光稼,我是卻把清梅嗅或南,如果您覺得文章對您有價值,歡迎 ??艾君,也歡迎關(guān)注我的博客或者Github采够。
如果您覺得文章還差了那么點東西,也請通過關(guān)注督促我寫出更好的文章——萬一哪天我進步了呢冰垄?