$30 off During Our Annual Pro Sale. View Details »

Fully Reactive Apps - Extended

pakoito
October 25, 2016

Fully Reactive Apps - Extended

Extended Droidcon UK 2016 talk about reactive patterns on Android

SIMPLE VERSION
=============
https://speakerdeck.com/pakoito/fully-reactive-apps

PROJECT
=======
https://github.com/pakoito/FunctionalAndroidReference

pakoito

October 25, 2016
Tweet

More Decks by pakoito

Other Decks in Programming

Transcript

  1. ABSTRACT ▸ People were asking for an advanced “deep dive”

    talk to RxJava ▸ A collection of thoughts around architectural experiments that went both right and wrong ▸ Based on ideas from other platforms and architectures ▸ The knowledge gained from applying them on a live app with hundreds of thousands of Daily Active Users ▸ Not a complete or canonical implementation, but more like reference examples and tips to come back to ▸ Do not get lost in the code snippets now ▸ Longer SpeakerDeck, links, and recap at the end!
  2. MVP WITH PASSIVE VIEW - VIEW ▸ View creates and

    retains a presenter ▸ Bind and unbind presenter ▸ Destroy presenter reference just in case public class MyView implements MVPView { Presenter p = null; public MyView() { presenter = new Presenter(); } public void onViewCreated() { presenter.bind(this); } public void onDestroy() { presenter.unbind(); presenter = null; } … }
  3. MVP WITH PASSIVE VIEW - PRESENTER ▸ Presenter retains view

    ▸ Subscriptions are added to CompositeSubscription ▸ When lifecycle ends, view is destroyed and subscriptions cleared public class MyPresenter { View view = null; CompositeSubscription subs = new CompositeSubscription(); public void bind(View v) { view = v; Subscription case = view.listClicks() .flatMap(getNetworkRequest()) .subscribe(view.setResult()); subs.add(case); } public void unbind() { subs.clear(); view = null; } … }
  4. PRESENTER HANDLING ITS OWN LIFECYCLE ▸ Function receives all dependencies

    ▸ No fields are retained, everything is a parameter ▸ No return needed, just binds and handles itself void start(View v, Observable<MyLifecycle> lifecycle, Scheduler main) { bind(lifecycle, main, view.listClicks().flatMap(getInfoForPosition()), view.setDetails()); bind(lifecycle, main, view.endOfList().flatMap(getMoreElements()), view.setElements()); } Values come from class or static. No judgement, just make this function pure.
  5. BINDING ABSTRACTION - EXAMPLE void <T> bind( Observable<MyLifecycle> lifecycle, Scheduler

    mainThreadScheduler, Observable<T> state, Action1<T> viewAction) { lifecycle .filter(lc -> lc == MyLifecycle.BEGIN) .switchMap(state) .observeOn(mainThreadScheduler) .takeUntil(lifecycle .filter(lc -> lc == MyLifecycle.END)) .subscribe(viewAction) } https://github.com/pakoito/FunctionalAndroidReference/blob/master/liblogic/src/main/kotlin/com/pacoworks/ dereference/architecture/ui/ControllerBinder.kt switchMap because we’re changing from infinite to infinite. We don’t want duplications. TakeUntil after observeOn because of Square talk
  6. THE LIFECYCLE OBSERVABLE ▸ RxLifecycle - Focuses on generic Android

    lifecycle ▸ Rx-aware MVP libraries - Pre-built support ▸ Roll your own binding according to your app’s needs ▸ Views instead of Fragments? ▸ Legacy architecture?
  7. LIFECYCLE FLAVOURS - CUSTOM ▸ Easy and simple ▸ Fits

    exactly your needs ▸ Works with legacy code without disrupting BehaviorRelay<ActivityLifecycle> lifecycle = BehaviorRelay.create() @Override protected void onCreate(Bundle b) { if (b == null) { lifecycle.call(ENTER); } lifecycle.call(CREATE); } … @Override protected void onDestroy() { super.onDestroy(); lifecycle.call(DESTROY); if (isFinishing()) { lifecycle.call(EXIT); } } https://github.com/pakoito/FunctionalAndroidReference/blob/master/liblogic/src/main/kotlin/com/pacoworks/ dereference/architecture/reactive/buddies/ReactiveActivity.kt Doesn’t require inheriting from BaseActivity You can use a delegate class and compose
  8. LIFECYCLE FLAVOURS - VIEWS ▸ By /u/btilbrook-nextfaze ▸ Uses RxBinding.attachEvents()

    to control lifecycle Observable.fromCallable(() -> v.isAttachedToWindow()) .concatWith( RxView.attachEvents(v) .map(event -> event.kind() == Kind.ATTACH))) https://www.reddit.com/r/androiddev/comments/574suy/android_leak_pattern_subscriptions_in_views/d8pgywm
  9. USE CASE AS STATE UPDATES - BEFORE view.listClicks() .flatMap(element ->

    NetworkApi.requestInfo(element.id)) .subscribe(view.setDetails())
  10. USE CASE AS STATE UPDATES - AFTER BehaviorRelay<UserDetails> state =

    BehaviorRelay.create(); PublishRelay<Nothing> clicks = PublishRelay.create(); … state.flatMap(currentState -> clicks .first() .flatMap(element -> NetworkApi.requestInfo(element.id)) .map(changes -> currentState.applyDelta(changes) ).subscribe(state)
  11. USE CASE AS STATE UPDATES - DESCRIPTION BehaviorRelay<UserDetails> state =

    BehaviorRelay.create(); PublishRelay<Nothing> clicks = PublishRelay.create(); … state.flatMap(currentState -> clicks .first() .flatMap(element -> NetworkApi.requestInfo(element.id)) .map(changes -> currentState.applyDelta(changes) ).subscribe(state) 1. There exists a current state 2. For every value of current state 3. Wait for the first signal 4. Make any requests required to modify current data 5. Apply new data to old data 6. Set new data as current state 7. Wait for the first signal…
  12. BUT WHAT IS STATE? ▸ State is a non-terminating stream

    with an initial value, followed by a sequence of immutable values over time Every value is a snapshot of the state, and it can be trusted until the next value is received. State is processed sequentially
  13. BUT WHAT IS STATE? ▸ State is a non-terminating stream

    with an initial value, followed by a sequence of immutable values over time ▸ State has to be explicit and measurable data Every value is a snapshot of the state, and it can be trusted until the next value is received. State is processed sequentially
  14. BUT WHAT IS STATE? ▸ State is a non-terminating stream

    with an initial value, followed by a sequence of immutable values over time ▸ State has to be explicit and measurable data ▸ The state has to be external to the use case, and is passed as a dependency into it Every value is a snapshot of the state, and it can be trusted until the next value is received. State is processed sequentially
  15. BUT WHAT IS STATE? ▸ State is a non-terminating stream

    with an initial value, followed by a sequence of immutable values over time ▸ State has to be explicit and measurable data ▸ The state has to be external to the use case, and is passed as a dependency into it ▸ Can be implemented using a Behaviour subject, or a Serialised one if state has be thread-safe Every value is a snapshot of the state, and it can be trusted until the next value is received. State is processed sequentially
  16. WHERE DO OTHER SIGNALS COME FROM? ▸ Community is quite

    active: RxBinding, RxPaper, RxPaparazzo, RxFileObserver… https://github.com/zsoltk/RxAndroidLibs ▸ Build for your own components: ItemTouchHelper, Exoplayer, ViewsViewPager, RecyclerAdapter… ▸ And release them as open-source!
  17. USE CASE AS STATE UPDATES - ADVANTAGES BehaviorRelay<UserDetails> state =

    BehaviorRelay.create(); PublishRelay<Nothing> clicks = PublishRelay.create(); … state.flatMap(currentState -> clicks .first() .flatMap(element -> NetworkApi.requestInfo(element.id)) .map(changes -> currentState.applyDelta(changes) ).subscribe(state) ‣ Pure input-output cases
  18. PURE INPUT-OUTPUT - SIMPLE TESTS @Test void homeScreen_UserClickOnRotationScreen_MoveToRotationScreen() { /*

    Create the state with an initial value */ StubView view = new StubView(); Pair startState = Pair.with(createHome(), Direction.FORWARD); BehaviorRelay navigation = createStateHolder(startState); TestSubscriber testSubscriber = TestSubscriber.create<Pair<Screen, Direction>>(); navigation.value.subscribe(testSubscriber); /* Start the subscription */ subscribeHomeInteractor(view, navigation); /* Act on screen by forwarding a value */ Pair newState = Pair.with(createRotation(), Direction.FORWARD); view.screenClick.call(newState); /* Assert correct output of a sequence of values */ testSubscriber.assertValueCount(2); testSubscriber.assertValues(startState, newState); testSubscriber.assertNoTerminalEvent(); }
  19. USE CASE AS STATE UPDATES - ADVANTAGES BehaviorRelay<UserDetails> state =

    BehaviorRelay.create(); PublishRelay<Nothing> clicks = PublishRelay.create(); … state.flatMap(currentState -> clicks .first() .flatMap(element -> NetworkApi.requestInfo(element.id)) .map(changes -> currentState.applyDelta(changes) ).subscribe(state) ‣ Pure input-output cases ‣ No strong references to the view
  20. AVOID CYCLICAL DEPENDENCIES TO VIEWS USING PROXIES Observable viewClicks =

    null; void onCreate() { viewClicks = RxBinding.clicks(view); } Observable<Element> viewClicks() { return viewClicks; }
  21. AVOID CYCLICAL DEPENDENCIES TO VIEWS USING PROXIES Observable viewClicks =

    null; void onCreate() { /** * Create an observable which emits on {@code view} click events. * The emitted value is unspecified and should only be used as * notification. * * The created observable keeps a strong reference to {@code view}. * Unsubscribe to free this reference. * * The created observable uses {@link View#setOnClickListener} to * observe clicks. * Only one observable can be used for a view at a time. */ viewClicks = RxView.clicks(view); } Observable<Element> viewClicks() { return viewClicks; } VIEW RXVIEW USE CASE
  22. AVOID CYCLICAL DEPENDENCIES TO VIEWS USING PROXIES PublishRelay<Void> clicksRelay =

    PublishRelay.create(); void onCreate() { RxView.clicks(view) .subscribe(clicksRelay); } Observable<Element> viewClicks() { return clicksRelay.asObservable(); } VIEW RXVIEW USE CASE PUBLISH
  23. USE CASE AS STATE UPDATES - ADVANTAGES BehaviorRelay<UserDetails> state =

    BehaviorRelay.create(); PublishRelay<Nothing> clicks = PublishRelay.create(); … state.flatMap(currentState -> clicks .first() .flatMap(element -> NetworkApi.requestInfo(element.id)) .map(changes -> currentState.applyDelta(changes) ).subscribe(state) ‣ Pure input-output cases ‣ No strong references to the view ‣ No main thread requirement
  24. BINDING ABSTRACTION - REMINDER void <T> bind( Observable<MyLifecycle> lifecycle, Scheduler

    mainThreadScheduler, Observable<T> state, Action1<T> viewAction) { lifecycle .filter(lc -> lc == MyLifecycle.BEGIN) .switchMap(state) .observeOn(mainThreadScheduler) .takeUntil(lifecycle .filter(lc -> lc == MyLifecycle.END)) .subscribe(viewAction) }
  25. USE CASE AS STATE UPDATES - ADVANTAGES BehaviorRelay<UserDetails> state =

    BehaviorRelay.create(); PublishRelay<Nothing> clicks = PublishRelay.create(); … state.flatMap(currentState -> clicks .first() .flatMap(element -> NetworkApi.requestInfo(element.id)) .map(changes -> currentState.applyDelta(changes) ).subscribe(state) ‣ Pure input-output cases ‣ No strong references to the view ‣ No main thread requirement ‣ No out-of-domain class dependencies?
  26. DECOUPLING FROM SERVICES - FUNCTION INJECTION BehaviorRelay<UserDetails> state = BehaviorRelay.create();

    PublishRelay<Nothing> clicks = PublishRelay.create(); /* Real implementation for integration */ Func1<String, Observable<Info>> networkRequest = { id -> NetworkService.requestInfo(id); } /* Mock implementation for errors */ Func1<String, Observable<Info>> failRequest = { id -> Observable.error(); } … state.flatMap(currentState -> clicks .first() .flatMap(element -> networkRequest.call(element.id)) .map(changes -> currentState.applyDelta(changes) ).subscribe(state) ‣ Replace dependencies on interfaces and statics with just the functions required for each use case Easy replacement for server responses, sensors, or events like lifecycle without needing a framework for testing Implementation is still coupled and checked by the compiler and tests
  27. USE CASE AS STATE UPDATES - ADVANTAGES BehaviorRelay<UserDetails> state =

    BehaviorRelay.create(); PublishRelay<Nothing> clicks = PublishRelay.create(); Func1<String, Observable<Info>> networkRequest = { id -> NetworkService.requestInfo(id); } … state.flatMap(currentState -> clicks .first() .flatMap(element -> networkRequest.call(element.id)) .map(changes -> currentState.applyDelta(changes) ).subscribe(state) ‣ Pure input-output cases ‣ No strong references to the view ‣ No main thread requirement ‣ No out-of-domain class dependencies ‣ Nested flatMap, switchMap, or concatMap
  28. BOILERPLATE REMOVAL - COMPREHENSIONS BehaviorRelay<UserDetails> state = BehaviorRelay.create(); PublishRelay<Nothing> clicks

    = PublishRelay.create(); Func1<String, Observable<Info>> networkRequest = { id -> NetworkService.requestInfo(id); } … doFM( { () -> state }, { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .map(changes -> currentState.applyDelta(changes) }) ).subscribe(state) ‣ Abstraction over nested flatMap, switchMap, and concatMap ‣ Improves readability ‣ Helps noticing subtle errors like using toList() on non-finite observables https://github.com/pakoito/ RxComprehensions doFM(), doSM(), and doCM() Every nested depth receives the result of all Observables above it Uses a function returning an Observable for each depth of nesting: Func0, Func1, Func2… Structurally, it splits use cases into reusable functions with N == depth parameters and 1 output
  29. USE CASE AS STATE UPDATES - ADVANTAGES BehaviorRelay<UserDetails> state =

    BehaviorRelay.create(); PublishRelay<Nothing> clicks = PublishRelay.create(); Func1<String, Observable<Info>> networkRequest = { id -> NetworkService.requestInfo(id); } … doFM( { () -> state }, { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .map(changes -> currentState.applyDelta(changes) }) ).subscribe(state) ‣ Pure input-output cases ‣ No strong references to the view ‣ No main thread requirement ‣ No out-of-domain class dependencies ‣ Nested flatMap, switchMap, or concatMap ‣ Most use cases follow the same pattern
  30. USE CASE ANALYSIS doFM( /* A non-terminating Observable */ {

    () -> state }, /* A terminating Observable */ { current -> clicks.first() }, /* Maybe more terminating Observables */ { current, clicks -> networkRequest.call(element.id) .map(changes -> currentState.delta(changes) }) ).subscribe(state) Use cases follow the same pattern: ‣ A non-terminating Observable ‣ flatMap/switchMap/concatMap ‣ A terminating Observable ‣ flatMap/switchMap/concatMap ‣ A terminating Observable ‣ … repeat 0 - N times ‣ Subscription to the new state No Exceptions! * *Some exceptions
  31. WHERE DO I SAVE MY STATE? No silver bullet answer

    yet: ‣ Use onRetainCustomNonConfigurationInstance() if you don’t care about losing state if your app is killed by the system ‣ Store in a static and handle its lifecycle manually ‣ Store in the Application class ‣ Save each state individually on the bundle ‣ Use a Dependency Injection framework ‣ Make all state go through persistence We’re open to new ideas!
  32. DRAG AND DROP void handleDragAndDrop( Observable<Pair<Int, Int>> dragAndDropObservable, BehaviorRelay<List<String>> elements)

    { doFM( { dragAndDropObservable }, { swap -> elements .first() .map { it.toMutableList() .apply { Collections.swap(this, swap.value0, swap.value1) } .toList() } } ).subscribe(elements) } https://github.com/pakoito/FunctionalAndroidReference/blob/master/liblogic/src/main/kotlin/com/pacoworks/ dereference/features/draganddrop/DragAndDropExampleInteractor.kt
  33. SELECT ON A LIST void handleSelected( Observable<Pair<Int, String>> listClicks, StateHolder<Set<String>>

    selected) { doSM( { selected }, { listClicks.map { it.value1 }.first() }, { selected, item -> Observable.just( if (selected.contains(item)) { selected.minus(item) } else { selected.plus(item) } ) } ).subscribe(selected) } https://github.com/pakoito/FunctionalAndroidReference/blob/master/liblogic/src/main/kotlin/com/pacoworks/ dereference/features/draganddrop/DragAndDropExampleInteractor.kt
  34. DEFECTS VS ERRORS ▸ A defect is an unexpected problem

    in the system. Can be caused by framework problems, environment, mistakes in the code… ▸ An error is an expected problem that you provision for. Can be caused by business and framework requirements. ▸ You want defects to be reported, and errors to be handled. You have to model errors into your domain.
  35. DEFECTS IN USE CASES PublishSubject<Nothing> clicks = PublishSubject.create(); doFM( {

    () -> state }, { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .map(changes -> currentState.delta(changes) }) ).subscribe(state, toastError) void onResume() { clicks.onError(new RuntimeException()); } ‣ WTF
  36. DEFECTS IN USE CASES PublishSubject<Nothing> clicks = PublishSubject.create(); doFM( {

    () -> state }, { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .map(changes -> currentState.delta(changes) }) ).subscribe(state, toastError) void onResume() { clicks.onError(new RuntimeException()); } ‣ WTF ‣ This shouldn’t happen
  37. DEFECTS IN USE CASES PublishSubject<Nothing> clicks = PublishSubject.create(); doFM( {

    () -> state }, { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .map(changes -> currentState.delta(changes) }) ).subscribe(state, toastError) void onResume() { clicks.onError(new RuntimeException()); } ‣ WTF ‣ This shouldn’t happen ‣ I wanted a stack trace but all I got is a lousy Toast
  38. DEFECTS IN USE CASES PublishSubject<Nothing> clicks = PublishSubject.create(); doFM( {

    () -> state }, { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .map(changes -> currentState.delta(changes) }) ).subscribe(state, toastError) void onResume() { clicks.onError(new RuntimeException()); } ‣ WTF ‣ This shouldn’t happen ‣ I wanted a stack trace but all I got is a lousy Toast ‣ Non-terminating Observable is unsubscribed
  39. DEFECTS IN USE CASES PublishSubject<Nothing> clicks = PublishSubject.create(); doFM( {

    () -> state }, { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .map(changes -> currentState.delta(changes) }) ).subscribe(state, toastError) void onResume() { clicks.onError(new RuntimeException()); } ‣ WTF ‣ This shouldn’t happen ‣ I wanted a stack trace but all I got is a lousy Toast ‣ Non-terminating Observable is unsubscribed ‣ The app isn’t working correctly, and the user doesn’t know why
  40. LETTING IT CRASH GRACEFULLY Not applicable for every Observable subscription,

    or every architecture. Do it responsibly! ▸ Helps catching errors in development stages rather than in live ▸ Doesn’t leave the app in an inconsistent state for the user. An unresponsive app might be worse than a crash ▸ No silent longstanding failures becoming bad user reviews ▸ Pick up the crash log and fix it for next release!
  41. SUBSCRIBING FOR DEFECTS ▸ Subscribe your use cases with subscribe(Action1)

    ▸ Debug with subscribe(Action1, Action1) ▸ Unlike Observer, Actions compose naturally. You can build your own tools around them: logging, threading, tracing… ▸ Subjects are not Action1. Use RxRelay instead ▸ All samples in this presentation used Relays! https://github.com/JakeWharton/RxRelay
  42. ERRORS IN USE CASES doFM( { () -> state },

    { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .map(changes -> currentState.delta(changes) }) ).subscribe(state) ‣ Exist inside the system ‣ Every IO operation ‣ Every network call ‣ Every database query ‣ Every interaction with the Android framework ‣ Every reactive library ‣ Everything outside your use case island that you don’t know about
  43. HANDLING ERRORS - LOG AND SWALLOW doFM( { () ->

    state }, { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .doOnError(logError()) .onErrorResumeNext( Observable.empty()) .map(changes -> current.delta(changes) }) ).subscribe(state) ‣ Very basic ‣ Removes the problem but not the cause ‣ You need to forward your logs to a report system ‣ Not recommended
  44. HANDLING ERRORS - RETRY doFM( { () -> state },

    { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .map(changes -> current.delta(changes) }) ).retry(/* */) .subscribe(state) ‣ Delays the problem instead of solving it ‣ Problems with inconsistency and persistence
  45. HANDLING ERRORS - MODELING ERRORS AS A STATE BehaviorRelay<Transaction> state

    = BehaviorRelay.create(Transaction.Idle); doFM( { () -> state }, { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .onErrorResumeNext( Observable.just( Transaction.Fail( “User Offline”))) .map(changes -> Transaction.Success( current.delta(changes)) }) ).subscribe(state) ‣ Your single state becomes a state machine ‣ The states already exist, we’re just making them explicit as data! Testing done by asserting for every possible input/output of the state machine All states have to be handled by a use case
  46. STATE MACHINES IN KOTLIN - SEALED CLASSES class Transaction {

    object Idle: Transaction() class Loading(val percent: Int): Transaction() class Fail(val cause: String): Transaction() class Success(val info: Info): Transaction() } val state: Transaction = Loading(10) when (state) { is Idle -> /* */ is Loading -> /* */ is Fail -> /* */ is Success -> /* */ } ‣ Sealed classes are closed inheritances ‣ Like enums but every element can be a different class ‣ Your code defines the transitions between states ‣ Matched with function when() ‣ The compiler checks that you always handle all possible cases http://tinyurl.com/KMobi16
  47. STATE MACHINES IN JAVA - UNIONS public interface Idle {}

    public class Loading { public final int percent; … } public class Fail { public final String cause; … } public class Success { public final Info info; … } Union4.Factory<Idle, Loading, Fail, Success> FACTORY = GenericUnions.quartetFactory(); Union4<Idle, Loading, Fail, Success> state = FACTORY.second(new Loading(50)); state.join({ idle -> /* */ }, { loading -> /* */ }, { fail -> /* */ }, { success -> /* */ }) ‣ Generic encoding of unions, similar to Scala ‣ Single, Optional, Either, Union3-9 ‣ Matched with methods join() and continued() ‣ More verbose than Kotlin’s ‣ The compiler still checks that you always handle all possible cases https://github.com/pakoito/ RxSealedUnions
  48. USING UNIONS WHEN BINDING STATE BehaviorRelay<Transaction> state = BehaviorRelay.create(Transaction.Idle); void

    <T> bind( Observable<MyLifecycle> lifecycle, Scheduler mainThreadScheduler, Observable<T> state, Action1<T> viewAction) { /* */ } bind( lifecycle, AndroidSchedulers.mainThread(), state, transaction -> when (transaction) { is Loading -> view.setLoading(transaction.percent) is Failure -> view.showError(transaction.reason) is Idle -> view.setWaiting() is Success -> view.showInfo(transaction.showInfo) })
  49. PAGINATION void handlePagination( Observable<Void>: endOfPage, PaginationExampleState state, Func1<Int, Observable<List<String>> service,

    BehaviorRelay<List<String>> elements, BehaviorRelay<Integer> pages) { doSM( { elements }, { endOfPage.first() }, { elements, click -> pages.first() }, { elements, click, page -> service.invoke(page) .map { elements.plus(it) } /* Update pages inline after a success */ .doOnNext { pages.call(page + 1) } } ).subscribe(elements) } https://github.com/pakoito/FunctionalAndroidReference/blob/master/liblogic/src/main/kotlin/com/pacoworks/ dereference/features/pagination/PaginationExampleInteractor.kt
  50. NAVIGATE TO SCREEN - WITH ITS OWN LIFECYCLE void pushScreen(

    Observable<Lifecycle>: lifecycle, NavigatorView navigatorView, BehaviorRelay<Pair<Screen, Direction>> navigation, Scheduler mainThreadScheduler) { navigation /* Skip the 1st value to avoid re-pushing the current value after rotation */ .skip(1) .filter { it.value1 == Direction.FORWARD } .map { it.value0 } .observeOn(mainThreadScheduler) .takeUntil(lifecycle .filter { it == ActivityLifecycle.Destroy }) .subscribe { navigatorView.goTo(it) } } https://github.com/pakoito/FunctionalAndroidReference/blob/master/liblogic/src/main/kotlin/com/pacoworks/ dereference/features/global/NavigationInteractor.kt
  51. RECAP ▸ Where possible, handle your lifecycle internally ▸ Split

    your problems into small use cases ▸ Make each use case a data input-output problem ▸ Model your solutions with data, not code ▸ The lifecycle and errors are data too! ▸ State has to be explicit and debuggable at any point ▸ Handle both defects and errors responsibly
  52. LINKS pacoworks.com/ @fe_hudl github.com/pakoito tinyurl.com/RxDroidcon16 This presentation will be soon

    available on the droidcon London website at the following link: https://uk.droidcon.com/#skillscasts Sample app https://github.com/pakoito/ FunctionalAndroidReference Modeling state with Sealed Classes tinyurl.com/KMobi16 Rx libraries https://github.com/zsoltk/RxAndroidLibs RxRelay https://github.com/JakeWharton/RxRelay RxComprehensions https://github.com/pakoito/ RxComprehensions RxSealedUnions https://github.com/pakoito/RxSealedUnions More functional goodies https://github.com/pakoito/FunctionalRx