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 full-size slide

  2. We make
    shitty
    software

    View full-size slide

  3. Mobile
    Engineering is
    hard

    View full-size slide

  4. ReactiveX
    AN API FOR ASYNCHRONOUS PROGRAMMING
    WITH OBSERVABLE STREAMS

    View full-size slide

  5. Reactive
    programming

    View full-size slide

  6. Programming
    Paradigms 101

    View full-size slide

  7. Object Oriented
    programming

    View full-size slide

  8. • 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 full-size slide

  9. Side-Effects are
    the Complexity
    Iceberg.

    View full-size slide

  10. Functional
    programming

    View full-size slide

  11. • 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 full-size slide

  12. Now back to
    reactive…

    View full-size slide

  13. • Observable & Observer
    Concepts

    View full-size slide

  14. • Observable & Observer
    • Subscriber
    Concepts

    View full-size slide

  15. • Observable & Observer
    • Subscriber
    • Subscription
    Concepts

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  22. /**

    * 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 full-size slide

  23. Adoption
    challenges

    View full-size slide

  24. • 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 full-size slide

  25. 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 full-size slide

  26. 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 full-size slide

  27. Common Pitfalls

    View full-size slide

  28. 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 full-size slide

  29. 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 full-size slide

  30. subscribeOn()

    View full-size slide

  31. 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 full-size slide

  32. Error Handling

    View full-size slide

  33. 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 full-size slide

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

    View full-size slide

  35. 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 full-size slide

  36. Unit Testing

    View full-size slide

  37. @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 full-size slide

  38. @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 full-size slide

  39. Memory Leaks

    View full-size slide

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

    View full-size slide

  41. Additional Resources

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide