Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Adopting RxJava on Airbnb Android

Adopting RxJava on Airbnb Android

Presentation at Droidcon DE on June 17th, 2016

felipecsl

June 17, 2016
Tweet

More Decks by felipecsl

Other Decks in Programming

Transcript

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

    View Slide

  2. Why RxJava?

    View Slide

  3. We make
    shitty
    software

    View Slide

  4. Mobile
    Engineering is
    hard

    View Slide

  5. View Slide

  6. ReactiveX
    AN API FOR ASYNCHRONOUS PROGRAMMING
    WITH OBSERVABLE STREAMS

    View Slide

  7. Streams

    View Slide

  8. Reactive
    programming

    View Slide

  9. View Slide

  10. Programming
    Paradigms 101

    View Slide

  11. Object Oriented
    programming

    View Slide

  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

    View Slide

  13. Side-Effects are
    the Complexity
    Iceberg.

    View Slide

  14. Functional
    programming

    View Slide

  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

    View Slide

  16. Now back to
    reactive…

    View Slide

  17. • Observable & Observer
    Concepts

    View Slide

  18. • Observable & Observer
    • Subscriber
    Concepts

    View Slide

  19. • Observable & Observer
    • Subscriber
    • Subscription
    Concepts

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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 Observable switchMap(
    Func1 super T, ? extends Observable extends R>> func) {

    return switchOnNext(map(func));

    }

    View Slide

  27. WAT?!

    View Slide

  28. Adoption
    challenges

    View Slide

  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

    View Slide

  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)

    View Slide

  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)

    View Slide

  32. Common Pitfalls

    View Slide

  33. subscribe()

    View Slide

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

    .>compose(group.transform(tag))

    .doOnError(new ErrorLoggingAction(request))

    .doOnError(NetworkUtil::checkForExpiredToken)
    .subscribeOn(Schedulers.io())

    .subscribe(request.observer());

    View Slide

  35. observeOn()

    View Slide

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

    .>compose(group.transform(tag))

    .doOnError(new ErrorLoggingAction(request))

    .doOnError(NetworkUtil::checkForExpiredToken)

    .subscribe(request.observer());

    View Slide

  37. subscribeOn()

    View Slide

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

    .>compose(group.transform(tag))

    .doOnError(new ErrorLoggingAction(request))

    .doOnError(NetworkUtil::checkForExpiredToken)
    .subscribeOn(Schedulers.io())

    .subscribe(request.observer());

    View Slide

  39. flatMap()

    View Slide

  40. View Slide

  41. Error Handling

    View Slide

  42. try {
    return observableFactory.toObservable(this)
    .compose(this.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 :(");
    }

    View Slide

  43. return observableRequest
    .rawRequest()
    .>>newCall()
    .observeOn(Schedulers.io())
    .unsubscribeOn(Schedulers.io())
    .flatMap(responseMapper(airRequest))
    .onErrorResumeNext(errorMapper(airRequest));

    View Slide

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

    .>compose(group.transform(tag))

    .doOnError(new ErrorLoggingAction(request))

    .doOnError(NetworkUtil::checkForExpiredToken)
    .subscribeOn(Schedulers.io())

    .subscribe(request.observer());

    View Slide

  45. Unit Testing

    View Slide

  46. @Test public void testErrorResponseNonJSON() {
    server.enqueue(new MockResponse()
    .setBody("something bad happened")
    .setResponseCode(500));
    TestRequest request = new TestRequest.Builder().build();
    TestSubscriber> subscriber = new TestSubscriber<>();
    observableFactory.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"));
    }

    View Slide

  47. @Test public void testUnicodeHeader() {
    server.enqueue(new MockResponse().setBody("\"Hello World\""));
    TestRequest request = new TestRequest.Builder()
    .header("Bogus", "Ӿ嶆櫮מ")
    .build();
    observableFactory.toObservable(request)
    .toBlocking()
    .first();
    RecordedRequest recordedRequest = server.takeRequest();
    assertThat(recordedRequest.getHeader("Bogus"), equalTo("????"));
    }

    View Slide

  48. Memory Leaks

    View Slide

  49. private final CompositeSubscription pendingSubscriptions =
    new CompositeSubscription();
    @Override public void onCreate() {
    pendingSubscriptions.add(
    observable.subscribe(observer));
    }
    @Override public void onDestroy() {
    pendingSubscriptions.clear();
    }

    View Slide

  50. compose()

    View Slide

  51. View Slide

  52. Additional Resources

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  56. Questions?

    View Slide

  57. Thanks!

    View Slide