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

Fully Reactive Apps

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

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