Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Why RxJava?

Slide 3

Slide 3 text

We make shitty software

Slide 4

Slide 4 text

Mobile Engineering is hard

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

ReactiveX AN API FOR ASYNCHRONOUS PROGRAMMING WITH OBSERVABLE STREAMS

Slide 7

Slide 7 text

Streams

Slide 8

Slide 8 text

Reactive programming

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

Programming Paradigms 101

Slide 11

Slide 11 text

Object Oriented programming

Slide 12

Slide 12 text

• 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

Slide 13

Slide 13 text

Side-Effects are the Complexity Iceberg.

Slide 14

Slide 14 text

Functional programming

Slide 15

Slide 15 text

• 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

Slide 16

Slide 16 text

Now back to reactive…

Slide 17

Slide 17 text

• Observable & Observer Concepts

Slide 18

Slide 18 text

• Observable & Observer • Subscriber Concepts

Slide 19

Slide 19 text

• Observable & Observer • Subscriber • Subscription Concepts

Slide 20

Slide 20 text

• Observable & Observer • Subscriber • Subscription • Producer Concepts

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

/**
 * 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> func) {
 return switchOnNext(map(func));
 }

Slide 27

Slide 27 text

WAT?!

Slide 28

Slide 28 text

Adoption challenges

Slide 29

Slide 29 text

• 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

Slide 30

Slide 30 text

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)

Slide 31

Slide 31 text

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)

Slide 32

Slide 32 text

Common Pitfalls

Slide 33

Slide 33 text

subscribe()

Slide 34

Slide 34 text

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());

Slide 35

Slide 35 text

observeOn()

Slide 36

Slide 36 text

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());

Slide 37

Slide 37 text

subscribeOn()

Slide 38

Slide 38 text

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());

Slide 39

Slide 39 text

flatMap()

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

Error Handling

Slide 42

Slide 42 text

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 :("); }

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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());

Slide 45

Slide 45 text

Unit Testing

Slide 46

Slide 46 text

@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")); }

Slide 47

Slide 47 text

@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("????")); }

Slide 48

Slide 48 text

Memory Leaks

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

compose()

Slide 51

Slide 51 text

No content

Slide 52

Slide 52 text

Additional Resources

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

Questions?

Slide 57

Slide 57 text

Thanks!