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/

1b77dd441f657f5aefb3e21283b252e6?s=128

GDG Montreal

August 28, 2019
Tweet

Transcript

  1. Coroutine + Flow

  2. Coroutine + Flow = MVI ❤

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

  4. None
  5. https://xkcd.com/1312

  6. https://xkcd.com/1312

  7. None
  8. !

  9. "

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

    streamB().subscribe(1)1 disposables.clear() ♻
  11. Photo source info here Coroutines & Flow

  12. Photo source info here Coroutines & Flow ♻

  13. Photo source info here Coroutines & Flow "

  14. Photo source info here Coroutines & Flow !❓

  15. output input input output user device

  16. output input input output user device

  17. output input input output user device

  18. output input input output user device

  19. input output input output user device

  20. Model Intent View

  21. Intent View View

  22. Intent View

  23. Intent View

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

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

    :_MainViewEvent() }_ MainViewEvent ThumbsUpClick LoveItClick + +❤ ❤ ❤
  26. View ❤ ❤ fun View.clicks(): Flow<Unit> = callbackFlow { val

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

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

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

    listener = View.OnClickListener { offer(Unit) } setOnClickListener(listener) awaitClose { setOnClickListener(null) } } ~
  30. ❤ ❤ View.clicks():Flow<Unit> View Flow<MainViewEvent>

  31. View thumbButton.clicks() map { MainViewEvent.ThumbsUpClick } Flow<MainViewEvent> ❤ ❤

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

  33. val flows = listOf( heartButton.clicks().map { MainViewEvent.LoveItClick }, thumbButton.clicks().map {

    MainViewEvent.ThumbsUpClick } )x return flows.asFlow().flattenMerge(flows.size)_ Flow<MainViewEvent> View ❤ ❤
  34. View ❤ ❤ class MainActivity {__ override_fun_viewEvents(): Flow<MainViewEvent>_{ val flows

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

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

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

    { MainViewEvent.LoveItClick }, thumbButton.clicks().map { MainViewEvent.ThumbsUpClick } )x return flows.asFlow().flattenMerge(flows.size)_ }_ }__ View ❤ ❤
  38. /** * This allows us to group all the_viewEvents from

    * one view in a single Flow. */ interface ViewEventFlow<E> { funxviewEvents():xFlow<E> }x class MainActivity_: ViewEventFlow<MainViewEvent> {x_ override_fun_viewEvents(): Flow<MainViewEvent>_{ val flows = listOf( heartButton.clicks().map { MainViewEvent.LoveItClick }, thumbButton.clicks().map { MainViewEvent.ThumbsUpClick } )x return flows.asFlow().flattenMerge(flows.size)_ }_ }__ View ❤ ❤
  39. class MainActivity_: ViewEventFlow<MainViewEvent> {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<MainViewEvent>_{ // ... }_ }__ View Intent
  40. class MainActivity_: ViewEventFlow<MainViewEvent> {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<MainViewEvent>_{ // ... }_ }__ View Intent
  41. class MainActivity_: ViewEventFlow<MainViewEvent> {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<MainViewEvent>_{ // ... }_ }__ View Intent
  42. class MainActivity_: ViewEventFlow<MainViewEvent> {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<MainViewEvent>_{ // ... }_ }__ View Intent ~
  43. class MainActivity_: ViewEventFlow<MainViewEvent> {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<MainViewEvent>_{ // ... }_ }__ View Intent ~
  44. class MainActivity_: ViewEventFlow<MainViewEvent> {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<MainViewEvent>_{ // ... }_ }__ View Intent ~
  45. class MainActivity_: ViewEventFlow<MainViewEvent> {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<MainViewEvent>_{ // ... }_ }__ View Intent ~
  46. class MainActivity_: ViewEventFlow<MainViewEvent> {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<MainViewEvent>_{ // ... }_ }__ View Intent
  47. class MainActivity_: ViewEventFlow<MainViewEvent> {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<MainViewEvent>_{ // ... }_ }__ View Intent
  48. View Intent class MainActivity_: ViewEventFlow<MainViewEvent> {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<MainViewEvent>_{ // ... }_ }__ ~
  49. ModelState store x Intent View ❓ Model

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

  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.)
  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 ❓
  53. ModelState store Intent View Model data class UpvoteModel(val hearts:Int, val

    thumbs:Int) interface Intent<T> {x fun reduce(oldState: T): T }x val ❤ :Int val :Int
  54. ModelState store Intent View Model data class UpvoteModel(val hearts:Int, val

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

    thumbs:Int) fun_toIntent(viewEvent: MainViewEvent):Intent<UpvoteModel>_{ return when (viewEvent) {z MainViewEvent.LoveItClick ->_AddHeart() MainViewEvent.ThumbsUpClick ->_AddThumb() }z }z2 val ❤ :Int val :Int
  56. interface IntentFactory<E>_{ suspend fun_process(viewEvent:E) }y object MainViewIntentFactory :-IntentFactory<MainViewEvent>_{ override suspend

    fun_process(viewEvent: MainViewEvent)_{ UpvoteModelStore.process(toIntent(viewEvent)) }x private fun_toIntent(viewEvent: MainViewEvent):Intent<UpvoteModel>_{ return when (viewEvent) {z MainViewEvent.LoveItClick ->_AddHeart() MainViewEvent.ThumbsUpClick ->_AddThumb() }z }z2 }w Intent View ~
  57. Intent View ~ interface IntentFactory<E>_{ suspend fun_process(viewEvent:E) }y object MainViewIntentFactory

    :-IntentFactory<MainViewEvent>_{ override suspend fun_process(viewEvent: MainViewEvent)_{ UpvoteModelStore.process(toIntent(viewEvent)) }x private fun_toIntent(viewEvent: MainViewEvent):Intent<UpvoteModel>_{ return when (viewEvent) {z MainViewEvent.LoveItClick ->_AddHeart() MainViewEvent.ThumbsUpClick ->_AddThumb() }z }z2 }w
  58. Intent View ~ interface IntentFactory<E>_{ suspend fun_process(viewEvent:E) }y object MainViewIntentFactory

    :-IntentFactory<MainViewEvent>_{ override suspend fun_process(viewEvent: MainViewEvent)_{ UpvoteModelStore.process(toIntent(viewEvent)) }x private fun_toIntent(viewEvent: MainViewEvent):Intent<UpvoteModel>_{ return when (viewEvent) {z MainViewEvent.LoveItClick ->_AddHeart() MainViewEvent.ThumbsUpClick ->_AddThumb() }z }z2 }w
  59. Intent View ~ interface IntentFactory<E>_{ suspend fun_process(viewEvent:E) }y object MainViewIntentFactory

    :-IntentFactory<MainViewEvent>_{ override suspend fun_process(viewEvent: MainViewEvent)_{ UpvoteModelStore.process(toIntent(viewEvent)) }x private fun_toIntent(viewEvent: MainViewEvent):Intent<UpvoteModel>_{ return when (viewEvent) {z MainViewEvent.LoveItClick ->_AddHeart() MainViewEvent.ThumbsUpClick ->_AddThumb() }z }z2 }w
  60. ModelStore RxModelStore UpvoteModelStore FlowModelStore Intent interface_ModelStore<S>_{y suspend_fun process(intent: Intent<S>) fun

    modelState(): Flow<S> }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 + + +
  61. Intent interface_ModelStore<S>_{y suspend_fun process(intent: Intent<S>) fun modelState(): Flow<S> }yy Model

    val ❤ :Int val :Int ModelState store + + + ModelStore RxModelStore UpvoteModelStore FlowModelStore (Don’t mind me. My turn is coming up soon too.)
  62. x +❤ +❤ + Intent ModelState store Hi again! interface_ModelStore<S>_{y

    suspend_fun process(intent: Intent<S>) fun modelState(): Flow<S> }yy
  63. x val intents = Channel<Intent<S>>() +❤ +❤ + Intent ModelState

    store suspend fun process(intent: Intent<S>) { intents.send(intent) } ~
  64. x val intents = Channel<Intent<S>>() +❤ +❤ + Intent ModelState

    store suspend fun process(intent: Intent<S>) { intents.send(intent) } ~ val store = ConflatedBroadcastChannel(startingState) override fun modelState(): Flow<S> { return store.asFlow() }
  65. val intents = Channel<Intent<S>>() +❤ +❤ + Intent ModelState store

    ❓❓❓ val store = ConflatedBroadcastChannel(startingState)
  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<Intent<S>>() 0❤, 1
  67. open class FlowModelStore<S>(startingState: S) : ModelStore<S> { private val scope

    = MainScope() private val intents = Channel<Intent<S>>() 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<S>) { intents.send(intent) } override fun modelState(): Flow<S> { return store.asFlow() } fun close() { intents.close() store.close() scope.cancel() } } ModelStore RxModelStore UpvoteModelStore FlowModelStore
  68. interface_ModelStore<S>_{ suspend fun process(intent: Intent<S>) fun modelState(): Flow<S> }x object

    UpvoteModelStore :_ FlowModelStore<UpvoteModel>(UpvoteModel(0, 0)) open class FlowModelStore<S>(startingState: S) : ModelStore<S> { // ... } ModelStore FlowModelStore UpvoteModelStore
  69. object UpvoteModelStore :_ FlowModelStore<UpvoteModel>(UpvoteModel(0, 0)) UpvoteModelStore

  70. Intent View Model val ❤ :Int val :Int ModelState store

    object UpvoteModelStore
  71. Intent View Model val ❤ :Int val :Int ModelState store

    interface_ModelStore<S>_{ fun process(intent: Intent<S>) fun modelState(): Flow<S> }x fun modelState(): Flow<S> object UpvoteModelStore
  72. View Model fun Flow<UpvoteModel>.forCounterTextView() = onEach { model -> counterTextView.text

    = } // Input(s) UpvoteModelStore .modelState() .forCounterTextView() .launchIn(scope) 0❤, 1 0❤, 0 1❤, 1 2❤, 1
  73. Live demo?

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

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