Why MVI? Model View Intent -- The curious case of yet another pattern

A3958eeb9a7f402b134c0c017d6614ee?s=47 ragdroid
March 27, 2018

Why MVI? Model View Intent -- The curious case of yet another pattern

All of us are aware of many architectural patterns like MVC, MVP, MVVM, etc. It was not long ago when we had started applying MVP or MVVM to our projects. It was not an easy task to refactor existing code to adopt these patterns, plus the cost of bringing the whole team to the same level was not insignificant. These patterns brought along many advantages like separation of logic and better testing.

We are happy with where our codebase is at the moment and it’s test coverage. Why do we then need to move to a new pattern now? What real problem does this pattern solve? Are all my efforts gone in vain now that we have a new pattern?

There have been multiple talks about MVI, so let's take a different approach & instead of jumping directly into code, let's answer the above questions first using the approach below:

In this talk:
- We will learn about the main problem that this pattern solves - The State Problem, What it is and how MVI solves it.
- We will also learn that MVI can also be applied on top of our existing patterns.
- We need not completely change the style that we have already adopted. Instead we can modify our existing style to take the advantage of the reactive flow and achieve better state management.

A3958eeb9a7f402b134c0c017d6614ee?s=128

ragdroid

March 27, 2018
Tweet

Transcript

  1. Why MVI? “Curious case of yet another pattern" Garima Jain

    @ragdroid
  2. @ragdroid @droidconBos Why MVI? MVP / MVVM -> MVI

  3. @ragdroid @droidconBos Part 1 Why MVI?

  4. @ragdroid @droidconBos MVI

  5. @ragdroid @droidconBos Model View Intent

  6. @ragdroid @droidconBos State View Intention Model View Intent

  7. @ragdroid @droidconBos User Intent State View State View Intention

  8. @ragdroid @droidconBos User Intent State View Interacts Changes Updates Seen

    State View Intention
  9. @ragdroid @droidconBos Model View Intent State

  10. @ragdroid @droidconBos State Single Source of Truth

  11. @ragdroid @droidconBos printing length completed noInternet loggedIn size scanning index

    State sPullToRefreshing List<Item> isButtonEnabled pageNo electedItem isChecked e r r o r M s g i s V i s i B l e i s L o a d i n g sLoadMore pen r u n n i n g active l i k e d l o c a t i o n i d l e
  12. @ragdroid @droidconBos State

  13. @ragdroid @droidconBos ItemsFragment State

  14. @ragdroid @droidconBos State { loading = true items = EMPTY

    } State
  15. @ragdroid @droidconBos State { loading = false items = EMPTY

    emptyStateVisible = true } State
  16. @ragdroid @droidconBos State { loading = false refreshing = true

    items = EMPTY emptyStateVisible = true } State
  17. @ragdroid @droidconBos State { loading = false refreshing = false

    items = EMPTY emptyStateVisible = false errorMsg = “No Internet” } State
  18. @ragdroid @droidconBos State { loading = false refreshing = false

    items = List<Item> emptyStateVisible = false errorMsg = “” } State
  19. @ragdroid @droidconBos State Problem

  20. @ragdroid @droidconBos

  21. @ragdroid @droidconBos View Presenter Data User Time

  22. @ragdroid @droidconBos View Presenter showLoading( ) Result : [ ]

    Data User start() refresh() loadItems( ) setRefreshing( ) Result : [ ] showEmptyState( ) hideLoading( ) showEmptyState( ) hideLoading( )?? refresh( )
  23. @ragdroid @droidconBos Presenter loadItems( ) refresh( ) Multiple Inputs

  24. @ragdroid @droidconBos Presenter showEmptyState( ) hideLoading( ) showEmptyState( ) hideLoading(

    )?? Multiple Outputs !!!
  25. @ragdroid @droidconBos State of UI coordinated by Presenter (or VM)

    Presenter, Business Logic and View : own states. MVI: Single Source of Truth : Hannes Model / View State MVI: Presenter has a single output : f(x) = y OR f(a,b,c) = y State Problem
  26. @ragdroid @droidconBos MVP / MVVM -> MVI Part 2

  27. @ragdroid @droidconBos MVPI

  28. @ragdroid @droidconBos View Presenter Data State { loading = true

    refreshing = false items = EMPTY emptyStateVisible = false errorMsg = “” } UIEvent Reducer newState Action initialState Result MVPI with Initial State
  29. @ragdroid @droidconBos View Presenter Data State { loading = true

    refreshing = false items = EMPTY emptyStateVisible = false errorMsg = “” } UIEvent Reducer newState Action Result oldState MVPI with Old State
  30. @ragdroid @droidconBos View Presenter Data Start Reducer newState LoadItems Loading

    Loading initialState State { loading = true refreshing = false items = EMPTY emptyStateVisible = false errorMsg = “” }
  31. @ragdroid @droidconBos View Presenter Data State { loading = true

    refreshing = false items = EMPTY emptyStateVisible = false errorMsg = “” } Start Reducer newState Loading initialState Loading LoadItems
  32. @ragdroid @droidconBos View Presenter Data Reducer Empty Start Result :

    [ ] Empty oldState newState State { loading = true refreshing = false items = EMPTY emptyStateVisible = false errorMsg = “” } LoadItems
  33. @ragdroid @droidconBos View Presenter Data State { loading = false

    refreshing = false items = EMPTY emptyStateVisible = true errorMsg = “” } Start Result : [ ] Reducer newState oldState Empty Empty LoadItems
  34. @ragdroid @droidconBos View Presenter Data Pull To Refresh Reducer newState

    RefreshItems oldState Refreshing Refreshing State { loading = false refreshing = false items = EMPTY emptyStateVisible = true errorMsg = “” }
  35. @ragdroid @droidconBos View Presenter Data Pull To Refresh Reducer newState

    RefreshItems oldState Refreshing Refreshing State { loading = false refreshing = true items = EMPTY emptyStateVisible = true errorMsg = “” }
  36. @ragdroid @droidconBos View Presenter Data Pull To Refresh Reducer newState

    RefreshItems Result : [ ] oldState RefreshEmpty RefreshEmpty State { loading = false refreshing = true items = EMPTY emptyStateVisible = true errorMsg = “” }
  37. @ragdroid @droidconBos View Presenter Data State { loading = false

    refreshing = false items = EMPTY emptyStateVisible = true errorMsg = “” } Pull To Refresh Reducer newState RefreshItems Result : [ ] oldState RefreshEmpty RefreshEmpty
  38. @ragdroid @droidconBos View Presenter Data State { loading = true

    refreshing = false items = EMPTY emptyStateVisible = false errorMsg = “” } UIEvent Reducer newState Action Result oldState MVPI with Concurrent Events UIEvent Action Result oldState newState
  39. @ragdroid @droidconBos Rules of Thumb

  40. @ragdroid @droidconBos Plain Objects Must have a type which is

    not undefined Convey intention of user User triggers Actions Action
  41. @ragdroid @droidconBos Immutable : List / Object nothing is mutable

    Set representing the state of View Any change is triggered only by dispatching an action State
  42. @ragdroid @droidconBos Function of previous state & result (action/ partialState)

    f(previousState, Result) = new State Return current state for unknown result Reducer
  43. @ragdroid @droidconBos Code

  44. @ragdroid @droidconBos interface View { /** * Expose various intents

    */ fun pullToRefreshIntent(): Observable<Boolean> fun loadingIntent(): Observable<Boolean> fun render(state: State) } View
  45. @ragdroid @droidconBos fun pullToRefreshIntent(): Observable<Boolean> = refreshLayout.refreshes() .map { ignored

    -> true } View fun loadingIntent(): Observable<Boolean> = Observable.just(true)
  46. @ragdroid @droidconBos data class State( val loading: Boolean, val items:

    List<Item>, val loadingError: Throwable?, val pullToRefreshing: Boolean, val pullToRefreshError: Throwable?, val emptyStateVisible: Boolean) State
  47. @ragdroid @droidconBos sealed class Result { object Loading: Result() object

    LoadingEmpty: Result() data class LoadingError(val throwable: Throwable): Result() data class LoadingComplete(val characters: List<Item>): Result() object PullToRefreshing: Result() object PullToRefreshEmpty: Result() data class PullToRefreshError(val throwable: Throwable): Result() data class PullToRefreshComplete(val characters: List<Item>): Result() } Result
  48. @ragdroid @droidconBos fun attachView(view: View) { val loadingResult = subscribeToLoading()

    val pullToRefreshResult = subscribeToPullToRefresh() val allResultObservable = Observable.merge(loadingResult, pullToRefreshResult) subscribeToStates(allResultObservable) } Presenter
  49. @ragdroid @droidconBos interface Repository { fun loadItems(): Observable<List<Item>> } Repository

  50. @ragdroid @droidconBos fun subscribeToLoading(): Observable<Result> = view.loadingIntent() .flatMap { repository.loadItems()

    .map { items -> if (items.isEmpty()) Result.LoadingEmpty else Result.LoadingComplete(items) } } .onErrorReturn { Result.LoadingError(it) } Presenter
  51. @ragdroid @droidconBos fun subscribeToPullToRefresh(): Observable<Result> { return view.pullToRefreshIntent() .flatMap {

    repository.loadItems() .map { items -> if (items.isEmpty()) Result.PullToRefreshEmpty else Result.PullToRefreshComplete(items) } .startWith(Result.PullToRefreshing) } .onErrorReturn { Result.PullToRefreshError(it) } } Presenter
  52. @ragdroid @droidconBos fun subscribeToStates(allResultObservable): Disposable { return allResultObservable .scan(State.init()) {

    previousState, result -> reducer(previousState, result) } .observeOn(UISched) .subscribe( { state -> view.render(state) }, { Timber.e(it) } ) } Presenter
  53. @ragdroid @droidconBos fun reducer(previousState: State, result: Result): State = when

    (result) { is Result.Loading -> previousState.copy( loading = true, loadingError = null) is Result.LoadingError -> previousState.copy( loading = false, loadingError = result.throwable) is Result.LoadingComplete -> previousState.copy( loading = false, loadingError = null, items = result.characters) is Result.LoadingEmpty -> previousState.copy( loading = false, loadingError = null, items = emptyList(), emptyStateVisible = true) ... } Presenter
  54. @ragdroid @droidconBos fun reducer(previousState: State, result: Result): State = when

    (result) { ... is Result.PullToRefreshing -> previousState.copy( loading = false, pullToRefreshing = true, pullToRefreshError = null) is Result.PullToRefreshError -> previousState.copy( pullToRefreshing = false, pullToRefreshError = result.throwable) is Result.PullToRefreshComplete -> previousState.copy( pullToRefreshing = false, pullToRefreshError = null, items = result.characters) is Result.PullToRefreshEmpty -> previousState.copy( pullToRefreshing = false, loadingError = null, items = emptyList(), emptyStateVisible = true ) } Presenter
  55. @ragdroid @droidconBos fun render(state: State) { binding.model = state when

    { state.pullToRefreshError != null -> return@render state.emptyStateVisible -> return@render state.loadingError != null -> { //show error return@render } else -> { //show list } } } View
  56. @ragdroid @droidconBos Why MVI? Hannes Dorfmann MVI Series

  57. @ragdroid @droidconBos State Problem

  58. @ragdroid @droidconBos Orientation Changes

  59. @ragdroid @droidconBos Navigation & Pop Backstack

  60. @ragdroid @droidconBos Process Death

  61. @ragdroid @droidconBos Immutability

  62. @ragdroid @droidconBos Debuggable

  63. @ragdroid @droidconBos Testability

  64. @ragdroid @droidconBos Conclusion

  65. @ragdroid @droidconBos If you model your system as pure functions,

    you will get a finite state machine - @ragdroid Try to avoid side-effects and model your screens as pure functions. - @ragdroid MVI is MVP or MVVM done right. - @_riteshhh MVI prevents us from misusing patterns like MVP or MVVM. - @_riteshhh
  66. @ragdroid @droidconBos http://hannesdorfmann.com/android/mosby3-mvi-1 - Hannes Dorfmann http://fragmentedpodcast.com/tag/hannes-dorfmann/ - Hannes Dorfmann

    https://www.youtube.com/watch?v=0IKHxjkgop4 - Jake Wharton https://www.youtube.com/watch?v=64rQ9GKphTg - Benoit Quenaudon https://github.com/oldergod - Benoit Quenaudon https://egghead.io/courses/getting-started-with-redux - Dan Abramov References
  67. @ragdroid @droidconBos https://github.com/ragdroid/klayground Demo

  68. FUELED http://fueled.com/garima garima@fueled.com THANK YOU