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

Adopting RxJava on Airbnb Andoid

Adopting RxJava on Airbnb Andoid

felipecsl

March 15, 2016
Tweet

More Decks by felipecsl

Other Decks in Programming

Transcript

  1. Adopting RxJava on
    Airbnb Android
    FELIPE LIMA / MARCH 14, 2016 / KOTLIN ∪ ANDROID MEETUP

    View Slide

  2. Why RxJava?

    View Slide

  3. We make
    shitty
    software

    View Slide

  4. Mobile
    Engineering is
    hard

    View Slide

  5. ReactiveX
    AN API FOR ASYNCHRONOUS PROGRAMMING
    WITH OBSERVABLE STREAMS

    View Slide

  6. Streams

    View Slide

  7. Reactive
    programming
    is hard

    View Slide

  8. Too many
    concepts

    View Slide

  9. • Observable & Observer
    Concepts

    View Slide

  10. • Observable & Observer
    • Subscriber
    Concepts

    View Slide

  11. • Observable & Observer
    • Subscriber
    • Subscription
    Concepts

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  18. WAT?!

    View Slide

  19. • Team Size: Getting everyone onboard and up to speed
    • Learning Curve: Steep, overwhelming
    • Terminology: Too many new concepts
    • Debugging: Unreadable stack traces
    Adoption
    Challenges

    View Slide

  20. 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

  21. 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

  22. Common Pitfalls

    View Slide

  23. observeOn()

    View Slide

  24. 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

  25. subscribeOn()

    View Slide

  26. 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

  27. Error Handling

    View 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 Slide

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

    View Slide

  30. Unit Testing

    View Slide

  31. @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

  32. @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

  33. Memory Leaks

    View Slide

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

    View Slide

  35. Additional Resources

    View Slide

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

    View Slide

  37. Questions?

    View Slide

  38. Thanks!

    View Slide