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
    Headless development
    in Fully Reactive Apps
    Paco Estevez - 2017
    1

    View Slide

  2. @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

    View Slide

  3. @pacoworks
    Why headless?
    Insanely faster feedback cycles
    Test and REPL oriented
    Same results across platforms
    3

    View Slide

  4. @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

    View Slide

  5. @pacoworks
    Requirement 1:
    Plain Java
    5

    View Slide

  6. @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

    View Slide

  7. @pacoworks
    DRYP
    (Don’t Repeat Your Peers)
    MVP, MVVM, Clean, VIPER, or any similar architecture
    Dependency Injection, SOLID, and Interface Mocking
    7

    View Slide

  8. @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

    View Slide

  9. @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

    View Slide

  10. @pacoworks
    Easy
    10

    View Slide

  11. @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

    View Slide

  12. @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

    View Slide

  13. @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

    View Slide

  14. @pacoworks
    Requirement 2:
    Wrap Android
    14

    View Slide

  15. @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

    View Slide

  16. @pacoworks
    Injecting static methods
    Observable operation() {
    NetworkService.request()
    .doOnError(e -> RxLogger.logError(e));
    }
    /* Pass globals as functions */
    Observable operation(
    Callable> network,
    Consumer 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

    View Slide

  17. @pacoworks
    Wrapping services
    static Observable 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

    View Slide

  18. @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

    View Slide

  19. @pacoworks
    Reactive Activity lifecycle
    BehaviorRelay 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

    View Slide

  20. @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

    View Slide

  21. @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

    View Slide

  22. @pacoworks
    Second pass to to login form
    Subscription authenticateUser(
    Observable emailInput,
    Predicate validator,
    Scheduler mainThread,
    Consumer loadingDisplay,
    Function> authenticate,
    Consumer onSuccess,
    Consumer 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

    View Slide

  23. @pacoworks
    Too many dependencies :(
    23

    View Slide

  24. @pacoworks
    Requirement 3:
    Logic flows as
    plain data
    24

    View Slide

  25. @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

    View Slide

  26. @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
    26

    View Slide

  27. @pacoworks
    Identifying program flows
    Subscription authenticateUser(
    Observable emailInput,
    Predicate validator,
    Scheduler mainThread,
    Consumer loadingDisplay,
    Function> authenticate,
    Consumer onSuccess,
    Consumer 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

    View Slide

  28. @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

    View Slide

  29. @pacoworks
    Expressing state machines
    interface Idle {}
    class Loading { Email email; … }
    class Fail { String cause; … }
    class Success { Profile profile; … }
    class ScreenState {
    Union4 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 stateHolder;
    Java Kotlin
    29

    View Slide

  30. @pacoworks
    Flow 1: Idle
    Subscription authenticateUserIdle(
    Observable state,
    Observable emailInput,
    Predicate validator,
    Consumer 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

    View Slide

  31. @pacoworks
    Flow 2: Loading
    Subscription authenticateUserLoading(
    Observable state,
    FunctionObservable> authenticate,
    Consumer 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

    View Slide

  32. @pacoworks
    Flow 3: Success
    Subscription authenticateUserSuccess(
    Observable state,
    Consumer 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

    View Slide

  33. @pacoworks
    Flow 4: Failure
    Subscription authenticateUserFailure(
    Observable state,
    Consumer 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

    View Slide

  34. @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

    View Slide

  35. @pacoworks
    Binding side-effects
    void bind(
    Observable stateHolder,
    Observable 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

    View Slide

  36. @pacoworks
    Testing in
    Fully Reactive Apps
    36

    View Slide

  37. 37

    View Slide

  38. @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

    View Slide

  39. @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

    View Slide

  40. @pacoworks
    Recap
    40

    View Slide

  41. @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

    View Slide

  42. @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

    View Slide