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

Rx in Multiplatformland (MobileOptimized 2019)

Rx in Multiplatformland (MobileOptimized 2019)

File with original colors can be downloaded here: https://bit.ly/2N1wlF6

Reactive programming has firmly become one of the modern Android developer's tools. More and more teams apply fully reactive architectures for their apps utilizing MV* approaches. This plays well for separating our OS-independent business logic from presentation layer. And while it can be implemented really nice using Kotlin, what if we want to expand this solid base further and don't repeat ourselves on every platform? What if we want to enter Multiplatformland?

Obviously, our best friend, RxJava, is not ready for that journey. But do we really have no alternatives? In this talk we'll discuss multiplatform friends of RxJava, such as Flow from Kotlin team and Reaktive from Badoo, their pros & cons, and how usable are they for our tasks.

Video: https://youtu.be/FazAHlmlwws

Sergey Ryabov

October 19, 2019
Tweet

More Decks by Sergey Ryabov

Other Decks in Programming

Transcript

  1. Last year at Mobius ✦ "How to cook a well

    done MVI for Android" talk1 ✦ Platform-agnostic ✦ Based on Kotlin. MPP! ! ✦ Based on RxJava. MPP? " ✦ Maybe RxKotlin? (for real) 1 https:/ /www.youtube.com/watch?v=hBkQkjWnAjg
  2. This year ✦ In March my friend Kostya sent me

    a link to a Reaktive repo ✦ In April Roman wrote an article about Flow Preview2 2 https:/ /medium.com/@elizarov/cold-flows-hot-channels-d74769805f9
  3. Fast-forward 6 months ✦ Reaktive - 1.0.1 ✦ Flow -

    stable since Coroutines 1.3.0 ✦ Supported by SQLDelight 1.2.0 ✦ Supported by Room 2.2.0
  4. RxJava2 Observable.just(1, 2, 3) .filter { it % 2 ==

    0 } .map { it * 10 } .subscribe { println(it) }
  5. Reaktive observableOf(1, 2, 3) .filter { it % 2 ==

    0 } .map { it * 10 } .subscribe { println(it) }
  6. Flow flowOf(1, 2, 3) .filter { it % 2 ==

    0 } .map { it * 10 } .collect { println(it) }
  7. Flow flowOf(1, 2, 3) .filter { it % 2 ==

    0 } .map { it * 10 } .collect { println(it) }
  8. Flow. In a nutshell interface Flow<out T> { suspend fun

    collect(collector: FlowCollector<T>) } interface FlowCollector<in T> { suspend fun emit(value: T) }
  9. How does it work? Flow fun <T, R> Flow<T>.map(transform: suspend

    (value: T) -> R): Flow<R> = flow { collect { value -> emit(transform(value)) } }
  10. How does it work? Flow fun <T, R> Flow<T>.map(transform: suspend

    (value: T) -> R): Flow<R> = flow { this: FlowCollector -> [email protected] { value -> this.emit(transform(value)) } }
  11. How does it work? Flow fun <T, R> Flow<T>.map(transform: suspend

    (value: T) -> R): Flow<R> = flow { this: FlowCollector -> [email protected] { value -> this.emit(transform(value)) } }
  12. How does it work? Flow fun <T, R> Flow<T>.map(transform: suspend

    (value: T) -> R): Flow<R> = flow { this: FlowCollector -> [email protected] { value -> this.emit(transform(value)) } }
  13. How does it work? Flow fun <T, R> Flow<T>.map(transform: suspend

    (value: T) -> R): Flow<R> = flow { this: FlowCollector -> [email protected] { value -> this.emit(transform(value)) } }
  14. How does it work? Flow fun <T, R> Flow<T>.map(transform: suspend

    (value: T) -> R): Flow<R> = flow { this: FlowCollector -> [email protected] { value -> this.emit(transform(value)) } }
  15. How does it work? Reaktive fun <T, R> Observable<T>.map(mapper: (T)

    -> R): Observable<R> = observable { emitter -> subscribeSafe( object : ObservableObserver<T>, CompletableCallbacks by emitter { override fun onSubscribe(disposable: Disposable) { emitter.setDisposable(disposable) } override fun onNext(value: T) { emitter.tryCatch(block = { mapper(value) }, onSuccess = emitter::onNext) } } ) }
  16. How does it work? Reaktive fun <T, R> Observable<T>.map(mapper: (T)

    -> R): Observable<R> = observable { emitter -> subscribeSafe( object : ObservableObserver<T>, CompletableCallbacks by emitter { override fun onSubscribe(disposable: Disposable) { emitter.setDisposable(disposable) } override fun onNext(value: T) { emitter.tryCatch(block = { mapper(value) }, onSuccess = emitter::onNext) } } ) }
  17. How does it work? Reaktive fun <T, R> Observable<T>.map(mapper: (T)

    -> R): Observable<R> = observable { emitter -> subscribeSafe( object : ObservableObserver<T>, CompletableCallbacks by emitter { override fun onSubscribe(disposable: Disposable) { emitter.setDisposable(disposable) } override fun onNext(value: T) { emitter.tryCatch(block = { mapper(value) }, onSuccess = emitter::onNext) } } ) }
  18. How does it work? Reaktive fun <T, R> Observable<T>.map(mapper: (T)

    -> R): Observable<R> = observable { emitter -> subscribeSafe( object : ObservableObserver<T>, CompletableCallbacks by emitter { override fun onSubscribe(disposable: Disposable) { emitter.setDisposable(disposable) } override fun onNext(value: T) { emitter.tryCatch(block = { mapper(value) }, onSuccess = emitter::onNext) } } ) }
  19. How does it work? Reaktive fun <T, R> Observable<T>.map(mapper: (T)

    -> R): Observable<R> = observable { emitter -> subscribeSafe( object : ObservableObserver<T>, CompletableCallbacks by emitter { override fun onSubscribe(disposable: Disposable) { emitter.setDisposable(disposable) } override fun onNext(value: T) { emitter.tryCatch(block = { mapper(value) }, onSuccess = emitter::onNext) } } ) }
  20. How does it work? Reaktive fun <T, R> Observable<T>.map(mapper: (T)

    -> R): Observable<R> = observable { emitter -> subscribeSafe( object : ObservableObserver<T>, CompletableCallbacks by emitter { override fun onSubscribe(disposable: Disposable) { emitter.setDisposable(disposable) } override fun onNext(value: T) { emitter.tryCatch(block = { mapper(value) }, onSuccess = emitter::onNext) } } ) }
  21. How does it work? Reaktive fun <T, R> Observable<T>.map(mapper: (T)

    -> R): Observable<R> = observable { emitter -> subscribeSafe( object : ObservableObserver<T>, CompletableCallbacks by emitter { override fun onSubscribe(disposable: Disposable) { emitter.setDisposable(disposable) } override fun onNext(value: T) { emitter.tryCatch(block = { mapper(value) }, onSuccess = emitter::onNext) } } ) }
  22. How does it work? RxJa... oh, damn public final class

    ObservableMap<T, U> extends AbstractObservableWithUpstream<T, U> { final Function<? super T, ? extends U> function; public ObservableMap(ObservableSource<T> source, Function<? super T, ? extends U> function) { super(source); this.function = function; } @Override public void subscribeActual(Observer<? super U> t) { source.subscribe(new MapObserver<T, U>(t, function)); } static final class MapObserver<T, U> extends BasicFuseableObserver<T, U> { final Function<? super T, ? extends U> mapper; MapObserver(Observer<? super U> actual, Function<? super T, ? extends U> mapper) { super(actual); this.mapper = mapper; } @Override public void onNext(T t) { if (done) { return; } if (sourceMode != NONE) { downstream.onNext(null); return; } U v; try { v = ObjectHelper.requireNonNull(mapper.apply(t), "The mapper function returned a null value."); } catch (Throwable ex) { fail(ex); return; } downstream.onNext(v); } @Override public int requestFusion(int mode) { return transitiveBoundaryFusion(mode); } @Nullable @Override public U poll() throws Exception { T t = qd.poll(); return t != null ? ObjectHelper.<U>requireNonNull(mapper.apply(t), "The mapper function returned a null value.") : null; } } }
  23. Operators Reaktive Flow flatMap flatMapMerge concatMap flatMapConcat switchMap flatMapLatest flatten

    flattenConcat merge flattenMerge combineLatest combine skip drop onError* catch ... ...
  24. Missing in Flow ✦ groupBy ✦ publish ✦ share 3

    ✦ replay ✦ withLatestFrom ✦ takeUntil ✦ parallel 3 https:/ /github.com/Kotlin/kotlinx.coroutines/issues/1261
  25. Missing in Flow ✦ groupBy 5 ✦ publish 5 ✦

    share ✦ replay 5 ✦ withLatestFrom ✦ takeUntil 5 ✦ parallel 5 5 https:/ /github.com/akarnokd/kotlin-flow-extensions
  26. Threading. Reaktive observableOf(1, 2, 3) .filter { it % 2

    == 0 } .subscribeOn(ioScheduler) .observeOn(computationScheduler) .map { it * 10 } .observeOn(mainScheduler) .subscribe { println(it) }
  27. Threading. Reaktive observableOf(1, 2, 3) .filter { it % 2

    == 0 } .subscribeOn(ioScheduler) .observeOn(computationScheduler) .map { it * 10 } .observeOn(mainScheduler) .subscribe { println(it) }
  28. Threading. Reaktive observableOf(1, 2, 3) .filter { it % 2

    == 0 } .subscribeOn(ioScheduler) .observeOn(computationScheduler) .map { it * 10 } .observeOn(mainScheduler) .subscribe { println(it) }
  29. Threading. Reaktive observableOf(1, 2, 3) .filter { it % 2

    == 0 } .subscribeOn(ioScheduler) .observeOn(computationScheduler) .map { it * 10 } .observeOn(mainScheduler) .subscribe { println(it) }
  30. Threading. Reaktive observableOf(1, 2, 3) .filter { it % 2

    == 0 } .subscribeOn(ioScheduler) .observeOn(computationScheduler) .map { it * 10 } .observeOn(mainScheduler) .subscribe { println(it) }
  31. Threading. Reaktive observableOf(1, 2, 3) .filter { it % 2

    == 0 } .subscribeOn(ioScheduler) .observeOn(computationScheduler) .map { it * 10 } .observeOn(mainScheduler) .subscribe { println(it) }
  32. Threading. Reaktive observableOf(1, 2, 3) .filter { it % 2

    == 0 } .subscribeOn(ioScheduler) .observeOn(computationScheduler) .map { it * 10 } .observeOn(mainScheduler) .subscribe { println(it) }
  33. Threading. Flow scope.launch(Dispatchers.MAIN) { flowOf(1, 2, 3) .filter { it

    % 2 == 0 } .flowOn(Dispatchers.IO) .map { it * 10 } .flowOn(Dispatchers.Default) .collect { println(it) } }
  34. Threading. Flow scope.launch(Dispatchers.MAIN) { flowOf(1, 2, 3) .filter { it

    % 2 == 0 } .flowOn(Dispatchers.IO) .map { it * 10 } .flowOn(Dispatchers.Default) .collect { println(it) } }
  35. Threading. Flow scope.launch(Dispatchers.MAIN) { flowOf(1, 2, 3) .filter { it

    % 2 == 0 } .flowOn(Dispatchers.IO) .map { it * 10 } .flowOn(Dispatchers.Default) .collect { println(it) } }
  36. Threading. Flow scope.launch(Dispatchers.MAIN) { flowOf(1, 2, 3) .filter { it

    % 2 == 0 } .flowOn(Dispatchers.IO) .map { it * 10 } .flowOn(Dispatchers.Default) .collect { println(it) } }
  37. Threading. Flow scope.launch(Dispatchers.MAIN) { flowOf(1, 2, 3) .filter { it

    % 2 == 0 } .flowOn(Dispatchers.IO) .map { it * 10 } .flowOn(Dispatchers.Default) .collect { println(it) } }
  38. Threading. Flow scope.launch(Dispatchers.MAIN) { flowOf(1, 2, 3) .filter { it

    % 2 == 0 } .flowOn(Dispatchers.IO) .map { it * 10 } .flowOn(Dispatchers.Default) .collect { println(it) } }
  39. Threading. Flow scope.launch(Dispatchers.MAIN) { flowOf(1, 2, 3) .filter { it

    % 2 == 0 } .flowOn(Dispatchers.IO) .map { it * 10 } .flowOn(Dispatchers.Default) .collect { println(it) } }
  40. Schedulers RxJava2 Reaktive Flow AndroidSchedulers.mainThread() mainScheduler Dispatchers.MAIN Schedulers.io() ioScheduler Dispatchers.IO

    6 Schedulers.computation() computationScheduler Dispatchers.Default Schedulers.trampoline() trampolineScheduler Executor.asCoroutineDispatcher() 6 Schedulers.newThread() i.r.Scheduler.asReaktive() 6 Executor.asCoroutineDispatcher() 6 Schedulers.single() i.r.Scheduler.asReaktive() 6 Executor.asCoroutineDispatcher() 6 6 only on JVM
  41. Subjects ✦ RxJava2: Behavior, Publish, Replay ✦ Reaktive: Behavior, Publish

    ✦ Flow: Behavior (ConflatedBroadcastChannel 7) ✦ Flow + Karnok5: Behavior, Publish, Replay 5 https:/ /github.com/akarnokd/kotlin-flow-extensions 7 DataFlow Proposal: https:/ /github.com/Kotlin/kotlinx.coroutines/pull/1354
  42. Interop & Gradual adoption. Flow Check this out: kotlinx-coroutines-core/common/src/flow/Migration.kt Deprecated(

    level = DeprecationLevel.ERROR, message = "Flow analogue of 'flatten' is 'flattenConcat'", replaceWith = ReplaceWith("flattenConcat()") ) fun <T> Flow<Flow<T>>.flatten(): Flow<T> = noImpl()
  43. Error handling bits dataObservable() .flatMap { map(it) } .subscribe( onNext

    = { crashHandle(it) }, onError = { report(it) } )
  44. Error handling bits. Flow dataFlow() .flatMap { map(it) } .subscribe(

    onNext = { crashHandle(it) }, onError = { report(it) } )
  45. Error handling bits. Flow dataFlow() .flatMapMerge { map(it) } .subscribe(

    onNext = { crashHandle(it) }, onError = { report(it) } )
  46. Error handling bits. Flow dataFlow() .flatMapMerge { map(it) } .catch

    { report(it) } .subscribe( onNext = { crashHandle(it) } )
  47. Error handling bits. Flow dataFlow() .flatMapMerge { map(it) } .catch

    { report(it) } .collect { crashHandle(it) } We crash!
  48. Error handling bits. Flow dataFlow() .flatMapMerge { map(it) } .onEach

    { crashHandle(it) } .catch { report(it) } .collect()
  49. Error handling bits. Flow dataFlow() .flatMapMerge { map(it) } .onEach

    { crashHandle(it) } .catch { report(it) } .collect()
  50. Error handling bits. Flow dataFlow() .flatMapMerge { map(it) } .onEach

    { crashHandle(it) } .catch { report(it) } // <-- last in chain .collect() You're not in Kansas anymore. Pay attention! 10 11 11 https:/ /proandroiddev.com/kotlin-coroutine-job-hierarchy-finish-cancel-and-fail-2d3d42a768a9 10 https:/ /github.com/MartinDevi/Coroutines-Completion-Demo
  51. Kotlin Native. Reaktive ✦ Workers-based threading ✦ Native freeze for

    switching threads, i.e. almost always ✦ threadLocal operator to stop freezing ✦ In practice, not only immutable events, but also captured vars in callbacks
  52. Performance 9 Benchmark Mode Score Units RxJava1ObservablePlaysScrabble avgt 156.474 ms/op

    RxJava2ObservablePlaysScrabble avgt 90.505 ms/op RxJava2FlowablePlaysScrabble avgt 96.144 ms/op FlowPlaysScrabble avgt 84.520 ms/op ReaktivePlaysScrabble avgt 208.666 ms/op 9 https:/ /github.com/akarnokd/akarnokd-misc
  53. Performance 9 Benchmark Mode Score Units RxJava1ObservablePlaysScrabble avgt 156.474 ms/op

    RxJava2ObservablePlaysScrabble avgt 90.505 ms/op RxJava2FlowablePlaysScrabble avgt 96.144 ms/op FlowPlaysScrabble avgt 84.520 ms/op ReaktivePlaysScrabble avgt 208.666 ms/op 9 https:/ /github.com/akarnokd/akarnokd-misc
  54. Performance 9 Benchmark Mode Score Units RxJava1ObservablePlaysScrabble avgt 156.474 ms/op

    RxJava2ObservablePlaysScrabble avgt 90.505 ms/op RxJava2FlowablePlaysScrabble avgt 96.144 ms/op FlowPlaysScrabble avgt 84.520 ms/op ReaktivePlaysScrabble avgt 208.666 ms/op 9 https:/ /github.com/akarnokd/akarnokd-misc
  55. Performance 9 Benchmark Mode Score Units RxJava1ObservablePlaysScrabble avgt 156.474 ms/op

    RxJava2ObservablePlaysScrabble avgt 90.505 ms/op RxJava2FlowablePlaysScrabble avgt 96.144 ms/op FlowPlaysScrabble avgt 84.520 ms/op ReaktivePlaysScrabble avgt 208.666 ms/op 9 https:/ /github.com/akarnokd/akarnokd-misc
  56. Performance 9 Benchmark Mode Score Units RxJava1ObservablePlaysScrabble avgt 156.474 ms/op

    RxJava2ObservablePlaysScrabble avgt 90.505 ms/op RxJava2FlowablePlaysScrabble avgt 96.144 ms/op FlowPlaysScrabble avgt 84.520 ms/op FlowPlaysScrabbleFlatMap avgt 744.464 ms/op ReaktivePlaysScrabble avgt 208.666 ms/op 9 https:/ /github.com/akarnokd/akarnokd-misc
  57. Memory 9 Benchmark Mode Score Units RxJava1ObservablePlaysScrabble avgt 254.334 MB/op

    RxJava2ObservablePlaysScrabble avgt 123.316 MB/op RxJava2FlowablePlaysScrabble avgt 139.861 MB/op FlowPlaysScrabble avgt 247.987 MB/op FlowPlaysScrabbleFlatMap avgt 780.728 MB/op ReaktivePlaysScrabble avgt 351.192 MB/op 9 https:/ /github.com/akarnokd/akarnokd-misc
  58. Memory 9 Benchmark Mode Score Units RxJava1ObservablePlaysScrabble avgt 254.334 MB/op

    RxJava2ObservablePlaysScrabble avgt 123.316 MB/op RxJava2FlowablePlaysScrabble avgt 139.861 MB/op FlowPlaysScrabble avgt 247.987 MB/op FlowPlaysScrabbleFlatMap avgt 780.728 MB/op ReaktivePlaysScrabble avgt 351.192 MB/op 9 https:/ /github.com/akarnokd/akarnokd-misc
  59. Memory 9 Benchmark Mode Score Units RxJava1ObservablePlaysScrabble avgt 254.334 MB/op

    RxJava2ObservablePlaysScrabble avgt 123.316 MB/op RxJava2FlowablePlaysScrabble avgt 139.861 MB/op FlowPlaysScrabble avgt 247.987 MB/op FlowPlaysScrabbleFlatMap avgt 780.728 MB/op ReaktivePlaysScrabble avgt 351.192 MB/op 9 https:/ /github.com/akarnokd/akarnokd-misc
  60. Memory 9 Benchmark Mode Score Units RxJava1ObservablePlaysScrabble avgt 254.334 MB/op

    RxJava2ObservablePlaysScrabble avgt 123.316 MB/op RxJava2FlowablePlaysScrabble avgt 139.861 MB/op FlowPlaysScrabble avgt 247.987 MB/op FlowPlaysScrabbleFlatMap avgt 780.728 MB/op ReaktivePlaysScrabble avgt 351.192 MB/op 9 https:/ /github.com/akarnokd/akarnokd-misc
  61. Memory 9 Benchmark Mode Score Units RxJava1ObservablePlaysScrabble avgt 254.334 MB/op

    RxJava2ObservablePlaysScrabble avgt 123.316 MB/op RxJava2FlowablePlaysScrabble avgt 139.861 MB/op FlowPlaysScrabble avgt 247.987 MB/op FlowPlaysScrabbleFlatMap avgt 780.728 MB/op ReaktivePlaysScrabble avgt 351.192 MB/op 9 https:/ /github.com/akarnokd/akarnokd-misc
  62. Consclusion ✦ Flow ✦ Not yet ready for MPP. Native?

    ✦ Not all operators we are used to are available ✦ Official support from JetBrains & Google ✦ Reaktive ✦ Real-ish Native support ✦ 1.0.x, but not really production ready ✦ Native affects performance on other platforms (JVM) - Is being FIXED ✦ Very responsive maintainers "
  63. Links ✦ Reaktive repo github.com/badoo/Reaktive ✦ Flow docs kotlinlang.org/docs/reference/coroutines/flow.html ✦

    Reactive Scrabble Benchmark akarnokd.blogspot.com/2016/12/the- reactive-scrabble-benchmarks.html ✦ Job Hierarchy ambiguity proandroiddev.com/kotlin-coroutine-job- hierarchy-finish-cancel-and-fail-2d3d42a768a9 ✦ Sample code github.com/colriot/talk-rx-multiplatform ✦ Cover photo unsplash.com/photos/PqU63ujaIa4