Adopting RxJava on Airbnb Android

Adopting RxJava on Airbnb Android

Presentation at Droidcon DE on June 17th, 2016

7fd4ba468da56bb5330a6352c1b54f52?s=128

felipecsl

June 17, 2016
Tweet

Transcript

  1. Adopting RxJava on Airbnb Android FELIPE LIMA / JUNE 17,

    2016 / DROIDCON DE
  2. Why RxJava?

  3. We make shitty software

  4. Mobile Engineering is hard

  5. None
  6. ReactiveX AN API FOR ASYNCHRONOUS PROGRAMMING WITH OBSERVABLE STREAMS

  7. Streams

  8. Reactive programming

  9. None
  10. Programming Paradigms 101

  11. Object Oriented programming

  12. • Class based • Objects as first class citizens •

    State management • Side causes: Hidden inputs • Side effects: Hidden outputs • Hard to unit test: Mocking libraries Object Oriented Programming The status quo
  13. Side-Effects are the Complexity Iceberg.

  14. Functional programming

  15. • Higher order functions • Pure functions • Referential transparency

    • Easy to reason about • Easy to unit test Functional Programming http://blog.jenkster.com/2015/12/what-is-functional-programming.html
  16. Now back to reactive…

  17. • Observable & Observer Concepts

  18. • Observable & Observer • Subscriber Concepts

  19. • Observable & Observer • Subscriber • Subscription Concepts

  20. • Observable & Observer • Subscriber • Subscription • Producer

    Concepts
  21. • Observable & Observer • Subscriber • Subscription • Producer

    • Hot & Cold Observables Concepts
  22. • Observable & Observer • Subscriber • Subscription • Producer

    • Hot & Cold Observables • Backpressure Concepts
  23. • Observable & Observer • Subscriber • Subscription • Producer

    • Hot & Cold Observables • Backpressure • Scheduler Concepts
  24. • Observable & Observer • Subscriber • Subscription • Producer

    • Hot & Cold Observables • Backpressure • Scheduler • Subject Concepts
  25. • Observable & Observer • Subscriber • Subscription • Producer

    • Hot & Cold Observables • Backpressure • Scheduler • Subject • And more! Concepts
  26. /**
 * Returns a new Observable by applying a function

    that you supply to each item emitted * by the source Observable that returns an Observable, and then emitting the items * emitted by the most recently emitted of these Observables.
 *
 * The resulting Observable completes if both the upstream Observable and the last * inner Observable, if any, complete.
 * If the upstream Observable signals an onError, the inner Observable is unsubscribed * and the error delivered in-sequence.
 * 
 * @param fund
 * a function that, when applied to an item emitted by the source Observable, * returns an Observable
 * @return an Observable that emits the items emitted by the Observable returned from * applying {@code func} to the most recently emitted item emitted by the source * Observable
 */ public final <R> Observable<R> switchMap( Func1<? super T, ? extends Observable<? extends R>> func) {
 return switchOnNext(map(func));
 }
  27. WAT?!

  28. Adoption challenges

  29. • Team Size: Getting everyone onboard and up to speed

    • Learning Curve: Steep, time consuming • Terminology: Lots of new concepts • Debugging: Unreadable stack traces Adoption Challenges
  30. java.lang.IllegalStateException: Fatal Exception thrown on Scheduler.Worker thread. at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:62) at

    android.os.Handler.handleCallback(Handler.java:739) at android.os.Handler.dispatchMessage(Handler.java:95) at android.os.Looper.loop(Looper.java:145) at android.app.ActivityThread.main(ActivityThread.java:6141) at java.lang.reflect.Method.invoke(Method.java:-2) at java.lang.reflect.Method.invoke(Method.java:372) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1399) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1194) Caused by: rx.exceptions.OnErrorFailedException: Error occurred when trying to propagate error to Observer.onError at rx.observers.SafeSubscriber._onError(SafeSubscriber.java:192) at rx.observers.SafeSubscriber.onError(SafeSubscriber.java:120) at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber.checkTerminated(OperatorObserveOn.java:254) at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber.call(OperatorObserveOn.java:186) at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55) at android.os.Handler.handleCallback(Handler.java:739) at android.os.Handler.dispatchMessage(Handler.java:95) at android.os.Looper.loop(Looper.java:145) at android.app.ActivityThread.main(ActivityThread.java:6141) at java.lang.reflect.Method.invoke(Method.java:-2) at java.lang.reflect.Method.invoke(Method.java:372) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1399) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1194) Caused by: rx.exceptions.CompositeException: 2 exceptions occurred. at rx.observers.SafeSubscriber._onError(SafeSubscriber.java:192) at rx.observers.SafeSubscriber.onError(SafeSubscriber.java:120) at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber.checkTerminated(OperatorObserveOn.java:254) at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber.call(OperatorObserveOn.java:186) at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55) at android.os.Handler.handleCallback(Handler.java:739) at android.os.Handler.dispatchMessage(Handler.java:95) at android.os.Looper.loop(Looper.java:145) at android.app.ActivityThread.main(ActivityThread.java:6141) at java.lang.reflect.Method.invoke(Method.java:-2) at java.lang.reflect.Method.invoke(Method.java:372) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1399) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1194) Caused by: rx.exceptions.CompositeException$CompositeExceptionCausalChain: Chain of Causes for CompositeException In Order Received => at android.util.Log.getStackTraceString(Log.java:499) at com.android.internal.os.RuntimeInit.Clog_e(RuntimeInit.java:59) at com.android.internal.os.RuntimeInit.access$200(RuntimeInit.java:43) at com.android.internal.os.RuntimeInit$UncaughtHandler.uncaughtException(RuntimeInit.java:91) at com.bugsnag.android.ExceptionHandler.uncaughtException(ExceptionHandler.java:56) at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:693) at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:690) at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:66) at android.os.Handler.handleCallback(Handler.java:739) at android.os.Handler.dispatchMessage(Handler.java:95) at android.os.Looper.loop(Looper.java:145) at android.app.ActivityThread.main(ActivityThread.java:6141) at java.lang.reflect.Method.invoke(Method.java:-2) at java.lang.reflect.Method.invoke(Method.java:372) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1399) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1194) Caused by: com.airbnb.airrequest.NetworkException at com.airbnb.airrequest.ObservableFactory.lambda$responseMapper$1(ObservableFactory.java:59) at com.airbnb.airrequest.ObservableFactory.access$lambda$1(ObservableFactory.java:-1) at com.airbnb.airrequest.ObservableFactory$$Lambda$4.call(Unknown:-1) at rx.internal.operators.OperatorMap$1.onNext(OperatorMap.java:54) at rx.internal.operators.OperatorUnsubscribeOn$1.onNext(OperatorUnsubscribeOn.java:52) at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber.call(OperatorObserveOn.java:207) at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:422) at java.util.concurrent.FutureTask.run(FutureTask.java:237) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:152) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:265) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587) at java.lang.Thread.run(Thread.java:818) Caused by: java.lang.ClassCastException: com.airbnb.android.responses.AirBatchResponse cannot be cast to com.airbnb.android.responses.ErrorResponse at com.airbnb.android.utils.NetworkUtil.error(NetworkUtil.java:357) at com.airbnb.android.utils.NetworkUtil.isExpiredOauthError(NetworkUtil.java:363) at com.airbnb.android.requests.base.ErrorLoggingAction.checkForExpiredToken(ErrorLoggingAction.java:96) at com.airbnb.android.requests.base.ErrorLoggingAction.call(ErrorLoggingAction.java:83) at com.airbnb.android.requests.base.ErrorLoggingAction.call(ErrorLoggingAction.java:33) at rx.Observable$10.onError(Observable.java:4561) at rx.internal.operators.OperatorDoOnEach$1.onError(OperatorDoOnEach.java:65) at com.airbnb.rxgroups.GroupSubscriptionTransformer$1.onError(GroupSubscriptionTransformer.java:45) at rx.Observable$30.onError(Observable.java:8280) at rx.observers.SafeSubscriber._onError(SafeSubscriber.java:157) at rx.observers.SafeSubscriber.onError(SafeSubscriber.java:120) at rx.internal.operators.OperatorDoOnEach$1.onError(OperatorDoOnEach.java:71) at rx.subjects.SubjectSubscriptionManager$SubjectObserver.onError(SubjectSubscriptionManager.java:227) at rx.internal.operators.NotificationLite.accept(NotificationLite.java:147) at rx.subjects.ReplaySubject$UnboundedReplayState.accept(ReplaySubject.java:465) at rx.subjects.ReplaySubject$UnboundedReplayState.replayObserverFromIndex(ReplaySubject.java:514) at rx.subjects.ReplaySubject$UnboundedReplayState.replayObserver(ReplaySubject.java:502) at rx.subjects.ReplaySubject.caughtUp(ReplaySubject.java:427) at rx.subjects.ReplaySubject.onError(ReplaySubject.java:387) at rx.Observable$30.onError(Observable.java:8280) at rx.observers.SafeSubscriber._onError(SafeSubscriber.java:157) at rx.observers.SafeSubscriber.onError(SafeSubscriber.java:120) at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber.checkTerminated(OperatorObserveOn.java:254) at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber.call(OperatorObserveOn.java:186) at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55) at android.os.Handler.handleCallback(Handler.java:739) at android.os.Handler.dispatchMessage(Handler.java:95) at android.os.Looper.loop(Looper.java:145) at android.app.ActivityThread.main(ActivityThread.java:6141) at java.lang.reflect.Method.invoke(Method.java:-2) at java.lang.reflect.Method.invoke(Method.java:372) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1399) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1194) Caused by: java.lang.ClassCastException: rx.exceptions.CompositeException cannot be cast to com.airbnb.airrequest.NetworkException at com.airbnb.android.requests.AirBatchRequestObserver.onError(AirBatchRequestObserver.java:60) at rx.Observable$30.onError(Observable.java:8280) at rx.observers.SafeSubscriber._onError(SafeSubscriber.java:157) at rx.observers.SafeSubscriber.onError(SafeSubscriber.java:120) at rx.internal.operators.OperatorDoOnEach$1.onError(OperatorDoOnEach.java:68) at com.airbnb.rxgroups.GroupSubscriptionTransformer$1.onError(GroupSubscriptionTransformer.java:45) at rx.Observable$30.onError(Observable.java:8280) at rx.observers.SafeSubscriber._onError(SafeSubscriber.java:157) at rx.observers.SafeSubscriber.onError(SafeSubscriber.java:120) at rx.internal.operators.OperatorDoOnEach$1.onError(OperatorDoOnEach.java:71) at rx.subjects.SubjectSubscriptionManager$SubjectObserver.onError(SubjectSubscriptionManager.java:227) at rx.internal.operators.NotificationLite.accept(NotificationLite.java:147) at rx.subjects.ReplaySubject$UnboundedReplayState.accept(ReplaySubject.java:465) at rx.subjects.ReplaySubject$UnboundedReplayState.replayObserverFromIndex(ReplaySubject.java:514) at rx.subjects.ReplaySubject$UnboundedReplayState.replayObserver(ReplaySubject.java:502) at rx.subjects.ReplaySubject.caughtUp(ReplaySubject.java:427) at rx.subjects.ReplaySubject.onError(ReplaySubject.java:387) at rx.Observable$30.onError(Observable.java:8280) at rx.observers.SafeSubscriber._onError(SafeSubscriber.java:157) at rx.observers.SafeSubscriber.onError(SafeSubscriber.java:120) at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber.checkTerminated(OperatorObserveOn.java:254) at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber.call(OperatorObserveOn.java:186) at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55) at android.os.Handler.handleCallback(Handler.java:739) at android.os.Handler.dispatchMessage(Handler.java:95) at android.os.Looper.loop(Looper.java:145) at android.app.ActivityThread.main(ActivityThread.java:6141) at java.lang.reflect.Method.invoke(Method.java:-2) at java.lang.reflect.Method.invoke(Method.java:372) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1399) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1194)
  31. Caused by: java.lang.ClassCastException: rx.exceptions.CompositeException cannot be cast to com.airbnb.airrequest.NetworkException at

    com.airbnb.android.requests.AirBatchRequestObserver.onError(AirBatchRequestObserver.java:60) at rx.Observable$30.onError(Observable.java:8280) at rx.observers.SafeSubscriber._onError(SafeSubscriber.java:157) at rx.observers.SafeSubscriber.onError(SafeSubscriber.java:120) at rx.internal.operators.OperatorDoOnEach$1.onError(OperatorDoOnEach.java:68) at com.airbnb.rxgroups.GroupSubscriptionTransformer$1.onError(GroupSubscriptionTransformer.java:45) at rx.Observable$30.onError(Observable.java:8280) at rx.observers.SafeSubscriber._onError(SafeSubscriber.java:157) at rx.observers.SafeSubscriber.onError(SafeSubscriber.java:120) at rx.internal.operators.OperatorDoOnEach$1.onError(OperatorDoOnEach.java:71) at rx.subjects.SubjectSubscriptionManager$SubjectObserver.onError(SubjectSubscriptionManager.java:227) at rx.internal.operators.NotificationLite.accept(NotificationLite.java:147) at rx.subjects.ReplaySubject$UnboundedReplayState.accept(ReplaySubject.java:465) at rx.subjects.ReplaySubject$UnboundedReplayState.replayObserverFromIndex(ReplaySubject.java:514) at rx.subjects.ReplaySubject$UnboundedReplayState.replayObserver(ReplaySubject.java:502) at rx.subjects.ReplaySubject.caughtUp(ReplaySubject.java:427) at rx.subjects.ReplaySubject.onError(ReplaySubject.java:387) at rx.Observable$30.onError(Observable.java:8280) at rx.observers.SafeSubscriber._onError(SafeSubscriber.java:157) at rx.observers.SafeSubscriber.onError(SafeSubscriber.java:120) at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber.checkTerminated(OperatorObserveOn.java:254) at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber.call(OperatorObserveOn.java:186) at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55) at android.os.Handler.handleCallback(Handler.java:739) at android.os.Handler.dispatchMessage(Handler.java:95) at android.os.Looper.loop(Looper.java:145) at android.app.ActivityThread.main(ActivityThread.java:6141) at java.lang.reflect.Method.invoke(Method.java:-2) at java.lang.reflect.Method.invoke(Method.java:372) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1399) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1194)
  32. Common Pitfalls

  33. subscribe()

  34. return observableFactory.<T>toObservable(this) .compose(this.<T>transform(observableRequest)) .observeOn(Schedulers.io()) .map(new ResponseMetadataOperator<>(this)) .flatMap(this::mapResponse) .observeOn(AndroidSchedulers.mainThread())
 .<AirResponse<T>>compose(group.transform(tag))
 .doOnError(new

    ErrorLoggingAction(request))
 .doOnError(NetworkUtil::checkForExpiredToken) .subscribeOn(Schedulers.io())
 .subscribe(request.observer());
  35. observeOn()

  36. return observableFactory.<T>toObservable(this) .compose(this.<T>transform(observableRequest)) .observeOn(Schedulers.io()) .map(new ResponseMetadataOperator<>(this)) .flatMap(this::mapResponse) .observeOn(AndroidSchedulers.mainThread())
 .<AirResponse<T>>compose(group.transform(tag))
 .doOnError(new

    ErrorLoggingAction(request))
 .doOnError(NetworkUtil::checkForExpiredToken)
 .subscribe(request.observer());
  37. subscribeOn()

  38. return observableFactory.<T>toObservable(this) .compose(this.<T>transform(observableRequest)) .observeOn(Schedulers.io()) .map(new ResponseMetadataOperator<>(this)) .flatMap(this::mapResponse) .observeOn(AndroidSchedulers.mainThread())
 .<AirResponse<T>>compose(group.transform(tag))
 .doOnError(new

    ErrorLoggingAction(request))
 .doOnError(NetworkUtil::checkForExpiredToken) .subscribeOn(Schedulers.io())
 .subscribe(request.observer());
  39. flatMap()

  40. None
  41. Error Handling

  42. try { return observableFactory.<T>toObservable(this) .compose(this.<T>transform(observableRequest)) .observeOn(Schedulers.io()) .map(new ResponseMetadataOperator<>(this)) .flatMap(this::mapResponse) .observeOn(AndroidSchedulers.mainThread())


    .subscribe(request.observer()); } catch(Exception e) { Log.e(TAG, "whoops :("); }
  43. return observableRequest .rawRequest() .<Observable<Response<T>>>newCall() .observeOn(Schedulers.io()) .unsubscribeOn(Schedulers.io()) .flatMap(responseMapper(airRequest)) .onErrorResumeNext(errorMapper(airRequest));

  44. return observableFactory.<T>toObservable(this) .compose(this.<T>transform(observableRequest)) .observeOn(Schedulers.io()) .map(new ResponseMetadataOperator<>(this)) .flatMap(this::mapResponse) .observeOn(AndroidSchedulers.mainThread())
 .<AirResponse<T>>compose(group.transform(tag))
 .doOnError(new

    ErrorLoggingAction(request))
 .doOnError(NetworkUtil::checkForExpiredToken) .subscribeOn(Schedulers.io())
 .subscribe(request.observer());
  45. Unit Testing

  46. @Test public void testErrorResponseNonJSON() { server.enqueue(new MockResponse() .setBody("something bad happened")

    .setResponseCode(500)); TestRequest request = new TestRequest.Builder<String>().build(); TestSubscriber<AirResponse<String>> subscriber = new TestSubscriber<>(); observableFactory.<String>toObservable(request).subscribe(subscriber); subscriber.awaitTerminalEvent(3L, TimeUnit.SECONDS); NetworkException exception = (NetworkException) subscriber.getOnErrorEvents().get(0); assertThat(exception.errorResponse(), equalTo(null)); assertThat(exception.bodyString(), equalTo("something bad happened")); }
  47. @Test public void testUnicodeHeader() { server.enqueue(new MockResponse().setBody("\"Hello World\"")); TestRequest request

    = new TestRequest.Builder<String>() .header("Bogus", "Ӿ嶆櫮מ") .build(); observableFactory.toObservable(request) .toBlocking() .first(); RecordedRequest recordedRequest = server.takeRequest(); assertThat(recordedRequest.getHeader("Bogus"), equalTo("????")); }
  48. Memory Leaks

  49. private final CompositeSubscription pendingSubscriptions = new CompositeSubscription(); @Override public void

    onCreate() { pendingSubscriptions.add( observable.subscribe(observer)); } @Override public void onDestroy() { pendingSubscriptions.clear(); }
  50. compose()

  51. None
  52. Additional Resources

  53. Reactive.community: Ben Christensen, Reactive Extensions at Netflix HTTPS://WWW.YOUTUBE.COM/WATCH?V=ET_SMMXKE5S

  54. Cycle.js and functional reactive user interfaces Andre Stalz HTTPS://WWW.YOUTUBE.COM/WATCH?V=UNZNFTSKSYG

  55. LDN Functionals Kris Jenkins: What is Functional Programming? HTTPS://WWW.YOUTUBE.COM/WATCH?V=TQRTTSIPYE4

  56. Questions?

  57. Thanks!