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

Headless development in Fully Reactive Apps

pakoito
February 16, 2017

Headless development in Fully Reactive Apps

Liberate yourself from the Android framework and start developing your business value on your laptop. This presentation covers the requirements, tips to fulfil them, and an overview of testing reactive state.

Demo:
https://gist.github.com/pakoito/1c87fcdff328d78918113ac2fcd5e3a9

Test examples:
https://github.com/pakoito/FunctionalAndroidReference/tree/master/liblogic/src/test/kotlin/com/pacoworks/dereference/features/rotation

pakoito

February 16, 2017
Tweet

More Decks by pakoito

Other Decks in Programming

Transcript

  1. @pacoworks What do I mean by headless development? Interacting with

    the apps directly using only code Avoid using devices or emulator Focus on delivering business value 2
  2. @pacoworks How can we achieve it? Write the logic in

    plain Java (or Kotlin) Wrap all Android framework behaviour Design logic flows using data 4
  3. @pacoworks About plain Java Can be run on a desktop

    JVM Fast compile times All the tools necessary to write and test business logic Kotlin bonus: REPL live console! 6
  4. @pacoworks DRYP (Don’t Repeat Your Peers) MVP, MVVM, Clean, VIPER,

    or any similar architecture Dependency Injection, SOLID, and Interface Mocking 7
  5. @pacoworks Login form example Subscription authenticateUser( LoginView view, NetworkService authServices)

    { view.onEmailAuthenticationClick() .filter(email -> !ModelUtils.isValid(email)) .observeOn(AndroidSchedulers.mainThread()) .doOnNext(ignored -> view.showLoading()) .flatMap(email -> authServices.auth(email) .subscribeOn(Schedulers.io())) .observeOn(AndroidSchedulers.mainThread()) .subscribe( profile -> view.goToScreen(profile), error -> view.showError(error) ) } Wait for the user to input an email/ password combo Authenticate against server Go to main screen Some validation and threading Display loading state 8
  6. @pacoworks First pass to login form Subscription authenticateUser( LoginView view,

    NetworkService authServices, Scheduler mainThread, Scheduler background) { view.onEmailAuthenticationClick() .filter(email -> !ModelUtils.isValid(email)) .observeOn(mainThread) .doOnNext(ignored -> view.showLoading()) .flatMap(email -> authServices.auth(email) .subscribeOn(backGround)) .observeOn(mainThread) .subscribe( profile -> view.goToScreen(profile), error -> view.showError(error) ) } Remove dependency on Android machinery Make all dependencies explicit Use the highest possible class in the hierarchy 9
  7. @pacoworks Psyched! interface NetworkService { Profile auth(String email); ... }

    class Profile extends RealmObject { ... } Beware of your data models! Separate your business logic models from your services layer Non-serialisable data models can be tailored for your behaviours 11
  8. @pacoworks Psyched! interface NetworkService { Profile auth(String email); ... }

    class Profile extends RealmObject { ... } Beware of your data models! Separate your business logic models from your services layer Non-serialisable data models can be tailored for your behaviours More on this later! ——> 12
  9. @pacoworks Restraining order Separate business logic into a separate java-only

    library Will fail at compile time when any Android dependency is required Share lightweight business logic across apps UI and services become an “implementation detail” 13
  10. @pacoworks Perfect reactive world Dependencies should be injected as instances

    of a function, plain data, or an Observable Scope dependencies and avoid Singletons simple fields in Activity and Application with a module like in Dagger 15
  11. @pacoworks Injecting static methods Observable<Int> operation() { NetworkService.request() .doOnError(e ->

    RxLogger.logError(e)); } /* Pass globals as functions */ Observable<Int> operation( Callable<Observable<Result>> network, Consumer<Throwable> logger) { return network.call() .doOnError(logger); } ... /* Call it with a function object */ operation( () -> NetworkService.request(), e -> RxLogger.logError()); Solves dependencies on static methods Decouples so no testing libs are required Doesn’t solve any memory management issues Interfaces may be different in RxJava1 and Kotlin 16
  12. @pacoworks Wrapping services static Observable<Boolean> wrap( ConnectivityService service) { return

    Observable.create(emitter -> { final Callback callback = /* Forward value from callback */ isConnected -> emitter.onNext(isConnected); /* Remove callback when the Observable terminates */ emitter.setCancellable(() -> service.removeCb(callback)); /* Start listening */ service.addCb(callback); }); } operation(() -> wrap(ConnectivityService.getInstance(activity)) ); Wraps any Singleton and Activity reference into a closure Decouples by converting ConnectivityService into an Observable Ties the service to the Observable lifecycle 17
  13. @pacoworks The operator takeUntil() Allows for fine-grained lifecycle control Needs

    an Observable signal, and cancels the Observable once the first value is received Works with the Android lifecycle with some adjustments 18
  14. @pacoworks Reactive Activity lifecycle BehaviorRelay<Lifecycle> lifecycle = BehaviorRelay.create() protected void

    onCreate(Bundle b) { if (b == null) { lifecycle.call(ENTER); } lifecycle.call(CREATE); } protected void onDestroy() { lifecycle.call(DESTROY); if (isFinishing()) { lifecycle.call(EXIT); } } Map every lifecycle state to a piece of data Store the data in an Observable that caches the latest value, like BehaviourSubject or BehaviourRelay 19
  15. @pacoworks Fixing leaks with takeUntil() operation(() -> wrap(ConnectivityService.getInstance(activity)) .takeUntil(lifecycle.filter(is(DESTROY))) );

    Ties the lifecycle of the Observable to the Activity Transparent to the business logic Allows compositions where some Observables are tied to Activities and others aren’t 20
  16. @pacoworks Back to to login form Subscription authenticateUser( LoginView view,

    NetworkService authServices, Scheduler mainThread, Scheduler background) { view.onEmailAuthenticationClick() .filter(email -> !ModelUtils.isValid(email)) .observeOn(mainThread) .doOnNext(ignored -> view.showLoading()) .flatMap(email -> authServices.auth(email) .subscribeOn(backGround)) .observeOn(mainThread) .subscribe( profile -> view.goToScreen(profile), error -> view.showError(error) ) } 21
  17. @pacoworks Second pass to to login form Subscription authenticateUser( Observable<Email>

    emailInput, Predicate<Email> validator, Scheduler mainThread, Consumer<Email> loadingDisplay, Function<Email, Observable<Profile>> authenticate, Consumer<Profile> onSuccess, Consumer<Error> onError) { emailInput .filter(validator) .observeOn(mainThread) .doOnNext(loadingDisplay) .flatMap(authenticate) .observeOn(mainThread) .subscribe(onSuccess, onError) } Describes what the business logic does, not how it does it Replace NetworkService and LoginView with their function calls Background requirement is gone 22
  18. @pacoworks Perfect reactive world 2 State is external and separate

    to behaviour Not burdened by serialisation requirements Model brings compile-time assurances that all behaviour is checked for you 25
  19. @pacoworks Perfect reactive world 2 State is external and separate

    to behaviour Not burdened by serialisation requirements Model brings compile-time assurances that all behaviour is checked for you <—— Links at the end 26
  20. @pacoworks Identifying program flows Subscription authenticateUser( Observable<Email> emailInput, Predicate<Email> validator,

    Scheduler mainThread, Consumer<Email> loadingDisplay, Function<Email, Observable<Profile>> authenticate, Consumer<Profile> onSuccess, Consumer<Error> onError) { emailInput .filter(validator) .observeOn(mainThread) .doOnNext(loadingDisplay) .flatMap(authenticate) .observeOn(mainThread) .subscribe(onSuccess, onError) } Screen is idle User fills information Screen is now loading Remote auth happens Screen is either error or success 27
  21. @pacoworks Redesigning the flows The first flow starts from Idle

    and transitions to Loading when the user inputs a valid value The second flow waits until Loading is required, displays a loading spinner, then makes a request to the network and transitions to either Success or Failure After a Success we navigate to the next screen and the current transitions to Idle After a Failure an error is displayed and the screen transitions back to Idle 28
  22. @pacoworks Expressing state machines interface Idle {} class Loading {

    Email email; … } class Fail { String cause; … } class Success { Profile profile; … } class ScreenState { Union4<Idle, Loading, Fail, Success> state; } sealed class ScreenState object Idle : ScreenState() data class Loading(val email:Email) : ScreenState() data class Fail(val cause: String) : ScreenState() data class Success(val profile: Profile) : ScreenState() BehaviorRelay<ScreenState> stateHolder; Java Kotlin 29
  23. @pacoworks Flow 1: Idle Subscription authenticateUserIdle( Observable<ScreenState> state, Observable<Email> emailInput,

    Predicate<Email> validator, Consumer<ScreenState> applyState) { state .filter(matchOf(Idle.class)) .flatMap(idle -> emailInput .filter(validator) .first()) .map(email -> ScreenState.loading(email)) .subscribe(applyState) } The first flow starts from Idle and transitions to Loading when the user inputs a valid value Transitions as soon as a valid email is seen Bonus: only one emailInput can be processed at a time 30
  24. @pacoworks Flow 2: Loading Subscription authenticateUserLoading( Observable<ScreenState> state, Function<Email, Observable<ScreenState>>

    authenticate, Consumer<ScreenState> applyState) { state .filter(matchOf(Loading.class)) .flatMap(loading -> authenticate.apply(loading.email)) .subscribe(applyState) } The second flow waits until Loading and transitions to either Success or Failure New value comes directly as a result of the authenticate observable 31
  25. @pacoworks Flow 3: Success Subscription authenticateUserSuccess( Observable<ScreenState> state, Consumer<ScreenState> applyState)

    { state .filter(matchOf(Success.class)) .map(success -> ScreenState.idle()) .subscribe(applyState) } After a Success we navigate to the next screen and the current transitions to Idle 32
  26. @pacoworks Flow 4: Failure Subscription authenticateUserFailure( Observable<ScreenState> state, Consumer<ScreenState> applyState)

    { state .filter(matchOf(Failure.class)) .map(failure -> ScreenState.idle()) .subscribe(applyState) } After a Failure an error is displayed and the screen transitions back to Idle 33
  27. @pacoworks Redesigning the flows The first flow starts from Idle

    and transitions to Loading when the user inputs a valid value The second flow waits until Loading is required, displays a loading spinner, then makes a request to the network and transitions to either Success or Failure After a Success we navigate to the next screen and the current transitions to Idle After a Failure an error is displayed and the screen transitions back to Idle 34
  28. @pacoworks Binding side-effects void bind( Observable<ScreenState> stateHolder, Observable<Lifecycle> lifecycle, Scheduler

    mainThread, LoginView view) { stateHolder .observeOn(mainThread) .takeUntil(lifecycle.filter(is(DESTROY))) .subscribe(state -> state.match( idle -> view.showIdle(), loading -> view.showLoading(), profile -> view.goToScreen(profile), error -> view.showError(error)) } Not part of the business logic Dependent on main thread Dependent on lifecycle 35
  29. 37

  30. @pacoworks Reactive testing blueprint Create initial state observed by TestSubscriber

    Subscribe initial state to logic Use TestSubscriber to assert initial state Act on view or state to trigger flow Assert no termination + assert no errors Assert changes from initial state to target state 38
  31. @pacoworks Asynchronous testing tips Everything is asynchronous! Use Schedulers.trampoline() where

    possible Use TestSubscriber#awaitTerminalEvent() for terminating flows For time-sensitive new state await using state.skip(N).toBlockingFirst() where N is the number of states from the current until the target one Use timeout() and TestSubscriber#awaitTerminalEvent() to assert values that shouldn’t change 39
  32. @pacoworks Headless reactive development! Immediate feedback Simpler input-output architecture Atomic

    dependencies w/o a DI framework Real dependencies w/o a mocking library REPL trial-and-error become tests and helpers 41
  33. @pacoworks Fully Reactive Apps: http://www.pacoworks.com/2016/11/02/fully- reactive-apps-at-droidcon-uk-2016-2/ Sample project: https://github.com/pakoito/ FunctionalAndroidReference

    Demo: https://gist.github.com/pakoito/ 1c87fcdff328d78918113ac2fcd5e3a9 Modeling state machines: http://www.pacoworks.com/2016/10/03/new- talk-a-domain-driven-approach-to-kotlins- new-types-at-mobilization-2016/ Memory management, lifecycle, and wrapping Android: http://tinyurl.com/RxMemBytes17 pacoworks.com/ @pacoworks github.com/pakoito Slides: https://tinyurl.com/RxHeadlessMobOS17 42