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

Peter Mahlen - Mobius A Loopy UI Architecture

Peter Mahlen - Mobius A Loopy UI Architecture

droidcon Berlin

July 12, 2018
Tweet

More Decks by droidcon Berlin

Other Decks in Programming

Transcript

  1. android @spotify • 100+ monthly contributors. • 6 cities. •

    50+ teams. • 400+ Android Studio Modules.
  2. objectives • A framework, not just a pattern • Reactive

    in nature • Strong separation of concerns • Explicit about side-effects • Simpler code
  3. FP aspects inspiration • Pure Functions Testable Composable fun add(x:

    Int, y: Int): Int = x + y • Impure Functions Declared side-effects
  4. Prior Art • “Managing State with RxJava” • Redux •

    Cycle.js/MVI • Elm • Andy Matuschak’s state machines
  5. the nature of programs • Can be modelled as a

    State Machine • State represented through data • State transitions are triggered by Events
  6. model // For Java we can use AutoValue data class

    Model(val email: String, val pass: String) Data class representing the internal state of the program.
  7. events sealed class Event data class EmailInputChanged(val email: String) :

    Event() data class PasswordInputChanged(val pass: String) : Event() Data classes representing different types of events that can happen in your domain.
  8. events in java @DataEnum interface Event_dataenum { dataenum_case EmailInputChanged(String email);

    dataenum_case PasswordInputChanged(String password); } DataEnum annotation processor allows you to create ADTs in Java.
  9. update(model, event) ➞ newModel Interaction Events Model Event Model User

    Interface Update M O B I U S P U R E I M P U R E
  10. update fun update(model: Model, event: Event): Model = when (event)

    { is EmailInputChanged -> model.copy(email = event.email) }
  11. update fun update(model: Model, event: Event): Model = when (event)

    { is EmailInputChanged -> model.copy(email = event.email) is PasswordInputChanged -> model.copy(pass = event.pass) }
  12. update fun update(model: Model, event: Event): Model = when (event)

    { is EmailInputChanged -> onEmailChanged(model, event) is PasswordInputChanged -> model.copy(pass = event.pass) } fun onEmailChanged(model: Model, event: EmailInputChanged): Model = model.copy(email = event.email)
  13. update fun update(model: Model, event: Event): Model = when (event)

    { is EmailInputChanged -> onEmailChanged(model, event) is PasswordInputChanged -> onPassChanged(model, event) } fun onEmailChanged(model: Model, event: EmailInputChanged): Model = model.copy(email = event.email) fun onPassChanged(model: Model, event: PasswordInputChanged): Model = model.copy(pass = event.pass)
  14. more state update data class Model(..., val canLogin: Boolean) fun

    verifyCreds(email: String, password: String): Boolean = ...
  15. more state update data class Model(..., val canLogin: Boolean) fun

    verifyCreds(email: String, password: String): Boolean = ... fun onEmailChanged(model: Model, event: EmailInputChanged): Model = model.copy(email = event.email)
  16. more state update data class Model(..., val canLogin: Boolean) fun

    verifyCreds(email: String, password: String): Boolean = ... fun onEmailChanged(model: Model, event: EmailInputChanged): Model = model.copy(email = event.email, canLogin = verifyCreds(event.email, model.pass))
  17. more state update data class Model(..., val canLogin: Boolean) fun

    verifyCreds(email: String, password: String): Boolean = ... fun onEmailChanged(model: Model, event: EmailInputChanged): Model = model.copy(email = event.email, canLogin = verifyCreds(event.email, model.pass)) fun onPassChanged(model: Model, event: PasswordInputChanged): Model = model.copy(pass = event.pass)
  18. more state update data class Model(..., val canLogin: Boolean) fun

    verifyCreds(email: String, password: String): Boolean = ... fun onEmailChanged(model: Model, event: EmailInputChanged): Model = model.copy(email = event.email, canLogin = verifyCreds(event.email, model.pass)) fun onPassChanged(model: Model, event: PasswordInputChanged): Model = model.copy(pass = event.pass, canLogin = verifyCreds(model.email, event.pass))
  19. update(model, event) ➞ newModel Interaction Events Model Event Model User

    Interface Update M O B I U S P U R E I M P U R E
  20. more events sealed class Event data class EmailInputChanged(val email: String)

    : Event() data class PasswordInputChanged(val pass: String) : Event() object LoginRequested : Event()
  21. more state update data class Model(..., val loggingIn: Boolean) when

    (event) { ... is LoginRequested -> onLoginRequested(model) }
  22. more state update data class Model(..., val loggingIn: Boolean) when

    (event) { ... is LoginRequested -> onLoginRequested(model) } fun onLoginRequested(model: Model): Model = if (!model.loggingIn && model.canLogin) model.copy(loggingIn = true) else model
  23. update(model, event) ➞ (newModel, effects) Interaction Events Model Event Model

    User Interface Update Effects Effect Handler ? M O B I U S P U R E I M P U R E
  24. effects sealed class Effect data class AttemptLogin(val email: String, val

    pass: String) : Effect() data class ShowErrorMessage(val msg: String) : Effect() Data classes representing different types of effects your program would like to happen.
  25. Next<M,F> Next.next(M model) Next.next(M model, Set<? extends F> effects) Next.dispatch(Set<?

    extends F> effects) Next.noChange() A special type that tells Mobius what should happen next.
  26. returning next fun update(model: Model, event: Event): Model = ...

    fun onEmailChanged(model: Model, event: EmailInputChanged): Model = model.copy(email = event.email, canLogin = verifyCreds(event.email, model.password)) fun onPassChanged(model: Model, event: PasswordInputChanged): Model = model.copy(pass = event.password, canLogin = verifyCreds(model.email, event.password))
  27. returning next fun update(model: Model, event: Event): Next<Model, Effect> =

    ... fun onEmailChanged(model: Model, event: EmailInputChanged): Model = model.copy(email = event.email, canLogin = verifyCreds(event.email, model.password)) fun onPassChanged(model: Model, event: PasswordInputChanged): Model = model.copy(pass = event.password, canLogin = verifyCreds(model.email, event.password))
  28. returning next fun update(model: Model, event: Event): Next<Model, Effect> =

    ... fun onEmailChanged(model: Model, event: EmailInputChanged): Next<Model, Effect> = model.copy(email = event.email, canLogin = verifyCreds(event.email, model.password)) fun onPassChanged(model: Model, event: PasswordInputChanged): Model = model.copy(pass = event.password, canLogin = verifyCreds(model.email, event.password))
  29. returning next fun update(model: Model, event: Event): Next<Model, Effect> =

    ... fun onEmailChanged(model: Model, event: EmailInputChanged): Next<Model, Effect> = next(model.copy(email = event.email, canLogin = verifyCreds(event.email, model.password))) fun onPassChanged(model: Model, event: PasswordInputChanged): Model = model.copy(pass = event.password, canLogin = verifyCreds(model.email, event.password))
  30. returning next fun update(model: Model, event: Event): Next<Model, Effect> =

    ... fun onEmailChanged(model: Model, event: EmailInputChanged): Next<Model, Effect> = next(model.copy(email = event.email, canLogin = verifyCreds(event.email, model.password))) fun onPassChanged(model: Model, event: PasswordInputChanged): Next<Model, Effect> = next(model.copy(pass = event.password, canLogin = verifyCreds(model.email, event.password)))
  31. returning next fun onLoginRequested(model: Model): Model = if (!model.loggingIn &&

    model.canLogin) model.copy(loggingIn = true) else model
  32. returning next fun onLoginRequested(model: Model): Next<Model, Effect> = if (!model.loggingIn

    && model.canLogin) next(model.copy(loggingIn = true)) else next(model)
  33. dispatching effects no change fun onLoginRequested(model: Model): Next<Model, Effect> =

    if (!model.loggingIn && model.canLogin) next(model.copy(loggingIn = true)) else noChange()
  34. dispatching effects no change fun onLoginRequested(model: Model): Next<Model, Effect> =

    if (!model.loggingIn && model.canLogin) next(model.copy(loggingIn = true), effects(AttemptLogin(model.email, model.pass))) else noChange()
  35. dispatching effects no change fun onLoginRequested(model: Model): Next<Model, Effect> =

    if (!model.loggingIn && model.canLogin) next(model.copy(loggingIn = true), effects(AttemptLogin(model.email, model.pass))) else dispatch(effects(ShowErrorMessage("Can't login”)))
  36. where do these effects go? Interaction Events Model Event Model

    User Interface Update Effects Effect Handler ? M O B I U S P U R E I M P U R E
  37. effect handlers • Do exactly what the name suggests.. Handle

    Effects • Effect handlers are impure • Can generate more Events to communicate results, progress or anything really
  38. effect handlers Interaction Events Model Event Model User Interface Update

    Effect Feedback Events Effects Effect Handler M O B I U S P U R E I M P U R E ?
  39. feedback events sealed class Event data class EmailInputChanged(val email: String)

    : Event() data class PasswordInputChanged(val pass: String) : Event() object LoginRequested : Event() object LoginSuccessful : Event() data class LoginFailed(val errorMsg: String) : Event()
  40. navigation effects sealed class Effect data class AttemptLogin(val email: String,

    val pass: String) : Effect() data class ShowErrorMessage(val msg: String) : Effect() object NavigateToHome : Effect()
  41. update chaining effects when (event) { ... is LoginSuccessful ->

    onLoginSuccess(model) is LoginFailed -> ... } fun onLoginSuccess(model: Model): Next<Model, Effect> = dispatch(effects(NavigateToHome))
  42. update chaining effects when (event) { ... is LoginSuccessful ->

    onLoginSuccess(model) is LoginFailed -> onLoginFailed(model, event) } fun onLoginSuccess(model: Model): Next<Model, Effect> = dispatch(effects(NavigateToHome)) fun onLoginFailed(model: Model, event: LoginFailed): Next<Model, Effect> = next(model.copy(loggingIn = false), effects(ShowErrorMessage(event.errorMsg)))
  43. effect handlers fun loginHandler(attemptLogins: Observable<AttemptLogin>) = attemptLogins.switchMap { loginService.login(it.email, it.password)

    .map { result -> when (result) { is Success -> LoginSuccessful is Timeout -> LoginFailed("Login request timed out!") is Failure -> LoginFailed(result.message) } } }
  44. kind of events • UI interactions (user intent) • Effect

    handlers (effect feedback) • External events • System events (eg. network changes) • Application global state changes
  45. mobius loops Interaction Events Model Event Model User Interface Update

    Effect Feedback Events Effects Effect Handler M O B I U S P U R E I M P U R E External Events Event Source
  46. mobius-rx • Handle effects using RxJava chains ObservableTransformer<Effect, Event> •

    Attach MobiusLoop to an Observable<Event> ObservableTransformer<Event, Model> • Create EventSource out of Observables Observable<Event>
  47. mobius-test val model: Model = Model(email = "[email protected]", pass =

    "Hunter2", canLogin = true, loggingIn = false) spec.given(model) .whenEvent(LoginRequested) .then(assertThatNext( hasModel(model.copy(loggingIn = true)), hasEffects(AttemptLogin("[email protected]", "Hunter2")) )) • TDD your Update with UpdateSpec and write behavior tests • Hamcrest Next matchers
  48. objectives recap • A framework, not just a pattern •

    Reactive • Separation of concerns • Explicit about side-effects • Simpler code
  49. comparing to mvi • Events instead of intent • Less

    Rx intense • MVI: intent -> action / state change • Mobius: event -> state change -> effect
  50. some nice side effects • Better debuggability • A unified

    modeling approach Collaborative and inclusive Answers the important questions as early as possible Easy to use and follow • Better planning