譯自: May 2017 Meetup: Advanced RxJava and Conductor
Slides
前言
我猜在你學習 RxJava + Retrofit 的歷程中,肯定見過這種代碼:
interface SeatGeekApi {
@GET("events") Observable<EventResponse> getUpcomingEvents();
}
Retrofit mRetrofit = new Retrofit.Builder()
.baseUrl("https://api.seatgeek.com/")
.build();
SeatGeekApi mApi = mRetrofit.create(SeatGeekApi.class);
mApi.getUpComingEvents()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(events -> {
// handle events
}, error -> {
// handle errors
});
上面的代碼缺少了哪些東西?
- 請求失敗時需要 retry
- 處理特定的 響應碼
- UI 中顯示 loading indicators
- 對系統 configuration changes 的響應
- 其他特殊事件
I. 重試請求 Retrying Request
網絡請求在移動設備上會因各種原因發生錯誤,通常我們需要添加一些重試請求的機制。
一種 naive 的實現:
private void makeRequest() {
getRequestObservable()
.subscribe(getObserver());
}
private Observer<Response> getObserver() {
return new Observer<Response>() {
...
@Override public void onError(Throwable e) {
// 發生錯誤時發送新的請求
if (someCondition) {
makeRequest();
}
}
};
}
使用 retryWhen() 實現:
RxJava 的 retryWhen() 方法可以便捷地實現這個需求:
- retryWhen() 只會在每個 subscription 中調用一次
- retryWhen() 返回的是 parent 數據流的 errors
- 只要從 retryWhen() 返回的 Observable 對象不是 complete 或者 error 的,parent Observable 將會被重新訂閱。
// 簡單的演示重試 3 次請求,分別延遲 5s,10s,15s.
getRequestObservable()
.retryWhen(attempt -> {
attempt
.zipWith(Observable.range(1, 3), (n, i) -> i)
.flatMap(i -> {
return Observable.timer(5 * i, TimeUnit.SECONDS);
})
})
.subscribe(viewModel -> {
// handle updated request state
});
一些優化:
- 頻繁的自動 retry 可能會導致 app DDoS 服務器。
- 比較好的處理方式是讓用戶選擇 retry,我們可以使用 RxRelay 中的Relay/Subject。
PublishRelay<Long> retryRequest = PublishRelay.create();
getRequestObservable()
.retryWhen(attempt ->retryRequest)
.subscribe(viewModel -> {
// handle updated request state
});
@OnClick(R.id.retry_view)
public void onRetryClicked() {
retryRequest.call(System.currentTimeMillis);
}
II. 處理特殊響應碼 Response Codes
使用 Response<T>
- 利用 Observable<Response<T>> 可以使用響應的元數據 (metadata)。
- 這種方法的優點是服務器響應碼在 400-500 之間不會產生異常。
interface SeatGeekApi {
@GET("events") Observable<Response<EventResponse>> getUpComingEvents();
}
Retrofit mRetrofit = new Retrofit.Builder()
.baseUrl("https://api.seatgeek.com/")
.build();
mApi.getUpComingEvents()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(eventsResponse -> {
int responseCode = eventsResponse.code();
switch (responseCode) {
HTTP_301:
...
HTTP_403:
...
}
}, error -> {
// ONLY handle i/o errors
});
一些優化
- 僅僅使用 Response<T> 處理所有的 response code
可能會顯得比較累贅,幸運的是 Retrofit 提供了 HttpException 處理 errors - 可以在 onError 回調里檢查 exception 是否為 HttpException
mApi.getUpComingEvents()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(eventsResponse -> {
// handle something with events
}, error -> {
if (error instanceof HttpException) {
Response response = ((HttpException)error).response();
switch (response.code()) {...}
} else {
// handle other errors
}
});
- 為了增強代碼可讀性,我們還可以使用 share().
interface SeatGeekApi {
@GET("events") Observable<Response<EventResponse>> getUpComingEvents();
}
Observable<Response<EventResponse>> eventsResponse = mApi.getUpcomingEvents()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.share();
eventsResponse
.filter(Response::isSuccessful)
.subscribe(this::handleSuccessfulResponse);
eventsResponse
.filter(response -> response.code() == HTTP_403)
.subscribe(this::handle403Response);
eventsResponse
.filter(response -> response.code() == HTTP_304)
.subscribe(this::handle304Response);
III. 顯示進度條 Loading Indicator
一種 naive 的實現
mApi.getUpcomingEvents()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe(() -> loadingIndicator.show())
.doOnUnsubscribe(() -> loadingIndicator.hide())
.subscribe(events -> {
// do something with events
}, error -> {
// handle errors
});
上例中存在的問題:
- UI 跟數據流深度耦合
- 無法響應數據流的變化,僅在簡單的請求時有效
使用分割數據流 Splitting Stream
- 嘗試新的方法,UI 更新位于另一條 stream
- 我們可以靈活的根據 loading 狀態更新 UI.
enum RequestState {
IDLE, LOADING, COMPLETE, ERROR
}
BehaviorRelay<RequestState> state = BehaviorRelay.create(RequestState.IDLE);
void publishRequestState(RequestState requestState) {
Observable.just(requestState)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(state);
}
mApi.getUpcomingEvents()
.doOnSubscribe(() -> publishRequestState(RequestState.LOADING))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(t -> publishRequestState(RequestState.ERROR))
.doOnComplete(() -> publishRequestState(RequestState.COMPLETE))
.subscribe(events -> {
// do something with events
}, error -> {
// handle errors
});
- 請求狀態發生變化時,在另一條 stream 修改 UI 的變化:
state.subscribe(requestState -> {
switch (requestState) {
IDLE:
break;
LOADING:
loadingIndicator.show();
errorView.hide();
break;
COMPLETE:
loadingIndicator.hide();
break;
ERROR:
loadingIndicator.hide();
errorView.show();
break;
}
});
一些優化
- 我們可以將這種模式擴展到各種 request result 上
BehaviorRelay<RequestState> state = BehaviorRelay.create(RequestState.IDLE);
BehaviorRelay<EventsResponse> response = BehaviorRelay.create();
BehaviorRelay<Throwable> errors = BehaviorRelay.create();
...
void executeRequest() {
mApi.getUpcomingEvents()
.doOnSubscribe(() -> publishRequestState(RequestState.LOADING))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(t -> publishRequestState(RequestState.ERROR))
.doOnComplete(() -> publishRequestState(RequestState.COMPLETE))
.subscribe(response, errors);
}
- 我們同樣可以將所有分割的 streams 合并成一條:
BehaviorRelay<RequestState> state = BehaviorRelay.create(RequestState.IDLE);
BehaviorRelay<Optional<EventsResponse>> response = BehaviorRelay.create(Optional.empty());
BehaviorRelay<Optional<Throwable>> errors = BehaviorRelay.create(Optional.empty());
class RequestViewModel {
public final RequestState mState;
public final Optional<EventsResponse> mResponse;
public final Optional<Throwable> mErrors;
RequestViewModel(...) {...}
}
Observable.combineLatest(state, response, errors, RequestViewModel::new)
.subscribe(viewModel -> {
// handle updated request state
});
IV. 管理 Configuration Changes
RxJava,以及像 AsyncTask 等其他異步模型默認情況下不會響應 Android 的生命周期。RxJava 提供了數據流 unsubscribe 的方法,但是問題是該如何正確的使用它。
Disposable mDisposable = Observable.combineLatest(state, response, errors, RequestViewModel::new)
.subscribe(viewModel -> {
// handle updated request state
});
...
mDisposable.dispose(); // when?
一種基本的實現
創建 Disposable 時保存 Disposable 的引用,在生命周期發生回調時調用 dispose()
方法。
class MyActivity extends Activity {
Disposable mDisposable;
@Override protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
mDisposable = Observable
.combineLatest(state, response, errors, ViewModel::new)
.subscribe(getSubscriber());
}
@Override protected void onStop() {
super.onStop();
mDisposable.dispose();
}
}
CompositeDisposable
- CompositeDisposable 非常適合 UI 訂閱了多條 stream 的場景。
- 使用
add()
方法添加每條 stream 的 Disposable 對象。 - CompositeDisposable 的清理方法非常適合在 MVP 框架下的
unbind()
方法使用。
class MyActivity extends Activity {
CompositeDisposable mDisposables = new CompositeDisposable();
@Override protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
mDisposables.add(state.subscribe(getStateSubscriber()));
mDisposables.add(state.subscribe(getStateSubscriber()));
mDisposables.add(state.subscribe(getStateSubscriber()));
}
@Override protected void onStop() {
super.onStop();
mDisposables.clear();
}
}
推薦的方式—— RxLifecycle
- RxLifecycle 提供了便捷的方式將我們的 Disposable 與 Activity/Fragment 的生命周期綁定。
- 當相應的生命周期回調發生時,Disposable 將會調用
dispose()
方法。
class MyActivity extends RxAppCompatActivity {
@Override protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
Observable.combineLatest(state, response, errors, ViewModel::new)
.compose(bindToLifecycle())
.subscribe(viewModel -> {
// handle updated request state
});
RxLifecycle + MVP
public class RxPresenter<T extends MvpView> extends Presenter<T> {
BehaviorRelay<PresenterLifecycle> mPresenterLifecycle =
BehaviorRelay.create(STOPPED);
@Override public void attach(T mvpView) {
mPresenterLifecycle.call(STARTED);
}
@Override public void detach() {
mPresenterLifecycle.call(STOPPED);
}
protected <T>LifecycleTransformer<T> bindToLifecycle() {
return RxLifecycle.bind(presenterLifecycleObservable, lifecycle -> {
switch (lifecycle) {
case STARTED:
return STOPPED;
case STOPPED:
default:
return STARTED;
}
});
}
}