RxJava 謹慎串聯Observable
問題
RxJava提供了flatMap和switchMap兩個操作符用于讓我們進行Observable的串聯,比如我們可以使用RxView.clicks()創建一個會發送點擊事件的Observable,同時我們還有一個用于請求網絡數據的Observable:
Observable<Void> loginPress(){
return RxView.clicks(findViewById(R.id.login));
}
Observable<LoginInfo> login() {
return httpApi.login();
}</code></pre>
需求希望在login按鈕點下之后,調用login()方法進行登陸。此時有兩種寫法:
- 直接串聯:
loginPress().flatMap(aVoid -> login()).subscribe(loginInfo -> {
// 處理登錄邏輯
}, throwable -> {
// 處理錯誤情況
});
- 分別調用:
loginPress().subscribe(aVoid -> {
login().subscribe(loginInfo -> {
// 處理登錄邏輯
}, throwable -> {
// 處理錯誤情況
});
});
從代碼上看,第一種方式顯然是Rx更為推薦的——不打破鏈式調用 的方式。但在有些時候,這種方法會出現比較嚴重的問題:原因是,subscriber在接受到錯誤以后,就無法接受到之后的事件了。
舉上面的第一個使用例子來說有兩個問題:
-
如果處理登錄邏輯里發生了一些意料不到的錯誤(比如服務器有時候成功返回了數據,但有些數據為空導致了處理邏輯出現空指針),發生錯誤時,錯誤會回調到 throwable->{} 中。之后再進行按鈕點擊,數據返回subscriber都接收不到了。
-
如果login()方法里有錯誤,比如網絡訪問異常。那么當第一次點擊按鈕時,subscriber會收到網絡異常的錯誤。但如果用戶再點擊登錄按鈕,無論是否成功,我們都沒有辦法再次接受到登錄信息,頁面也無法發生跳轉。
前者在使用 flatMap 或者 switchMap 會發生,而后者在任何情況下都有可能出現。
解決方案
使用方案2,分別調用不會產生相應的問題。但打破了RxJava的鏈式調用。
對于使用方案1,最簡單的解決方案是:在遇到錯誤重新綁定。但這種方式的成本比較高。每個處理訂閱的地方都需要進行特殊處理。
首先是第一個問題:
- 如果處理登錄邏輯里發生了一些意料不到的錯誤(比如服務器有時候成功返回了數據,但有些數據為空導致了處理邏輯出現空指針),發生錯誤時,錯誤會回調到 throwable->{} 中,但之后的任何數據返回subscriber都接不到了。
這種情況出現的其實比較少。對于這種不可意料的錯誤,我們可以使用一個大大的try-catch把subscriber包起來,比如實現一個類似這樣的類:
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
}
}}
}</code></pre>
在使用時:
login().subscribe(new ErrorHandlerSubscriber(loginInfo -> {
// 處理登錄邏輯
} , throwable -> {
// 處理失敗
}));
這樣一來,錯誤實際上不會被轉發到Subscriber內,而只是會傳到我們自定義的 throwable -> {} 里。也就不會影響實際Subscriber后續事件的接受。
同時,建議在 // log it 的地方將錯誤日志打出來,方便調試。
對于第二個問題:
2.如果login()方法里有錯誤,比如網絡訪問異常。那么當第一次點擊按鈕時,subscriber會收到網絡異常的錯誤。但如果用戶再點擊登錄按鈕,無論是否成功,我們都沒有辦法再次接受到登錄信息,頁面也無法發生跳轉。
這種情況出現出現會十分頻繁,尤其在進行網絡請求時。解決方案有N種
1.如果你不關心錯誤,可以使用 switchMapDelayError
這個關鍵字可以起到忽略錯誤的作用,但大部分情況下,我們希望在遇到錯誤對用戶進行提示。所以如果你不關心錯誤是否發生的情況下,使用這個關鍵字進行串聯是最簡單的。
2.使用 materialize() 將next和error都包裝到notification中:
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實際上就不會發生錯誤,也就不會造成相應的問題了。
來自:http://www.jianshu.com/p/647fb66d218f