問題
RxJava提供了flatMap和switchMap兩個操作符用于讓我們進行Observable的串聯(lián)脐雪,比如我們可以使用RxView.clicks()創(chuàng)建一個會發(fā)送點擊事件的Observable,同時我們還有一個用于請求網(wǎng)絡數(shù)據(jù)的Observable:
Observable<Void> loginPress(){
return RxView.clicks(findViewById(R.id.login));
}
Observable<LoginInfo> login() {
return httpApi.login();
}
需求希望在login按鈕點下之后饮潦,調(diào)用login()方法進行登陸。此時有兩種寫法:
- 直接串聯(lián):
loginPress().flatMap(aVoid -> login()).subscribe(loginInfo -> {
// 處理登錄邏輯
}, throwable -> {
// 處理錯誤情況
});
- 分別調(diào)用:
loginPress().subscribe(aVoid -> {
login().subscribe(loginInfo -> {
// 處理登錄邏輯
}, throwable -> {
// 處理錯誤情況
});
});
從代碼上看吕嘀,第一種方式顯然是Rx更為推薦的——不打破鏈式調(diào)用 的方式陡舅。但在有些時候酌住,這種方法會出現(xiàn)比較嚴重的問題:原因是,subscriber在接受到錯誤以后刃麸,就無法接受到之后的事件了醒叁。
舉上面的第一個使用例子來說有兩個問題:
如果處理登錄邏輯里發(fā)生了一些意料不到的錯誤(比如服務器有時候成功返回了數(shù)據(jù),但有些數(shù)據(jù)為空導致了處理邏輯出現(xiàn)空指針)泊业,發(fā)生錯誤時把沼,錯誤會回調(diào)到
throwable->{}
中。之后再進行按鈕點擊脱吱,數(shù)據(jù)返回subscriber都接收不到了智政。如果login()方法里有錯誤,比如網(wǎng)絡訪問異常箱蝠。那么當?shù)谝淮吸c擊按鈕時续捂,subscriber會收到網(wǎng)絡異常的錯誤。但如果用戶再點擊登錄按鈕宦搬,無論是否成功牙瓢,我們都沒有辦法再次接受到登錄信息,頁面也無法發(fā)生跳轉间校。
前者在使用flatMap
或者switchMap
會發(fā)生矾克,而后者在任何情況下都有可能出現(xiàn)。
解決方案
使用方案2憔足,分別調(diào)用不會產(chǎn)生相應的問題胁附。但打破了RxJava的鏈式調(diào)用。
對于使用方案1滓彰,最簡單的解決方案是:在遇到錯誤重新綁定控妻。但這種方式的成本比較高。每個處理訂閱的地方都需要進行特殊處理揭绑。
首先是第一個問題:
- 如果處理登錄邏輯里發(fā)生了一些意料不到的錯誤(比如服務器有時候成功返回了數(shù)據(jù)弓候,但有些數(shù)據(jù)為空導致了處理邏輯出現(xiàn)空指針)郎哭,發(fā)生錯誤時,錯誤會回調(diào)到
throwable->{}
中菇存,但之后的任何數(shù)據(jù)返回subscriber都接不到了夸研。
這種情況出現(xiàn)的其實比較少。對于這種不可意料的錯誤依鸥,我們可以使用一個大大的try-catch把subscriber包起來亥至,比如實現(xiàn)一個類似這樣的類:
public class ErrorHandlerSubscriber<T> extends Subscriber<T> {
private Action1<T> onNext;
private Action1<Throwable> onError;
public ErrorHandlerSubscriber(Action1<T> onNext, Action1<Throwable> onError) {
this.onNext = onNext;
this.onError = onError;
}
@Override
public void onCompleted() {}
@Override
public void onError(Throwable e) {
if (onError != null) {
onError.call(e);
}
}
@Override
public void onNext(T t) {
try {
if (onNext != null) {
onNext.call(t);
}
} catch (Exception e) {
if (onError != null) {
onError.call(e);
} else {
// log it
}
}}
}
在使用時:
login().subscribe(new ErrorHandlerSubscriber(loginInfo -> {
// 處理登錄邏輯
} , throwable -> {
// 處理失敗
}));
這樣一來,錯誤實際上不會被轉發(fā)到Subscriber內(nèi)毕籽,而只是會傳到我們自定義的throwable -> {}
里抬闯。也就不會影響實際Subscriber后續(xù)事件的接受井辆。
同時关筒,建議在// log it
的地方將錯誤日志打出來,方便調(diào)試杯缺。
對于第二個問題:
2.如果login()方法里有錯誤蒸播,比如網(wǎng)絡訪問異常祠够。那么當?shù)谝淮吸c擊按鈕時圈浇,subscriber會收到網(wǎng)絡異常的錯誤。但如果用戶再點擊登錄按鈕溉痢,無論是否成功塘揣,我們都沒有辦法再次接受到登錄信息包雀,頁面也無法發(fā)生跳轉。
這種情況出現(xiàn)出現(xiàn)會十分頻繁亲铡,尤其在進行網(wǎng)絡請求時才写。解決方案有N種
1.如果你不關心錯誤,可以使用switchMapDelayError
這個關鍵字可以起到忽略錯誤的作用奖蔓,但大部分情況下赞草,我們希望在遇到錯誤對用戶進行提示。所以如果你不關心錯誤是否發(fā)生的情況下吆鹤,使用這個關鍵字進行串聯(lián)是最簡單的厨疙。
2.使用materialize()
將next和error都包裝到notification中:
http://stackoverflow.com/questions/32084824/rxjava-rxbinding-how-to-handle-errors-on-rxview
loginPress().flatMap(aVoid -> login().materialize()).subscrber(notification -> {
if(notification.hasValue()){
// 處理登錄邏輯
} else if(notification.isOnError()) {
// 處理失敗邏輯
}
});
3.使用doOnError處理錯誤,同時使用onErrorResumeNext忽略錯誤:
loginPress().flatMap(aVoid -> login().doOnError(throwable -> {
// 處理失敗邏輯
}).onErrorResumeNext(throwable -> Observable.empty())
).subscriber(loginInfo -> {
// 處理登錄邏輯
} , throwable -> {
// 處理失敗
});
這樣一來疑务,flatMap里的Observable實際上就不會發(fā)生錯誤沾凄,也就不會造成相應的問題了。