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. !

  2. "

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

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

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

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

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

    listener = View.OnClickListener { offer(Unit) } setOnClickListener(listener) awaitClose { setOnClickListener(null) } } ~
  8. val flows = listOf( heartButton.clicks().map { MainViewEvent.LoveItClick }, thumbButton.clicks().map {

    MainViewEvent.ThumbsUpClick } )x return flows.asFlow().flattenMerge(flows.size)_ Flow<MainViewEvent> View ❤ ❤
  9. 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)_ }_ }__
  10. 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)_ }_ }__
  11. 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)_ }_ }__
  12. 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 ❤ ❤
  13. /** * 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 ❤ ❤
  14. 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
  15. 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
  16. 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
  17. 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 ~
  18. 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 ~
  19. 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 ~
  20. 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 ~
  21. 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
  22. 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
  23. 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>_{ // ... }_ }__ ~
  24. 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.)
  25. 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 ❓
  26. 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
  27. 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
  28. 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
  29. 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 ~
  30. 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
  31. 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
  32. 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
  33. 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 + + +
  34. 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.)
  35. x +❤ +❤ + Intent ModelState store Hi again! interface_ModelStore<S>_{y

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

    store suspend fun process(intent: Intent<S>) { intents.send(intent) } ~
  37. 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() }
  38. val intents = Channel<Intent<S>>() +❤ +❤ + Intent ModelState store

    ❓❓❓ val store = ConflatedBroadcastChannel(startingState)
  39. +❤ +❤ + 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
  40. 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
  41. 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
  42. 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
  43. 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