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

Fully Reactive Apps

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for pakoito pakoito
October 27, 2016

Fully Reactive Apps

Original Droidcon UK 2016 talk about reactive patterns on Android

EXTENDED VERSION
================
This version was too meaty to present in 45', but it includes more concepts and notes

https://speakerdeck.com/pakoito/fully-reactive-apps-extended

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

Avatar for pakoito

pakoito

October 27, 2016
Tweet

More Decks by pakoito

Other Decks in Programming

Transcript

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

    talk on reactive architectures ▸ Do not get lost in the code snippets now ▸ Extended slides, links, and takeaways recap at the end 2
  2. ABSTRACT ▸ The results of evolving MVP and MVVM further

    than their canonical implementations by taking a functional approach ▸ Taking ideas from architectures on other platforms ▸ The knowledge gained from applying them on a live app with hundreds of thousands of Daily Active Users ▸ Applying in context some concepts and open source libraries that I have presented over the past year https://github.com/pakoito/FunctionalRx www.pacoworks.com 3
  3. 4

  4. MVP WITH PASSIVE VIEW - VIEW ▸ Single lifecycle ▸

    Rotation not handled class MyView implements MViewP { public void onViewCreated() { presenter.bind(this); } public void onDestroy() { presenter.unbind(); presenter = null; } … } 6
  5. PRESENTER HANDLING ITS OWN LIFECYCLE ▸ Function receives all dependencies

    ▸ No return needed, just binds and handles itself ▸ Any lifecycle possibilities, including no lifecycle! void start(View v, Scheduler main, Observable<MyLifecycle> lifecycle) { bind(lifecycle, main, view.listClicks().flatMap(getInfoForPosition()), view.setDetails()); handleEndOfList( view.endOfList(), view.setElements()); } Values come from class or static. No judgement, just make this function pure. 8
  6. 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) }z 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 9
  7. 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) }z 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 10
  8. 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? 11
  9. 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); }z lifecycle.call(CREATE); }z … @Override protected void onDestroy() { super.onDestroy(); lifecycle.call(DESTROY); if (isFinishing()) { lifecycle.call(EXIT); }z }z 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 12
  10. 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); }z lifecycle.call(CREATE); }z … @Override protected void onDestroy() { super.onDestroy(); lifecycle.call(DESTROY); if (isFinishing()) { lifecycle.call(EXIT); }z }z 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 13
  11. MVP WITH PASSIVE VIEW - PRESENTER ▸ Strong reference to

    view ▸ Presenter has to know about the main thread ▸ State is not trivial to debug ▸ Difficult rotation ▸ Error handling is a one-off public class MyPresenter { public void bind(View v) { view = v; Subscription case = view.listClicks() .flatMap(toNetworkRequest()) .observeOn(AndroidSchedulers.main()) .subscribe( view.setResult(), view.displayError()); subscriptions.add(case); } public void unbind() { subscriptions.clear(); view = null; } } 15
  12. USE CASE AS STATE UPDATES - BEFORE listClicks .flatMap(element ->

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

    BehaviorRelay.create(); …z state.flatMap( currentState -> listClicks .first() .flatMap(element -> NetworkApi.requestInfo(element.id)) .map(changes -> currentState.applyDelta(changes) ).subscribe(state) 18
  14. USE CASE AS STATE UPDATES - AFTER BehaviorRelay<UserDetails> state =

    BehaviorRelay.create(); …z state.flatMap( currentState -> listClicks .first() .flatMap(element -> NetworkApi.requestInfo(element.id)) .map(changes -> currentState.applyDelta(changes) ).subscribe(state) 19
  15. 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 20
  16. 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 21
  17. BUT WHAT IS STATE? ▸ State is a non-terminating stream

    with an initial value, followed by a sequence of immutable values over time ▸ It has to be explicit and measurable data ▸ It also 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 22
  18. BUT WHAT IS STATE? ▸ State is a non-terminating stream

    with an initial value, followed by a sequence of immutable values over time ▸ It has to be explicit and measurable data ▸ It also has to be external to the use case, and is passed as a dependency into it ▸ State can be implemented using a Behaviour subject, or a Serialised one if it needs to 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 23
  19. WHERE DO OTHER SIGNALS COME FROM? ▸ Existing solutions by

    the community: RxBinding, RxPaper, RxPaparazzo, RxFileObserver… https://github.com/zsoltk/RxAndroidLibs ▸ Build your own components: ItemTouchHelper, Exoplayer, ViewsViewPager, RecyclerAdapter… 24 Release your own components as open-source!
  20. 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(); }z 26
  21. 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(); }z 27
  22. 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(); }z 28
  23. 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(); }z 29
  24. USE CASE AS STATE UPDATES - ADVANTAGES ‣ Pure input-output

    cases ‣ No strong references to the view 30
  25. AVOID CYCLICAL DEPENDENCIES TO VIEWS USING PROXIES Observable<Void> viewClicks =

    null; void onCreate() { viewClicks = RxView.clicks(view); } Observable<Element> viewClicks() { return viewClicks; } VIEW RXVIEW USE CASE 31
  26. 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 32
  27. USE CASE AS STATE UPDATES - ADVANTAGES ‣ Pure input-output

    cases ‣ No strong references to the view ‣ No main thread requirement 33
  28. 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) } 34
  29. USE CASE AS STATE UPDATES - ADVANTAGES ‣ Pure input-output

    cases ‣ No strong references to the view ‣ No main thread requirement ‣ Most use cases follow the same pattern 35
  30. USE CASE ANALYSIS /* A non-terminating Observable */ state.flatMap(currentState ->

    /* A terminating Observable */ clicks.first() .flatMap(element -> /* Maybe more terminating Observables */ networkRequest.call(element.id)) .map(changes -> currentState.applyDelta(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 36
  31. USE CASE ANALYSIS /* A non-terminating Observable */ state.flatMap(currentState ->

    /* A terminating Observable */ clicks.first() .flatMap(element -> /* Maybe more terminating Observables */ networkRequest.call(element.id) .map(changes -> currentState.applyDelta(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 37
  32. BOILERPLATE REMOVAL - COMPREHENSIONS doFM( /* A non-terminating Observable */

    { () -> state }, /* A terminating Observable */ { current -> clicks.first() }, /* Other terminating Observables */ { current, clicks -> networkRequest.call(element.id) .map(changes -> currentState .applyDelta(changes) }) ).subscribe(state) ‣ Replaces 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 38
  33. BOILERPLATE REMOVAL - COMPREHENSIONS doFM( /* A non-terminating Observable */

    { () -> state }, /* A terminating Observable */ { current -> clicks.first() }, /* Other terminating Observables */ { current, clicks -> networkRequest.call(element.id) .map(changes -> currentState .applyDelta(changes) }) ).subscribe(state) ‣ Replaces 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 39
  34. 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! 40
  35. SELECT ON A LIST void handleSelected( Observable<Pair<Int, Value>> listClicks, BehaviorRelay<Set<String>>

    selected) { doSM( { selected }, { listClicks.map { it.second }.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 42
  36. DRAG AND DROP void handleDragAndDrop( Observable<Pair<Int, Int>> dragAndDropObservable, BehaviorRelay<List<Value>> 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 43
  37. PAGINATION void handlePagination( Observable<Void>: endOfPage, Func1<Int, Observable<List<Value>> service, BehaviorRelay<List<Value>> elements,

    BehaviorRelay<Integer> pages) { doSM( { elements }, { elements -> endOfPage.first() }, { elements, click -> pages.first() }, { elements, click, page -> service.invoke(page) .map { elements.plus(it) } .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 44
  38. DEFECTS VS ERRORS ▸ A defect is an unexpected problem

    in the environment ▸ An error is an expected problem that you provision for 48
  39. DEFECTS VS ERRORS ▸ A defect is an unexpected problem

    in the environment ▸ An error is an expected problem that you provision for ▸ You want defects to be reported, and errors to be handled 49
  40. DEFECTS VS ERRORS ▸ A defect is an unexpected problem

    in the environment ▸ An error is an expected problem that you provision for ▸ You want defects to be reported, and errors to be handled ▸ To handle errors, you have to model them into your domain 50
  41. 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 51
  42. 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 52
  43. 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 53
  44. 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 54
  45. 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 55
  46. LETTING IT CRASH GRACEFULLY ▸ 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! Not applicable for every Observable subscription, or every architecture. Do it responsibly! 57
  47. 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 58
  48. 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 59
  49. 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 60
  50. 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 61
  51. 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) .map(changes -> Transaction.Success( current.delta(changes))) .onErrorResumeNext( Observable.just( Transaction.Fail(“User Offline”))) ).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 62
  52. 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) .map(changes -> Transaction.Success( current.delta(changes))) .onErrorResumeNext( Observable.just( Transaction.Fail(“User Offline”))) ).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 63
  53. 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() }z val state: Transaction = Loading(10) when (state) { is Idle -> view.showIdle() is Loading -> view.spinner(state.percentage) is Fail -> view.toastError(state.cause) is Success -> view.display(state.info) }z ‣ 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 64
  54. 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() }z val state: Transaction = Loading(10) when (state) { is Idle -> view.showIdle() is Loading -> view.spinner(state.percentage) is Fail -> view.toastError(state.cause) is Success -> view.display(state.info) }z ‣ 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 65
  55. 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.continued( { idle -> view.showIdle() }, { loading -> view.spinner(loading.percent) }, { fail -> view.toastError(fail.cause) }, { success -> view.display(success.info) }) ‣ 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 66
  56. 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.continued( { idle -> view.showIdle() }, { loading -> view.spinner(loading.percent) }, { fail -> view.toastError(fail.cause) }, { success -> view.display(success.info) }) ‣ 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 67
  57. TRANSACTION FROM LOADING TO SUCCESS/FAIL void handleLoad( transaction: BehaviorRelay<Transaction>, services:

    TransactionRequest) { transaction.ofType(Loading.class) .switchMap { services.call(it.user.name) } .subscribe(transaction) }z https://github.com/pakoito/FunctionalAndroidReference/blob/master/liblogic/src/main/kotlin/com/pacoworks/ dereference/features/rotation/RotationInteractor.kt 69
  58. TRANSACTION FROM LOADING TO SUCCESS/FAIL void handleLoad( transaction: BehaviorRelay<Transaction>, services:

    TransactionRequest) { transaction.ofType(Loading.class) .switchMap { services.call(it.user.name) } .subscribe(transaction) }z https://github.com/pakoito/FunctionalAndroidReference/blob/master/liblogic/src/main/kotlin/com/pacoworks/ dereference/features/rotation/RotationInteractor.kt 70
  59. TRANSACTION FROM FAIL TO RELOAD void handleRetryAfterFailure( user: BehaviorRelay<UserInput>, transaction:

    BehaviorRelay<Transaction> countdown: Int) { transaction .ofType(Failure.class) .flatMap { Observable.interval(0, 1, TimeUnit.SECONDS) .map { WaitingForRetry(countdown - it - 1) } .startWith(WaitingForRetry(countdown)) .takeUntil { it.seconds <= 0 } .concatWith(Observable.just(Loading(0)) }.subscribe(transaction) }z https://github.com/pakoito/FunctionalAndroidReference/blob/master/liblogic/src/main/kotlin/com/pacoworks/ dereference/features/rotation/RotationInteractor.kt 71
  60. TRANSACTION FROM FAIL TO RELOAD void handleRetryAfterFailure( user: BehaviorRelay<UserInput>, transaction:

    BehaviorRelay<Transaction> countdown: Int) { transaction .ofType(Failure.class) .flatMap { Observable.interval(0, 1, TimeUnit.SECONDS) .map { WaitingForRetry(countdown - it - 1) } .startWith(WaitingForRetry(countdown)) .takeUntil { it.seconds <= 0 } .concatWith(Observable.just(Loading(0)) }.subscribe(transaction) }z https://github.com/pakoito/FunctionalAndroidReference/blob/master/liblogic/src/main/kotlin/com/pacoworks/ dereference/features/rotation/RotationInteractor.kt 72
  61. RECAP ▸ Where possible, handle your lifecycle internally ▸ State

    has to be explicit and debuggable at any point 74
  62. RECAP ▸ Where possible, handle your lifecycle internally ▸ State

    has to be explicit and debuggable at any point ▸ Split your problems into small use cases 75
  63. RECAP ▸ Where possible, handle your lifecycle internally ▸ State

    has to be explicit and debuggable at any point ▸ Split your problems into small use cases ▸ Make each use case an input-output solution 76
  64. RECAP ▸ Where possible, handle your lifecycle internally ▸ State

    has to be explicit and debuggable at any point ▸ Split your problems into small use cases ▸ Make each use case an input-output solution ▸ Model your solutions with data, not code 77
  65. RECAP ▸ Where possible, handle your lifecycle internally ▸ State

    has to be explicit and debuggable at any point ▸ Split your problems into small use cases ▸ Make each use case an input-output solution ▸ Model your solutions with data, not code ▸ Handle both defects and errors responsibly 78
  66. LINKS pacoworks.com/ @fe_hudl github.com/pakoito Slides: tinyurl.com/RxDroidcon16 Extended Slides: tinyurl.com/RxDroidcon16Ext 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 79