Slide 1

Slide 1 text

Networking with RxJava + Retrofit Beyond the canonical REST example Stephen D’Amico - @sddamico - SeatGeek - May 2017

Slide 2

Slide 2 text

The Basics I bet at some point in your journey of learning networking with RxJava + Retrofit, you’ve seen a code example that looks something like this... interface SeatGeekApi { @GET("events") Observable getUpcomingEvents(); } Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.seatgeek.com/") .build(); SeatGeekApi api = retrofit.create(SeatGeekApi.class); api.getUpcomingEvents() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(events -> { // do something with events }, error -> { // handle errors });

Slide 3

Slide 3 text

But, what doesn't this handle? ● Retrying requests when they fail ● Dealing with specific response codes ● Showing loading indicators in your UI ● Configuration changes/subscription management ● Probably other cases you have

Slide 4

Slide 4 text

Let's see how we can improve…

Slide 5

Slide 5 text

What if my request fails?

Slide 6

Slide 6 text

Retrying requests Network requests on mobile can fail due to a multitude of reasons. Often, you'll want some sort of mechanism for retrying a request. getRequestObservable() .subscribe(response -> { // handle response });

Slide 7

Slide 7 text

Retrying requests - Naive approach The most simple way is to recreate your source Observable every time that you want to make a request. private void makeRequest() { getRequestObservable() .subscribe(getObserver()); } private Observer getObserver() { return new Observer<>() { … @Override public void onError(Throwable t) { if (someCondition) { makeRequest(); } } } }

Slide 8

Slide 8 text

Retrying requests - retryWhen() RxJava has retryWhen()! retryWhen() can be a little tricky to grok. Let's walk through it: ● retryWhen() is called once per subscription ● It's given an Observable of the errors by the parent stream (Observable) ● As long as the Observable returned from retryWhen() does not complete or error, the parent will be resubscribed // this code retries the request three times, with a 5s delay, // then 10s, then 15s getRequestObservable() .retryWhen(attempt -> { return attempt .zipWith(Observable.range(1, 3), (n, i) -> i) .flatMap(i -> return Observable.timer(5 * i, TimeUnit.SECONDS); }) }) .subscribe(viewModel -> { // handle updated request state });

Slide 9

Slide 9 text

Retrying requests - retryWhen() Some of you might be very concerned though, automatic retries are dangerous business! If you retry too often you can easily have your apps DDOS'ing your servers. So, let's only retry when the user wants to retry… Fortunately this is easy to do with Relays/Subjects PublishRelay 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); }

Slide 10

Slide 10 text

My API has very specific response codes…

Slide 11

Slide 11 text

Using Response In our canonical example, we're using an Observable representing the typed body of the response document. This is not a requirement! If you need access to metadata about the response, you can use Observable> One nicety of this approach is that server responses in the 400-500 level will not cause an exception in your stream. interface SeatGeekApi { @GET("events") Observable> getUpcomingEvents(); } Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.seatgeek.com/") .build(); SeatGeekApi api = retrofit.create(SeatGeekApi.class); api.getUpcomingEvents() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(eventsResponse -> { int responseCode = eventsResponse.code(); switch (responseCode) { HTTP_301: … HTTP_403: … } }, error -> { // note: only handle i/o errors! });

Slide 12

Slide 12 text

Using Response - Just errors Always using Response can be cumbersome though if you only want to check error codes. Fortunately, Retrofit has you covered with HttpException. You can check your error in onError to see if you have an HttpException to work with. interface SeatGeekApi { @GET("events") Observable getUpcomingEvents(); } Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.seatgeek.com/") .build(); SeatGeekApi api = retrofit.create(SeatGeekApi.class); api.getUpcomingEvents() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(eventsResponse -> { // do something with events }, error -> { if (error instanceof HttpException) { Response response = ((HttpException)error).response(); switch (response.code()) { … } } else { // handle other errors } });

Slide 13

Slide 13 text

Using Response - With share() Sometimes to help with readability or because you have specific branching conditions downstream, it can be useful to multicast your request with share(). share() lets multiple subscribers handle the data provided from upstream, in our case this is the network response. In this example, we're able to set up dedicated subscribers for specific response codes. interface SeatGeekApi { @GET("events") Observable> getUpcomingEvents(); } Observable> eventsResponse = api.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);

