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

Simple MVI Architecture for Android

Simple MVI Architecture for Android

An introduction to the Model View Intent architecture pattern.

This pattern takes ideas and concepts from Reactive and Functional programming. We'll go over the core principles of this pattern, and how you can apply them in your Android applications.

We'll look at concrete implementation details, and explore some of its benefits. Some familiarity with Kotlin and RxJava is assumed.

Etienne Caron

April 09, 2019
Tweet

More Decks by Etienne Caron

Other Decks in Technology

Transcript

  1. Simple MVI Architecture
    for Android

    View Slide

  2. http://hannesdorfmann.com/android/mosby3-mvi-1

    View Slide

  3. View Slide

  4. https://xkcd.com/1312

    View Slide

  5. View Slide

  6. Simple MVI Architecture
    for Android

    View Slide

  7. Simple MVI Architecture
    for Android

    View Slide

  8. output
    input
    input
    output
    user
    device

    View Slide

  9. output
    input
    input
    output
    user
    device

    View Slide

  10. output
    input
    input
    output
    user
    device

    View Slide

  11. output
    input
    input
    output
    user
    device

    View Slide

  12. input
    output
    input output
    user
    device

    View Slide

  13. input
    output
    input output
    user
    device

    View Slide

  14. Model
    Intent
    View

    View Slide

  15. Intent
    View

    View Slide

  16. Intent
    View

    View Slide

  17. Intent
    View

    View Slide

  18. Intent
    View

    View Slide

  19. Intent
    View

    View Slide

  20. Intent
    View

    View Slide

  21. View MainViewEvent Intent
    sealed class MainViewEvent {_
    object ThumbsUpClick :_MainViewEvent()
    object LoveItClick :_MainViewEvent()
    }_

    View Slide

  22. MainViewEvent Intent
    sealed class MainViewEvent {_
    object ThumbsUpClick :_MainViewEvent()
    object LoveItClick :_MainViewEvent()
    }_
    View

    View Slide

  23. View
    sealed class MainViewEvent {_
    object ThumbsUpClick :_MainViewEvent()
    object LoveItClick :_MainViewEvent()
    }_
    MainViewEvent
    ThumbsUpClick
    LoveItClick
    +
    +❤

    View Slide

  24. View
    sealed class MainViewEvent {_
    object ThumbsUpClick :_MainViewEvent()
    object LoveItClick :_MainViewEvent()
    }_
    MainViewEvent
    ThumbsUpClick
    LoveItClick
    +
    +❤

    View Slide

  25. View
    sealed class MainViewEvent {_
    object ThumbsUpClick :_MainViewEvent()
    object LoveItClick :_MainViewEvent()
    }_
    MainViewEvent
    ThumbsUpClick
    LoveItClick
    +
    +❤
    ❤ ❤

    View Slide


  26. View
    sealed class MainViewEvent {_
    object ThumbsUpClick :_MainViewEvent()
    object LoveItClick :_MainViewEvent()
    }_
    MainViewEvent
    ThumbsUpClick
    LoveItClick
    +
    +❤
    ❤ ❤

    View Slide

  27. ❤ ❤
    View.clicks():Observable
    View
    package com.jakewharton.rxbinding2.view
    Observable

    View Slide

  28. View
    thumbButton.clicks()
    package com.jakewharton.rxbinding2.view
    Observable
    ❤ ❤

    View Slide

  29. View
    thumbButton.clicks()
    map { MainViewEvent.ThumbsUpClick }
    package com.jakewharton.rxbinding2.view
    Observable
    ❤ ❤

    View Slide

  30. View
    thumbButton.clicks()
    map { MainViewEvent.ThumbsUpClick }

    package com.jakewharton.rxbinding2.view
    Observable
    ❤ ❤

    View Slide

  31. View
    heartButton.clicks()
    package com.jakewharton.rxbinding2.view
    Observable
    ❤ ❤

    View Slide

  32. View
    heartButton.clicks()
    map { MainViewEvent.LoveItClick }
    package com.jakewharton.rxbinding2.view
    Observable
    ❤ ❤

    View Slide

  33. View
    heartButton.clicks()
    map { MainViewEvent.LoveItClick }

    package com.jakewharton.rxbinding2.view
    Observable
    ❤ ❤

    View Slide

  34. View
    heartButton.clicks()
    map { MainViewEvent.LoveItClick }
    ❤ ❤
    package com.jakewharton.rxbinding2.view
    Observable
    ❤ ❤

    View Slide

  35. Observable
    View
    ❤ ❤

    View Slide

  36. Observable.merge(
    heartButton.clicks().map {_LoveItClick },
    thumbButton.clicks().map {_ThumbsUpClick_}
    )
    Observable
    View
    ❤ ❤

    View Slide

  37. View
    ❤ ❤
    class MainActivity {_
    override fun viewEvents(): Observable_{
    return Observable.merge(
    heartButton.clicks().map {_LoveItClick },
    thumbButton.clicks().map {_ThumbsUpClick_}
    )_
    }_
    }_

    View Slide

  38. /**
    * This allows us to group all the viewEvents from
    * one view in a single Observable.
    */
    interface ViewEventObservable_{
    funxviewEvents():xObservable
    }x
    class MainActivity :_ViewEventObservable {_
    override fun viewEvents(): Observable_{
    return Observable.merge(
    heartButton.clicks().map {_LoveItClick },
    thumbButton.clicks().map {_ThumbsUpClick_}
    )_
    }_
    }_
    View
    ❤ ❤

    View Slide

  39. class MainActivity :_ViewEventObservable {_
    private val disposables = CompositeDisposable()
    override fun onResume()_{
    super.onResume()
    disposables += viewEvents().subscribe(1 )1
    }1
    override fun onPause()_{
    super.onPause()
    disposables.clear()
    }2
    override fun viewEvents(): Observable_{
    return Observable.merge(
    heartButton.clicks().map {_LoveItClick },
    thumbButton.clicks().map {_ThumbsUpClick_}
    )_
    }_
    }_ View Intent

    View Slide

  40. class MainActivity :_ViewEventObservable {_
    private val disposables = CompositeDisposable()
    override fun onResume()_{
    super.onResume()
    disposables += viewEvents().subscribe(1 )1
    }1
    override fun onPause()_{
    super.onPause()
    disposables.clear()
    }2
    override fun viewEvents(): Observable_{
    return Observable.merge(
    heartButton.clicks().map {_LoveItClick },
    thumbButton.clicks().map {_ThumbsUpClick_}
    )_
    }_
    }_ View Intent


    View Slide

  41. class MainActivity :_ViewEventObservable {_
    private val disposables = CompositeDisposable()
    override fun onResume()_{
    super.onResume()
    disposables += viewEvents().subscribe(1
    MainViewIntentFactory::process
    )1
    }1
    override fun onPause()_{
    super.onPause()
    disposables.clear()
    }2
    override fun viewEvents(): Observable_{
    return Observable.merge(
    heartButton.clicks().map {_LoveItClick },
    thumbButton.clicks().map {_ThumbsUpClick_}
    )_
    }_
    }_
    View
    x
    Intent

    View Slide

  42. ModelState
    store
    x
    Intent
    View

    Model
    M

    View Slide

  43. ModelState
    store
    x
    Intent
    View


    Model
    M

    View Slide

  44. ModelState
    store
    x
    Intent
    View


    Model
    M

    View Slide

  45. ModelState
    store
    Model
    M
    val ❤:Int
    val :Int
    M

    View Slide

  46. ModelState
    store
    Model
    M
    val ❤:Int
    val :Int
    data class UpvoteModel(val hearts:Int, val thumbs:Int)
    M
    (Don’t mind me. My
    turn is coming up
    soon.)

    View Slide

  47. ModelState
    store
    Intent
    x
    View

    Model
    M
    data class UpvoteModel(val hearts:Int, val thumbs:Int)
    val ❤ :Int
    val :Int
    x
    x

    View Slide

  48. ModelState
    store
    Intent
    x
    View

    Model
    M
    data class UpvoteModel(val hearts:Int, val thumbs:Int)
    newState = oldState.copy(thumbs = thumbs + 1) // 0, 1
    val ❤ :Int
    val :Int
    x
    x

    View Slide

  49. ModelState
    store
    Intent
    x
    View

    Model
    M
    data class UpvoteModel(val hearts:Int, val thumbs:Int)
    newState = oldState.copy(thumbs = thumbs + 1) // 0, 1
    newState = newState.copy(hearts = hearts + 1) // 1, 1
    val ❤ :Int
    val :Int
    x
    x

    View Slide

  50. ModelState
    store
    Intent
    x
    View

    Model
    M
    data class UpvoteModel(val hearts:Int, val thumbs:Int)
    newState = oldState.copy(thumbs = thumbs + 1) // 0, 1
    newState = newState.copy(hearts = hearts + 1) // 1, 1
    newState = newState.copy(hearts = hearts + 1) // 2, 1
    val ❤ :Int
    val :Int
    x
    x

    View Slide

  51. ModelState
    store
    Intent
    x
    View

    Model
    M
    data class UpvoteModel(val hearts:Int, val thumbs:Int)
    newState = oldState.copy(thumbs = thumbs + 1) // 0, 1
    newState = newState.copy(hearts = hearts + 1) // 1, 1
    newState = newState.copy(hearts = hearts + 1) // 2, 1
    val ❤ :Int
    val :Int
    x
    x

    View Slide

  52. ModelState
    store
    Intent
    x
    View

    Model
    M
    data class UpvoteModel(val hearts:Int, val thumbs:Int)
    interface Intent {x
    fun reduce(oldState: T): T
    }x
    val ❤ :Int
    val :Int
    x
    x

    View Slide

  53. ModelState
    store
    Intent
    x
    View

    Model
    M
    data class UpvoteModel(val hearts:Int, val thumbs:Int)
    interface Intent {x
    fun reduce(oldState: T): T
    }x
    class AddHeart():Intent {y
    override fun reduce(oldState: UpvoteModel)_=
    oldState.copy(hearts =_oldState.hearts_+ 1)
    }y
    val ❤ :Int
    val :Int
    x
    x

    View Slide

  54. ModelState
    store
    Intent
    x
    View

    Model
    M
    data class UpvoteModel(val hearts:Int, val thumbs:Int)
    fun_toIntent(viewEvent: MainViewEvent):Intent_{
    return when (viewEvent) {z
    MainViewEvent.LoveItClick ->_AddHeart()
    MainViewEvent.ThumbsUpClick ->_AddThumb()
    }z
    }z2
    val ❤ :Int
    val :Int
    x
    x

    View Slide

  55. interface IntentFactory {
    fun_process(viewEvent:E)
    }
    object MainViewIntentFactory : IntentFactory {
    override fun_process(viewEvent: MainViewEvent)_{
    UpvoteModelStore.process(toIntent(viewEvent))
    }x
    private fun_toIntent(viewEvent: MainViewEvent):Intent_{
    return when (viewEvent) {z
    MainViewEvent.LoveItClick ->_AddHeart()
    MainViewEvent.ThumbsUpClick ->_AddThumb()
    }z
    }z2
    }
    Intent
    x
    View

    View Slide

  56. Intent
    x
    interface_ModelStore {
    fun process(intent: Intent)
    fun modelState(): Observable
    }
    override fun_process(viewEvent: MainViewEvent)_{
    UpvoteModelStore.process(toIntent(viewEvent))
    }x
    ModelStore
    RxModelStore
    UpvoteModelStore
    (Don’t mind me. My
    turn is coming up soon
    too.)
    Model
    M
    val ❤ :Int
    val :Int
    ModelState
    store
    + +
    +

    View Slide

  57. Intent
    x
    interface_ModelStore_{
    fun process(intent: Intent)
    fun modelState(): Observable
    }x
    ModelStore
    RxModelStore
    UpvoteModelStore
    (Don’t mind me. My
    turn is coming up soon
    too.)
    Model
    M
    val ❤ :Int
    val :Int
    ModelState
    store
    + +
    +

    View Slide

  58. +❤ +❤
    +
    Intent
    x

    interface_ModelStore_{
    fun process(intent: Intent)
    fun modelState(): Observable
    }x
    ModelState
    store
    Hi again!

    View Slide

  59. PublishRelay.accept(intent)
    +❤ +❤
    +
    Intent
    x

    import com.jakewharton.rxrelay2.PublishRelay
    ModelState
    store
    fun process(intent: Intent) = intents.accept(intent)

    View Slide

  60. PublishRelay.accept(intent)
    +❤ +❤
    +
    Intent
    x

    import com.jakewharton.rxrelay2.PublishRelay
    ModelState
    store

    View Slide

  61. PublishRelay.accept(intent)
    +❤ +❤
    +
    Intent
    x

    import com.jakewharton.rxrelay2.PublishRelay
    ModelState
    store

    View Slide

  62. PublishRelay.accept(intent)
    +❤ +❤
    +
    Intent
    x

    import com.jakewharton.rxrelay2.PublishRelay
    ModelState
    store

    View Slide

  63. PublishRelay.accept(intent)
    +❤ +❤
    +
    Intent
    x

    scan { , -> .reduce( ) }
    scan { , -> .reduce( ) }
    ModelState
    store

    View Slide

  64. PublishRelay.accept(intent)
    +❤ +❤
    +
    Intent
    x

    0❤, 0
    0❤, 0
    scan { , -> .reduce( ) }
    scan { , -> .reduce( ) }
    ModelState
    store

    View Slide

  65. PublishRelay.accept(intent)
    +❤ +❤
    +
    Intent
    x

    0❤, 1
    0❤, 0
    0❤, 0
    scan { , -> .reduce( ) }
    scan { , -> .reduce( ) }
    ModelState
    store

    View Slide

  66. PublishRelay.accept(intent)
    +❤ +❤
    +
    Intent
    x

    0❤, 1
    0❤, 0
    0❤, 0
    1❤, 1
    scan { , -> .reduce( ) }
    scan { , -> .reduce( ) }
    ModelState
    store

    View Slide

  67. PublishRelay.accept(intent)
    +❤ +❤
    +
    Intent
    x

    0❤, 1
    0❤, 0
    0❤, 0
    1❤, 1 1❤, 2
    scan { , -> .reduce( ) }
    scan { , -> .reduce( ) }
    ModelState
    store

    View Slide

  68. View Slide

  69. View Slide

  70. replay(1)
    connect()
    PublishRelay.accept(intent)
    +❤ +❤
    +
    Intent
    x

    0❤, 1
    0❤, 0
    0❤, 0
    1❤, 1 1❤, 2
    scan { , -> .reduce( ) }
    scan { , -> .reduce( ) }
    ModelState
    store

    View Slide

  71. open_class_RxModelStore(startingState: S)_:_ModelStore_{
    private val intents = PublishRelay.create>()
    private val store = intents
    .observeOn(AndroidSchedulers.mainThread())
    .scan(startingState)_{ oldState, intent_->_intent.reduce(oldState) }
    .replay(1)
    .apply {_connect()_}
    override_fun_process(intent: Intent) =_intents.accept(intent)
    override_fun_modelState(): Observable =_store
    }a
    ModelStore
    RxModelStore
    UpvoteModelStore

    View Slide

  72. interface_ModelStore_{
    fun process(intent: Intent)
    fun modelState(): Observable
    }x
    object UpvoteModelStore :_RxModelStore(UpvoteModel(0, 0))
    open_class_RxModelStore(startingState: S)_:_ModelStore_{
    private val intents = PublishRelay.create>()
    private val store = intents
    .observeOn(AndroidSchedulers.mainThread())
    .scan(startingState)_{ oldState, intent_->_intent.reduce(oldState) }
    .replay(1)
    .apply {_connect()_}
    override_fun_process(intent: Intent) =_intents.accept(intent)
    override_fun_modelState(): Observable =_store
    }a
    ModelStore
    RxModelStore
    UpvoteModelStore
    Hi again. Btw, I’m
    not actually abstract.
    He just goofed up the
    UML…

    View Slide

  73. object UpvoteModelStore :_RxModelStore(UpvoteModel(0, 0))
    UpvoteModelStore

    View Slide

  74. Intent
    x
    View

    Model
    M
    val ❤ :Int
    val :Int
    x
    x
    x
    ModelState
    store
    object UpvoteModelStore

    View Slide

  75. Intent
    x
    View

    Model
    M
    val ❤ :Int
    val :Int
    x
    x
    x
    ModelState
    store
    object UpvoteModelStore

    View Slide

  76. Intent
    x
    View

    Model
    M
    val ❤ :Int
    val :Int
    x
    x
    x
    ModelState
    store
    interface_ModelStore_{
    fun process(intent: Intent)
    fun modelState(): Observable
    }x
    fun modelState(): Observable
    object UpvoteModelStore

    View Slide

  77. View
    Model
    M
    UpvoteModelStore.modelState().subscribe { model ->
    counterTextView.text =
    }
    0❤, 1
    0❤, 0 1❤, 1 1❤, 2

    View Slide

  78. https://github.com/kanawish/upvote

    View Slide

  79. Live demo

    View Slide

  80. Live demo

    View Slide

  81. https://github.com/kanawish/android-mvi-sample

    View Slide

  82. https://github.com/kanawish/android-mvi-sample

    View Slide

  83. https://github.com/kanawish/android-mvi-sample

    View Slide

  84. https://github.com/kanawish/android-mvi-sample

    View Slide

  85. https://github.com/kanawish/android-mvi-sample

    View Slide

  86. https://github.com/kanawish/android-mvi-sample

    View Slide

  87. https://github.com/kanawish/android-mvi-sample

    View Slide

  88. https://github.com/kanawish/android-mvi-sample

    View Slide

  89. ♻ ☠

    View Slide

  90. *
    https://github.com/kanawish/android-mvi-sample

    View Slide

  91. https://github.com/kanawish/android-mvi-sample

    View Slide

  92. https://play.google.com/store/apps/details?id=com.shopify.logomaker.hatchful

    View Slide

  93. Photo source info here
    Thank you!
    https://github.com/kanawish/android-mvi-sample
    https://github.com/kanawish/upvote

    View Slide