Slide 1

Slide 1 text

FULLY REACTIVE APPS PACO ESTEVEZ 1

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

4

Slide 5

Slide 5 text

LIFECYCLE 5

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

WHAT IF LIFECYCLE WAS DATA PASSED IN AN OBSERVABLE? 7

Slide 8

Slide 8 text

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 lifecycle) { bind(lifecycle, main, view.listClicks().flatMap(getInfoForPosition()), view.setDetails()); handlePagination(view.endOfList(), view.setElements()); } Values come from class or static. No judgement, just make this function pure. 8

Slide 9

Slide 9 text

BINDING ABSTRACTION - EXAMPLE void bind(Observable lifecycle, Scheduler mainThreadScheduler, Observable state, Action1 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

Slide 10

Slide 10 text

BINDING ABSTRACTION - EXAMPLE void bind(Observable lifecycle, Scheduler mainThreadScheduler, Observable state, Action1 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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

LIFECYCLE FLAVOURS - CUSTOM ▸ Easy and simple ▸ Fits exactly your needs ▸ Works with legacy code without disrupting BehaviorRelay lifecycle = BehaviorRelay.create() protected void onCreate(Bundle b) { if (b == null) { lifecycle.call(ENTER); }z lifecycle.call(CREATE); }z protected void 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

Slide 13

Slide 13 text

LIFECYCLE FLAVOURS - CUSTOM ▸ Easy and simple ▸ Fits exactly your needs ▸ Works with legacy code without disrupting BehaviorRelay lifecycle = BehaviorRelay.create() protected void onCreate(Bundle b) { if (b == null) { lifecycle.call(ENTER); }z lifecycle.call(CREATE); }z protected void 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

Slide 14

Slide 14 text

USE CASES 14

Slide 15

Slide 15 text

MVP WITH PASSIVE VIEW - PRESENTER ▸ Strong reference to view ▸ Presenter has to know about the main thread ▸ Chains not trivial to debug ▸ Difficult rotation ▸ Error handling is a one-off public void bind(View v) { mView = v; Subscription case = view.listClicks() .flatMap(toNetworkRequest()) .observeOn(AndroidSchedulers.main()) .subscribe(view.setResult(), view.displayError()); subscriptions.add(case); } public void unbind() { subscriptions.clear(); mView = null; } 15

Slide 16

Slide 16 text

WHAT IF EVERY PROBLEM WAS MODELED AS JUST UPDATES TO A VALUE? 16

Slide 17

Slide 17 text

USE CASE AS STATE UPDATES - BEFORE listClicks .flatMap(element -> NetworkApi.requestInfo(element.id)) .subscribe(view.setDetails()) 17

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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!

Slide 25

Slide 25 text

USE CASE AS STATE UPDATES - ADVANTAGES ‣ Pure input-output cases 25

Slide 26

Slide 26 text

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>(); 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 ‣ Create initial state ‣ Create view and service stubs

Slide 27

Slide 27 text

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>(); 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 ‣ Start subscription

Slide 28

Slide 28 text

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>(); 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 ‣ Act on stub view

Slide 29

Slide 29 text

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>(); 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 ‣ Assert values seen

Slide 30

Slide 30 text

USE CASE AS STATE UPDATES - ADVANTAGES ‣ Pure input-output cases ‣ No strong references to the view 30

Slide 31

Slide 31 text

AVOID CYCLICAL DEPENDENCIES TO VIEWS USING PROXIES Observable viewClicks = null; void onCreate() { viewClicks = RxView.clicks(view); } Observable viewClicks() { return viewClicks; } VIEW RXVIEW USE CASE 31

Slide 32

Slide 32 text

AVOID CYCLICAL DEPENDENCIES TO VIEWS USING PROXIES PublishRelay clicksRelay = PublishRelay.create(); void onCreate() { RxView.clicks(view) .subscribe(clicksRelay); } Observable viewClicks() { return clicksRelay.asObservable(); } VIEW RXVIEW USE CASE PUBLISH 32

Slide 33

Slide 33 text

USE CASE AS STATE UPDATES - ADVANTAGES ‣ Pure input-output cases ‣ No strong references to the view ‣ No main thread requirement 33

Slide 34

Slide 34 text

BINDING ABSTRACTION - REMINDER void bind( Observable lifecycle, Scheduler mainThreadScheduler, Observable state, Action1 viewAction) { lifecycle .filter(lc -> lc == MyLifecycle.BEGIN) .switchMap(state) .observeOn(mainThreadScheduler) .takeUntil(lifecycle .filter(lc -> lc == MyLifecycle.END)) .subscribe(viewAction) } 34

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

EXAMPLES PT1 41

Slide 42

Slide 42 text

SELECT ON A LIST void handleSelected(Observable> listClicks, BehaviorRelay> selected) { doSM( { selected }, { listClicks.map { pair -> pair.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

Slide 43

Slide 43 text

DRAG AND DROP void handleDragAndDrop( Observable> dragNDropObs, BehaviorRelay> elements) { doFM( { dragNDropObs }, { swap -> elements.first() .map { list -> Collections.swap(list, swap.first, swap.second) } }).subscribe(elements) } https://github.com/pakoito/FunctionalAndroidReference/blob/master/liblogic/src/main/kotlin/com/pacoworks/dereference/features/draganddrop/ DragAndDropExampleInteractor.kt 43

Slide 44

Slide 44 text

PAGINATION void handlePagination(Observable: nextPage, Func1> service, BehaviorRelay> elements, BehaviorRelay pages) { doSM( { elements }, { elements -> nextPage.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

Slide 45

Slide 45 text

MODELING STATE 45

Slide 46

Slide 46 text

WHAT IF ERRORS WERE NOT ERRORS BUT ANOTHER STATE? 46

Slide 47

Slide 47 text

DEFECTS VS ERRORS ▸ A defect is an unexpected problem in the environment 47

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

DEFECTS IN USE CASES PublishSubject clicks = /* */; clicks.subscribe(state, toastError); void onResume() { clicks.onError(new RuntimeException()); } ‣ WTF 51

Slide 52

Slide 52 text

DEFECTS IN USE CASES PublishSubject clicks = /* */; clicks.subscribe(state, toastError); void onResume() { clicks.onError(new RuntimeException()); } ‣ WTF ‣ This shouldn’t happen 52

Slide 53

Slide 53 text

DEFECTS IN USE CASES PublishSubject clicks = /* */; clicks.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

Slide 54

Slide 54 text

DEFECTS IN USE CASES PublishSubject clicks = /* */; clicks.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

Slide 55

Slide 55 text

DEFECTS IN USE CASES PublishSubject clicks = /* */; clicks.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

Slide 56

Slide 56 text

LET IT CRASH (RESPONSIBLY) 56

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

ERRORS IN USE CASES doFM( { () -> state }, { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .map(changes -> current.delta(changes) }) ).subscribe(state) ‣ 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

Slide 60

Slide 60 text

ERRORS IN USE CASES 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

Slide 61

Slide 61 text

ERRORS IN USE CASES doFM( { () -> state }, { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .map(changes -> current.delta(changes) }) ).retry(/* */) .subscribe(state) ‣ Defers the problem instead of solving it ‣ Problems with inconsistency and persistence across rotation 61

Slide 62

Slide 62 text

HANDLING ERRORS - MODELING ERRORS AS A STATE doFM( { () -> state }, { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .map(changes -> Transaction.Success(current.delta(changes))) .onErrorResumeNext( Observable.just(Transaction.Fail(“”))) ).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

Slide 63

Slide 63 text

HANDLING ERRORS - MODELING ERRORS AS A STATE doFM( { () -> state }, { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .map(changes -> Transaction.Success(current.delta(changes))) .onErrorResumeNext( Observable.just(Transaction.Fail(“”))) ).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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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; } 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

Slide 67

Slide 67 text

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; } 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

Slide 68

Slide 68 text

EXAMPLES PT 2 68

Slide 69

Slide 69 text

TRANSACTION FROM LOADING TO SUCCESS/FAIL void handleLoad( BehaviorRelay transaction, TransactionRequest services) { 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

Slide 70

Slide 70 text

TRANSACTION FROM LOADING TO SUCCESS/FAIL void handleLoad( BehaviorRelay transaction, TransactionRequest services) { 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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

RECAP ▸ Where possible, handle your lifecycle internally 73

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

LINKS pacoworks.com/ @fe_hudl github.com/pakoito Slides: tinyurl.com/RxCoimbra16 Extended Slides: tinyurl.com/RxDroidcon16Ext 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