Slide 14

Slide 14 text

My designer wants a loading indicator…

Slide 15

Slide 15 text

Showing loading The goal with showing a loading state is that you'll want a simple way to show loading without tightly coupling your UI to the request stream First, we'll look at the naive approach... api.getUpcomingEvents() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(events -> { // do something with events }, error -> { // handle errors });

Slide 16

Slide 16 text

Showing loading - Naive approach What are the problems here? ● We've tightly coupled our UI to the stream ● These lambdas leak references to our Views (more on subscription management in a bit) ● Can't handle "streams", only works as a single request api.getUpcomingEvents() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnSubscribe(() -> loadingIndicator.show()) .doOnUnsubscribe(() -> loadingIndicator.hide()) .subscribe(events -> { // do something with events }, error -> { // handle errors });

Slide 17

Slide 17 text

Showing loading - Splitting streams So, let's try something new. What if our loading state was actually a separate stream? This gives us flexibility to have our UI subscribe to the loading state separately enum RequestState { IDLE, LOADING, COMPLETE, ERROR } BehaviorRelay state = BehaviorRelay.create(RequestState.IDLE); void publishRequestState(RequestState requestState) { Observable.just(requestState) .observeOn(AndroidSchedulers.mainThread()) // ensure state updates // are on proper thread .subscribe(state); } api.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 });

Slide 18

Slide 18 text

Showing loading - Splitting streams Let's look at how the UI then subscribes to our request state then… We can now manage our view updates separately from the request stream! enum RequestState { IDLE, LOADING, COMPLETE, ERROR } BehaviorRelay state = BehaviorRelay.create(RequestState.IDLE); state.subscribe(requestState -> { switch(requestState) { IDLE: break; LOADING: loadingIndicator.show(); errorView.hide(); break; COMPLETE: loadingIndicator.hide(); break; ERROR: loadingIndicator.hide(); errorView.show(); break; } );

Slide 19

Slide 19 text

Showing loading - Splitting streams We can extend this model then to other aspects of the request! Now we've decoupled the UI from the request stream completely, success! BehaviorRelay state = BehaviorRelay.create(RequestState.IDLE); BehaviorRelay response = BehaviorRelay.create(); BehaviorRelay errors = BehaviorRelay.create(); … void executeRequest() { api.getUpcomingEvents() .doOnSubscribe(() -> publishRequestState(RequestState.LOADING)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnError(t -> publishRequestState(RequestState.ERROR)) .doOnComplete(() -> publishRequestState(RequestState.COMPLETE)) .subscribe(response, errors); } …

Slide 20

Slide 20 text

Showing loading - Splitting streams Now our UI can subscribe independently to the data and errors and handle them separately as desired. BehaviorRelay state = BehaviorRelay.create(RequestState.IDLE); BehaviorRelay response = BehaviorRelay.create(); BehaviorRelay errors = BehaviorRelay.create(); state.subscribe(requestState -> updateLoadingView(requestState)); response.subscribe(eventsResponse -> showEventsResponse(eventsResponse)); errors.subscribe(throwable -> extractAndShowError(throwable));

Slide 21

Slide 21 text

Showing loading - Splitting streams You can always combine these streams together in the UI if you don't like handling to be spread out… BehaviorRelay state = BehaviorRelay.create(RequestState.IDLE); BehaviorRelay> response = BehaviorRelay.create(Optional.absent()); BehaviorRelay> errors = BehaviorRelay.create(Optional.absent()); class RequestViewModel { public final RequestState state; public final Optional response; public final Optional errors; RequestViewModel(...) { … } } Observable.combineLatest(state, response, errors, RequestViewModel::new) .subscribe(viewModel -> { // handle updated request state });

Slide 22

Slide 22 text

How about configuration changes?

Slide 23

Slide 23 text

Subscription management RxJava, like AsyncTask and other asynchronous patterns do not work with the Android lifecycles by default… RxJava provides a method to unsubscribe from streams via Disposable But the key question is how to use it properly? Disposable disposable = Observable .combineLatest(state, response, errors, RequestViewModel::new) .subscribe(viewModel -> { // handle updated request state }); ... disposable.dispose(); // when?

Slide 24

Slide 24 text

Subscription management - Basic Disposable Basic Disposable usage is hopefully straightforward. Keep a reference to the returned object from your subscription and then dispose() of it in an appropriate lifecycle method. class MyActivity extends Activity { Disposable disposable; … disposable = Observable .combineLatest(state, response, errors, ViewModel::new) .subscribe(getSubscriber()); … public void onStop() { disposable.dispose(); } }

Slide 25

Slide 25 text

Subscription management - CompositeDisposable CompositeDisposable is a very useful tool if you have your UI subscribed to multiple streams. You can add() Disposables to it so that you can have them all be disposed at the same time! Note: This can be very convenient in a MVP-like system where you want all your subscriptions to end in an unbind() method. class MyActivity extends Activity { CompositeDisposable disposables = new CompositeDisposable(); … disposables.add(state.subscribe(getStateSubscriber())); disposables.add(result.subscribe(getResultSubscriber())); disposables.add(error.subscribe(getErrorSubscriber())); … public void onStop() { disposable.clear(); } }

Slide 26

Slide 26 text

Subscription management - nonConfigurationInstance Out of the box, there's not an easy way to continue an Observable stream across configuration changes. However, we can coopt the Android Activity's onRetainNonConfigurationInstance() so that our stream is accessible again after a configuration change… Note: this is also a handy strategy for keeping your Presenter/Dagger graph around for your next Activity class MyActivity extends Activity { Observable request; Disposable disposable; public void onCreate(Bundle instanceState) { request = getLastNonConfigurationInstance(); } … request = Observable .combineLatest(state, response, errors, ViewModel::new) .cache(); disposable = request .subscribe(getSubscriber()); … public void onStart() { if (request != null && disposable == null) { disposable = request .subscribe(getSubscriber()); // can null request field // in subscriber } } Object onRetainNonConfigurationInstance() { return request; } public void onStop() { disposable.dispose(); } }

Slide 27

Slide 27 text

Subscription management - RxLifecycle https://github.com/trello/RxLifecycle RxLifecycle gives a simple way to tie subscriptions to your Activity/Fragment lifecycle. When a corresponding lifecycle event occurs, the Disposable will be dispose() 'd class MyActivity extends RxAppCompatActivity { ... Observable .combineLatest(state, response, errors, ViewModel::new) .compose(bindToLifecycle()) .subscribe(viewModel -> { // handle updated request state }); … }

Slide 28

Slide 28 text

Subscription management - RxLifecycle + retain If we want to retain our Observable, that works with RxLifecycle too. class MyActivity extends Activity { Observable request; public void onCreate(Bundle instanceState) { request = getLastNonConfigurationInstance(); } … request = Observable .combineLatest(state, response, errors, ViewModel::new) .cache(); request .compose(bindToLifecycle()) .subscribe(getSubscriber()); … public void onStart() { if (request != null) { request .compose(bindToLifecycle()) .subscribe(getSubscriber()); } } Object onRetainNonConfigurationInstance() { return request; } public void onStop() { disposable.dispose(); } }

Slide 29

Slide 29 text

Subscription management - RxLifecycle + MVP You can customize your MVP Presenter as well to take advantage of similar functionality! Now, you can bind your observation of the request streams and never worry about your view being detached. enum PresenterLifecycle { STOPPED, STARTED } class RxPresenter extends Presenter { BehaviorRelay presenterLifecycle = BehaviorRelay.create(PresenterLifecycle.STOPPED); void bind(V view) { presenterLifecycle.call(PresenterLifecycle.STARTED); } void unbind() { presenterLifecycle.call(PresenterLifecycle.STOPPED); } protected LifecycleTransformer bindToLifecycle() { return RxLifecycle.bind(presenterLifecycleObservable, lifecycle -> { switch (lifecycle) { case STARTED: return PresenterLifecycle.STOPPED; case STOPPED: default: return PresenterLifecycle.STARTED; } }); } }

Slide 30

Slide 30 text

Questions?

Slide 31

Slide 31 text

Thanks! About me… name: Stephen D’Amico twitter: @sddamico github: sddamico Shameless plug… seatgeek.com/jobs