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

Coroutine + Flow = MVI ❤ by Étienne Caron

Coroutine + Flow = MVI ❤ by Étienne Caron

Coroutine + Flow = MVI ❤
Level: Intermediate
by Étienne Caron, Google Developer Expert, Shopify

Managing state in Android applications can be painful. Over the years, various architectural patterns have evolved to try and tame it: MVC, MVP, and MVVM. MVI (Model-View-Intent) is an evolution of these patterns. Thanks to Immutable State management and Unidirectional Data Flow, we can finally say goodbye to race conditions and rogue mutations. Combine the power of MVI with Kotlin Coroutines alongside the recently released Kotlin Flows library. The result? Real magic. No RxJava incantations required! In this session, you’ll learn how to build a Model-View-Intent (MVI) Android App, using both Kotlin Coroutines and Kotlin Flows. You’ll walk away understanding the core principles of this pattern, as well as its key benefits. Get ready to fall for MVI❤!
https://gdgmontreal.com/2019/07/26/kotlin-everywhere/

GDG Montreal

August 28, 2019
Tweet

More Decks by GDG Montreal

Other Decks in Programming

Transcript

  1. Coroutine + Flow

    View Slide

  2. Coroutine + Flow = MVI ❤

    View Slide

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

    View Slide

  4. View Slide

  5. https://xkcd.com/1312

    View Slide

  6. https://xkcd.com/1312

    View Slide

  7. View Slide

  8. !

    View Slide

  9. "

    View Slide

  10. private val disposables = CompositeDisposable()
    disposables += streamA().subscribe(1)1
    disposables += streamB().subscribe(1)1
    disposables.clear()

    View Slide

  11. Photo source info here
    Coroutines & Flow

    View Slide

  12. Photo source info here
    Coroutines & Flow

    View Slide

  13. Photo source info here
    Coroutines & Flow
    "

    View Slide

  14. Photo source info here
    Coroutines & Flow
    !❓

    View Slide

  15. output
    input
    input
    output
    user
    device

    View Slide

  16. output
    input
    input
    output
    user
    device

    View Slide

  17. output
    input
    input
    output
    user
    device

    View Slide

  18. output
    input
    input
    output
    user
    device

    View Slide

  19. input
    output
    input output
    user
    device

    View Slide

  20. Model
    Intent
    View

    View Slide

  21. Intent
    View
    View

    View Slide

  22. Intent
    View

    View Slide

  23. Intent
    View

    View Slide

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

    View Slide


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


    View Slide

  26. View
    ❤ ❤
    fun View.clicks(): Flow = callbackFlow {
    val listener = View.OnClickListener { offer(Unit) }
    setOnClickListener(listener)
    awaitClose {
    setOnClickListener(null)
    }
    }

    View Slide

  27. View
    ❤ ❤
    fun View.clicks(): Flow = callbackFlow {
    val listener = View.OnClickListener { offer(Unit) }
    setOnClickListener(listener)
    awaitClose {
    setOnClickListener(null)
    }
    }

    View Slide

  28. View
    ❤ ❤
    fun View.clicks(): Flow = callbackFlow {
    val listener = View.OnClickListener { offer(Unit) }
    setOnClickListener(listener)
    awaitClose {
    setOnClickListener(null)
    }
    }

    View Slide

  29. View
    ❤ ❤
    fun View.clicks(): Flow = callbackFlow {
    val listener = View.OnClickListener { offer(Unit) }
    setOnClickListener(listener)
    awaitClose {
    setOnClickListener(null)
    }
    }
    ~

    View Slide

  30. ❤ ❤
    View.clicks():Flow
    View
    Flow

    View Slide

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

    Flow
    ❤ ❤

    View Slide

  32. View
    heartButton.clicks()
    map { MainViewEvent.LoveItClick }
    ❤ ❤
    Flow
    ❤ ❤

    View Slide

  33. val flows = listOf(
    heartButton.clicks().map { MainViewEvent.LoveItClick },
    thumbButton.clicks().map { MainViewEvent.ThumbsUpClick }
    )x
    return flows.asFlow().flattenMerge(flows.size)_
    Flow
    View
    ❤ ❤

    View Slide

  34. View
    ❤ ❤
    class MainActivity {__
    override_fun_viewEvents(): Flow_{
    val flows = listOf(
    heartButton.clicks().map { MainViewEvent.LoveItClick },
    thumbButton.clicks().map { MainViewEvent.ThumbsUpClick }
    )x
    return flows.asFlow().flattenMerge(flows.size)_
    }_
    }__

    View Slide

  35. View
    ❤ ❤
    class MainActivity {__
    override_fun_viewEvents(): Flow_{
    val flows = listOf(
    heartButton.clicks().map { MainViewEvent.LoveItClick },
    thumbButton.clicks().map { MainViewEvent.ThumbsUpClick }
    )x
    return flows.asFlow().flattenMerge(flows.size)_
    }_
    }__

    View Slide

  36. View
    ❤ ❤
    class MainActivity {__
    override_fun_viewEvents(): Flow_{
    val flows = listOf(
    heartButton.clicks().map { MainViewEvent.LoveItClick },
    thumbButton.clicks().map { MainViewEvent.ThumbsUpClick }
    )x
    return flows.asFlow().flattenMerge(flows.size)_
    }_
    }__

    View Slide

  37. class MainActivity {__
    override_fun_viewEvents(): Flow_{
    val flows = listOf(
    heartButton.clicks().map { MainViewEvent.LoveItClick },
    thumbButton.clicks().map { MainViewEvent.ThumbsUpClick }
    )x
    return flows.asFlow().flattenMerge(flows.size)_
    }_
    }__
    View
    ❤ ❤

    View Slide

  38. /**
    * This allows us to group all the_viewEvents from
    * one view in a single Flow.
    */
    interface ViewEventFlow {
    funxviewEvents():xFlow
    }x
    class MainActivity_: ViewEventFlow {x_
    override_fun_viewEvents(): Flow_{
    val flows = listOf(
    heartButton.clicks().map { MainViewEvent.LoveItClick },
    thumbButton.clicks().map { MainViewEvent.ThumbsUpClick }
    )x
    return flows.asFlow().flattenMerge(flows.size)_
    }_
    }__
    View
    ❤ ❤

    View Slide

  39. class MainActivity_: ViewEventFlow {x_
    val_scope: CoroutineScope_= MainScope()
    override_fun_onCreate(savedInstanceState: Bundle?)_{
    super.onCreate(savedInstanceState)
    // ...
    }1
    override_fun_onDestroy()_{
    super.onDestroy()
    scope.cancel()
    }2
    override_fun_viewEvents(): Flow_{
    // ...
    }_
    }__
    View Intent

    View Slide

  40. class MainActivity_: ViewEventFlow {x_
    val_scope: CoroutineScope_= MainScope()
    override_fun_onCreate(savedInstanceState: Bundle?)_{
    super.onCreate(savedInstanceState)
    // ...
    }1
    override_fun_onDestroy()_{
    super.onDestroy()
    scope.cancel()
    }2
    override_fun_viewEvents(): Flow_{
    // ...
    }_
    }__
    View Intent

    View Slide

  41. class MainActivity_: ViewEventFlow {x_
    val_scope: CoroutineScope_= MainScope()
    override_fun_onCreate(savedInstanceState: Bundle?)_{
    super.onCreate(savedInstanceState)
    // ...
    scope.launch_{
    viewEvents()
    .onEach { ?_}
    .collect()
    }
    }1
    override_fun_onDestroy()_{
    super.onDestroy()
    scope.cancel()
    }2
    override_fun_viewEvents(): Flow_{
    // ...
    }_
    }__
    View Intent

    View Slide

  42. class MainActivity_: ViewEventFlow {x_
    val_scope: CoroutineScope_= MainScope()
    override_fun_onCreate(savedInstanceState: Bundle?)_{
    super.onCreate(savedInstanceState)
    // ...
    scope.launch_{
    viewEvents()
    .onEach { ?_}
    .collect()
    }
    }1
    override_fun_onDestroy()_{
    super.onDestroy()
    scope.cancel()
    }2
    override_fun_viewEvents(): Flow_{
    // ...
    }_
    }__
    View Intent
    ~

    View Slide

  43. class MainActivity_: ViewEventFlow {x_
    val_scope: CoroutineScope_= MainScope()
    override_fun_onCreate(savedInstanceState: Bundle?)_{
    super.onCreate(savedInstanceState)
    // ...
    scope.launch_{
    viewEvents()
    .onEach { ?_}
    .collect()
    }
    }1
    override_fun_onDestroy()_{
    super.onDestroy()
    scope.cancel()
    }2
    override_fun_viewEvents(): Flow_{
    // ...
    }_
    }__
    View Intent
    ~

    View Slide

  44. class MainActivity_: ViewEventFlow {x_
    val_scope: CoroutineScope_= MainScope()
    override_fun_onCreate(savedInstanceState: Bundle?)_{
    super.onCreate(savedInstanceState)
    // ...
    scope.launch_{
    viewEvents()
    .onEach { ?_}
    .collect()
    }
    }1
    override_fun_onDestroy()_{
    super.onDestroy()
    scope.cancel()
    }2
    override_fun_viewEvents(): Flow_{
    // ...
    }_
    }__
    View Intent
    ~

    View Slide

  45. class MainActivity_: ViewEventFlow {x_
    val_scope: CoroutineScope_= MainScope()
    override_fun_onCreate(savedInstanceState: Bundle?)_{
    super.onCreate(savedInstanceState)
    // ...
    scope.launch_{
    viewEvents()
    .onEach { ?_}
    .collect()
    }
    }1
    override_fun_onDestroy()_{
    super.onDestroy()
    scope.cancel()
    }2
    override_fun_viewEvents(): Flow_{
    // ...
    }_
    }__
    View Intent
    ~

    View Slide

  46. class MainActivity_: ViewEventFlow {x_
    val_scope: CoroutineScope_= MainScope()
    override_fun_onCreate(savedInstanceState: Bundle?)_{
    super.onCreate(savedInstanceState)
    // ...
    viewEvents()
    .onEach { ? }
    .launchIn(scope)
    }1
    override_fun_onDestroy()_{
    super.onDestroy()
    scope.cancel()
    }2
    override_fun_viewEvents(): Flow_{
    // ...
    }_
    }__ View Intent

    View Slide

  47. class MainActivity_: ViewEventFlow {x_
    val_scope: CoroutineScope_= MainScope()
    override_fun_onCreate(savedInstanceState: Bundle?)_{
    super.onCreate(savedInstanceState)
    // ...
    viewEvents()
    .onEach { event -> ? }
    .launchIn(scope)
    }1
    override_fun_onDestroy()_{
    super.onDestroy()
    scope.cancel()
    }2
    override_fun_viewEvents(): Flow_{
    // ...
    }_
    }__ View Intent

    View Slide

  48. View Intent

    class MainActivity_: ViewEventFlow {x_
    val_scope: CoroutineScope_= MainScope()
    override_fun_onCreate(savedInstanceState: Bundle?)_{
    super.onCreate(savedInstanceState)
    // ...
    viewEvents()
    .onEach { event ->
    MainViewIntentFactory.process(event)
    }
    .launchIn(scope)
    }1
    override_fun_onDestroy()_{
    super.onDestroy()
    scope.cancel()
    }2
    override_fun_viewEvents(): Flow_{
    // ...
    }_
    }__
    ~

    View Slide

  49. ModelState
    store
    x
    Intent
    View


    Model

    View Slide

  50. ModelState
    store
    Model
    val ❤:Int
    val :Int

    View Slide

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

    View Slide

  52. ModelState
    store
    Intent
    View

    Model
    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

    View Slide

  53. ModelState
    store
    Intent
    View

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

    View Slide

  54. ModelState
    store
    Intent
    View

    Model
    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

    View Slide

  55. ModelState
    store
    Intent
    View

    Model
    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

    View Slide

  56. interface IntentFactory_{
    suspend fun_process(viewEvent:E)
    }y
    object MainViewIntentFactory :-IntentFactory_{
    override suspend 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
    }w
    Intent
    View

    ~

    View Slide

  57. Intent
    View

    ~
    interface IntentFactory_{
    suspend fun_process(viewEvent:E)
    }y
    object MainViewIntentFactory :-IntentFactory_{
    override suspend 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
    }w

    View Slide

  58. Intent
    View

    ~
    interface IntentFactory_{
    suspend fun_process(viewEvent:E)
    }y
    object MainViewIntentFactory :-IntentFactory_{
    override suspend 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
    }w

    View Slide

  59. Intent
    View

    ~
    interface IntentFactory_{
    suspend fun_process(viewEvent:E)
    }y
    object MainViewIntentFactory :-IntentFactory_{
    override suspend 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
    }w

    View Slide

  60. ModelStore
    RxModelStore
    UpvoteModelStore
    FlowModelStore
    Intent
    interface_ModelStore_{y
    suspend_fun process(intent: Intent)
    fun modelState(): Flow
    }yy
    override suspend fun_process(viewEvent: MainViewEvent)_{
    UpvoteModelStore.process(toIntent(viewEvent))
    }x
    (Don’t mind me. My
    turn is coming up soon
    too.)
    Model
    val ❤ :Int
    val :Int
    ModelState
    store
    + +
    +

    View Slide

  61. Intent
    interface_ModelStore_{y
    suspend_fun process(intent: Intent)
    fun modelState(): Flow
    }yy
    Model
    val ❤ :Int
    val :Int
    ModelState
    store
    + +
    +
    ModelStore
    RxModelStore
    UpvoteModelStore
    FlowModelStore
    (Don’t mind me. My
    turn is coming up soon
    too.)

    View Slide

  62. x
    +❤ +❤
    +
    Intent

    ModelState
    store
    Hi again!
    interface_ModelStore_{y
    suspend_fun process(intent: Intent)
    fun modelState(): Flow
    }yy

    View Slide

  63. x
    val intents = Channel>()
    +❤ +❤
    +
    Intent

    ModelState
    store
    suspend fun process(intent: Intent) {
    intents.send(intent)
    }
    ~

    View Slide

  64. x
    val intents = Channel>()
    +❤ +❤
    +
    Intent

    ModelState
    store
    suspend fun process(intent: Intent) {
    intents.send(intent)
    }
    ~
    val store = ConflatedBroadcastChannel(startingState)
    override fun modelState(): Flow {
    return store.asFlow()
    }

    View Slide

  65. val intents = Channel>()
    +❤ +❤
    +
    Intent

    ModelState
    store
    ❓❓❓
    val store = ConflatedBroadcastChannel(startingState)

    View Slide

  66. +❤ +❤
    +
    Intent

    0❤, 0
    0❤, 0
    1❤, 1 2❤, 1
    ModelState
    store
    store.offer(intents.receive().reduce(store.value))
    val store = ConflatedBroadcastChannel(startingState)
    val intents = Channel>()
    0❤, 1

    View Slide

  67. open class FlowModelStore(startingState: S) : ModelStore {
    private val scope = MainScope()
    private val intents = Channel>()
    private val store = ConflatedBroadcastChannel(startingState)
    init {
    // Reduce from MainScope()
    scope.launch {
    while (isActive) store.offer(intents.receive().reduce(store.value))
    }
    }
    // Could be called from any coroutine scope/context.
    override suspend fun process(intent: Intent) {
    intents.send(intent)
    }
    override fun modelState(): Flow {
    return store.asFlow()
    }
    fun close() {
    intents.close()
    store.close()
    scope.cancel()
    }
    }
    ModelStore
    RxModelStore
    UpvoteModelStore
    FlowModelStore

    View Slide

  68. interface_ModelStore_{
    suspend fun process(intent: Intent)
    fun modelState(): Flow
    }x
    object UpvoteModelStore :_
    FlowModelStore(UpvoteModel(0, 0))
    open class FlowModelStore(startingState: S) : ModelStore {
    // ...
    }
    ModelStore
    FlowModelStore
    UpvoteModelStore

    View Slide

  69. object UpvoteModelStore :_
    FlowModelStore(UpvoteModel(0, 0))
    UpvoteModelStore

    View Slide

  70. Intent
    View

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

    View Slide

  71. Intent
    View

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

    View Slide

  72. View
    Model
    fun Flow.forCounterTextView() =
    onEach { model ->
    counterTextView.text =
    }
    // Input(s)
    UpvoteModelStore
    .modelState()
    .forCounterTextView()
    .launchIn(scope)
    0❤, 1
    0❤, 0 1❤, 1 2❤, 1

    View Slide

  73. Live demo?

    View Slide

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

    View Slide

  75. Photo source info here
    Thank you!
    https://github.com/kanawish/upvote

    View Slide