Flowing Things, not so strange in the MVI world

A3958eeb9a7f402b134c0c017d6614ee?s=47 ragdroid
August 27, 2019

Flowing Things, not so strange in the MVI world

Migrating an Rx MVI pattern completely to coroutines using Flow. In the MVI world, there was a missing piece from the coroutines framework and due to this, it always felt strange to completely adopt coroutines. Recently, with the introduction of Co-routines Flow library, things are not so strange anymore. In this talk we will have a look at Coroutines Flow library, its need and how it compares with the reactive world. We will then learn to migrate an Rx MVI pattern to use coroutines Flow.

A3958eeb9a7f402b134c0c017d6614ee?s=128

ragdroid

August 27, 2019
Tweet

Transcript

  1. FLOWING THINGS Not so strange in the MVI world

  2. Garima Jain @ragdroid

  3. @ragdroid #droidconNYC Who?

  4. Who? • Rx background • Coroutines • MVI • Stranger

    Things?
  5. @ragdroid #droidconNYC What?

  6. What? • Chapter One : Coroutines and Flow • Chapter

    Two : Channels and Flows • Chapter Three : MVI • Chapter Four : Rx to Flow • Summary
  7. CHAPTER ONE THE FLOW

  8. Coroutines • Lightweight thread • Run computations without blocking •

    Can be suspended
  9. Coroutines • Coroutine Scope ★ ViewModel Scope ★ Lifecycle Scope

    • Coroutine Builders ★ launch { } - fire and forget ★ async { } - await() result ★ within a scope, Structured Concurrency
  10. Channels • Communication between different coroutines • Similar to BlockingQueue

  11. Flow • Cold asynchronous stream that sequentially emits values •

    Utilizes coroutines and channels • Experimental Recently Stable APIs in 1.3.0
  12. Flow Builders ★ flowOf( ) ★ asFlow( ) ★ flow

    { } ★ channelFlow { }
  13. Flow Operators • Intermediate : ★ map, filter, take, zip,

    etc. • Terminal : ★ collect, single, reduce, etc.
  14. Flow Operators fun main() = runBlocking { val flow =

    flow { for (i in 1..10) { delay(500L) emit(i) } }.filter { it % 2 == 0 } flow.collect { println(it) } } 2 4 6 8 10
  15. Flow Constraints ★ Context Preservation ★ Exception Transparency

  16. Context Preservation val flowA = flowOf(1, 2, 3) .map {

    it + 1 } .flowOn(ctxA) val filtered = flowA .filter { it == 3 } withContext(Dispatchers.Main) { val result = filtered.single() myUi.text = result } https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/ // Will be executed in ctxA // Changes the upstream context: flowOf and map // Pure operator without a context yet // All non-encapsulated operators will be executed in Main: filter and single
  17. CHAPTER TWO CONSUMER, DO YOU COPY? Hot Channels and Cold

    Flows
  18. fun main() = runBlocking { val job = launch {

    produce { println(“We are hot") send(1) } } job.invokeOnCompletion { println("Completed") } delay(1000L) println("Done waiting") } Channels Done waiting Delay We are hot
  19. Channels fun main() = runBlocking { val job = launch

    { val hotChannel = produce { println(“We are hot") send(1) } channel.consumeEach { println(it) } } job.invokeOnCompletion { println("Completed") } delay(1000L) println("Done waiting") } 1 Completed Done waiting We are hot
  20. Flows fun main() = runBlocking { val job = launch

    { val coldFlow = flow { println(“We are cold”) emit(1) } } job.invokeOnCompletion { println("Completed") } delay(1000L) println("Done waiting") } Completed Done waiting
  21. Flows fun main() = runBlocking { val job = launch

    { val coldFlow = flow { println("We are cold") emit(1) } coldFlow.collect { println(it) } } job.invokeOnCompletion { println("Completed") } delay(1000L) println("Done waiting") } We are cold 1 Completed Done waiting
  22. Channel Gotchas • Channels are a pathway to a different

    dimension (Upside Down) • If we they are open, another dimension exists • Existence of another dimension could be dangerous • It could use up some heavy resources
  23. Channel Gotchas Easy to make mistakes

  24. Channel Gotchas Easy to make mistakes

  25. CHAPTER THREE THE BATTLE OF ACTIONS MVI

  26. Model View Intent

  27. Model View Intent State View Intention

  28. State View Intention User Intent State View Changes Updates Seen

    Interacts
  29. View ViewModel Data Intent newState Action Result previousState Reducer State

    View Intention State MVI on top of MVVM
  30. View ViewModel Data Intent newState Action Result previousState Reducer State

    View Intention State 4 3 5 1 2 MVI on top of MVVM
  31. State View Intention 1 3 2 4 5 Action Events

    Actions to Results Reduce Results to new State Plugging-in the pieces State stream
  32. CHAPTER FOUR VANISHING OF Rx FLOWABLES Migrating an Rx MVI

    pattern to use Flow
  33. Demo

  34. Actions sealed class MainAction { object PullToRefresh object LoadData data

    class LoadDescription(val characterId: Long) }
  35. Actions - ViewModel interface ViewModel { fun onAction(action: MainAction) ...

    } Rx
  36. Actions - ViewModel interface ViewModel { fun onAction(action: MainAction) ...

    } Flow
  37. Action Events viewModel.onAction(MainAction.LoadData) viewModel.onAction(..)

  38. viewModel.onAction(MainAction.LoadData) viewModel.onAction(..) refreshLayout.setOnRefreshListener { viewModel.onAction(MainAction.PullToRefresh) } Action Events

  39. viewModel.onAction(MainAction.LoadData) viewModel.onAction(..) override fun onCharacterDescriptionClicked(itemId: Long) { viewModel.onAction(MainAction.LoadDescription(itemId)) } refreshLayout.setOnRefreshListener

    { viewModel.onAction(MainAction.PullToRefresh) } Action Events
  40. State View Intention 1 Action Events 3 2 4 5

    Actions to Results Reduce Results to new State State stream Plugging-in the pieces
  41. State View Intention 1 Action Events 3 2 4 5

    Actions to Results Reduce Results to new State State stream Plugging-in the pieces
  42. State View Intention 1 Action Events 3 2 4 5

    Actions to Results Reduce Results to new State State stream Plugging-in the pieces same
  43. Actions to Results sealed class MainResult { object Loading object

    LoadingError data class LoadingComplete(val characters: List<CharacterMarvel>) object PullToRefreshing: MainResult() object PullToRefreshError data class PullToRefreshComplete(val characters: List<CharacterMarvel>) sealed class DescriptionResult(val characterId: Long) { data class DescriptionLoading(private val id: Long) data class DescriptionError(private val id: Long) data class DescriptionLoadComplete(private val id: Long, val description: String) } }
  44. View ViewModel Data Intent newState Action Result previousState Reducer State

    View Intention State
  45. View ViewModel Data Intent newState Action Result previousState Reducer State

    View Intention State Repository Network
  46. Data Layer @GET("characters/{characterId}") fun getCharactersSingle(...): Single<List<CharacterMarvel>> fun fetchCharactersSingle(): Single<List<CharacterMarvel>> =

    marvelApi.getCharactersSingle( ... ) MarvelApi MainRepository
  47. Actions to Results - LoadData fun loadingResult(loadDataActionStream: Flowable<MainAction.LoadData>) : Flowable<MainResult>

    { return loadDataActionStream .observeOn(Schedulers.io()) .flatMap { mainRepository.fetchCharactersSingle().toFlowable() .map { list -> MainResult.LoadingComplete(list) } .startWith(MainResult.Loading) .onErrorReturn { error -> navigate(MainNavigation.Snackbar(error.message)) MainResult.LoadingError } } } ViewModel
  48. Data Layer @GET("characters/{characterId}") fun getCharactersSingle(...): Single<List<CharacterMarvel>> MarvelApi - Rx

  49. Data Layer @GET("characters/{characterId}") suspend fun getCharacters(…): List<CharacterMarvel> MarvelApi - Coroutines

    2.6.0
  50. Data Layer fun fetchCharactersSingle(): Single<List<CharacterMarvel>> = marvelApi.getCharactersSingle( ... ) MainRepository

    - Rx
  51. Data Layer suspend fun fetchCharacters(): List<CharacterMarvel> = marvelApi.getCharacters( ... )

    MainRepository - Coroutines
  52. fun loadingResult(actions: Flowable<...>) : Flowable<MainResult> = actions .observeOn(Schedulers.io()) .flatMap {

    mainRepository.fetchCharactersFlowable() .map { MainResult.LoadingComplete } .startWith(MainResult.Loading) .onErrorReturn { error -> navigate(MainNavigation.Snackbar) MainResult.LoadingError } } fun loadingResult(actions: Flowable<MainAction.LoadData>) : Flowable<MainResult> = actions .observeOn(Schedulers.io()) .flatMap { mainRepository.fetchCharactersSingle().toFlowable() .map { states -> MainResult.LoadingComplete(states) } .startWith(MainResult.Loading) .onErrorReturn { error -> navigate(MainNavigation.Snackbar(error.message) MainResult.LoadingError } } Actions to Results - Load Data RxViewModel
  53. fun loadingResult(actions: Flowable<...>) : Flowable<MainResult> = actions .observeOn(Schedulers.io()) .flatMap {

    mainRepository.fetchCharactersFlowable() .map { MainResult.LoadingComplete } .startWith(MainResult.Loading) .onErrorReturn { error -> navigate(MainNavigation.Snackbar) MainResult.LoadingError } } RxViewModel fun loadingResult(actionsFlow: Flow<MainAction.LoadData>) : Flow<MainResult> = actionsFlow.flatMapMerge { flow { emit(MainResult.Loading) val characters = mainRepository.fetchCharacters() emit(MainResult.LoadingComplete(characters)) }.catch { navigate(MainNavigation.Snackbar(it.message) emit(MainResult.LoadingError) } } FlowViewModel Actions to Results - Load Data
  54. fun loadingResult(actions: Flowable<...>) : Flowable<MainResult> = actions .observeOn(Schedulers.io()) .flatMap {

    mainRepository.fetchCharactersFlowable() .map { MainResult.LoadingComplete } .startWith(MainResult.Loading) .onErrorReturn { error -> navigate(MainNavigation.Snackbar) MainResult.LoadingError } } RxViewModel FlowViewModel fun loadingResult(actionsFlow: Flow<MainAction.LoadData>) : Flow<MainResult> = actionsFlow.flatMapMerge { flow { emit(mainRepository.fetchCharacters()) } .map { MainResult.LoadingComplete(characters) } .onStart { emit(MainResult.Loading) } .catch { navigate(MainNavigation.Snackbar(it.message) emit(MainResult.LoadingError) } } Actions to Results - Load Data
  55. fun pullToRefreshResult(actions: Flowable<MainAction.LoadData>) : Flowable<MainResult> = ... Actions to Results

    - Pull To Refresh RxViewModel
  56. fun pullToRefreshResult(actions: Flowable<MainAction.LoadData>) : Flow<MainResult> = ... Actions to Results

    - Pull To Refresh FlowViewModel
  57. fun loadDesciptionResult(actions: Flowable<MainAction.LoadData>) : Flowable<MainResult> = ... Actions to Results

    - Load Desciption RxViewModel
  58. fun loadDesciptionResult(actions: Flowable<MainAction.LoadData>) : Flow<MainResult> = ... Actions to Results

    - Load Desciption FlowViewModel
  59. Actions to Results fun actionsToResultTransformer(actions: Flowable<MainAction>): Flowable<MainResult> = actions.publish {

    shared -> Flowable.merge(loadingResult(shared.ofType(MainAction.LoadData::class.java)), loadDescriptionResult(shared.ofType(MainAction.LoadDescription::class.java)), pullToRefreshResult(shared.ofType(MainAction.PullToRefresh::class.java))) } RxViewModel
  60. Actions to Results fun Flow<MainAction>.actionsToResultTransformer(): Flow<MainResult> = publish { loadingResult(it.ofType(MainAction.LoadData::class.java))

    .merge(loadDescriptionResult(it.ofType(MainAction.LoadDescription::class.java)), pullToRefreshResult(it.ofType(MainAction.PullToRefresh::class.java))) } Flow
  61. Merge Operator Actions to Results fun Flow<MainAction>.actionsToResultTransformer(): Flow<MainResult> = publish

    { loadingResult(it.ofType(MainAction.LoadData::class.java)) .merge(loadDescriptionResult(it.ofType(MainAction.LoadDescription::class.java)), pullToRefreshResult(it.ofType(MainAction.PullToRefresh::class.java))) } Flow
  62. Merge Operator • No merge operator at the time •

    Creating operators with coroutines is comparatively easy • Let's create one
  63. fun <T> Flow<T>.merge(other: Flow<T>): Flow<T> = channelFlow { launch {

    collect { send(it) } } launch { other.collect { send(it) } } } FlowExtensions.kt // collect from this coroutine and send it // collect and send from this coroutine, too, concurrently Merge Operator
  64. fun Flow<MainAction>.actionsToResultTransformer(): Flow<MainResult> = publish { loadingResult(it.ofType(MainAction.LoadData::class.java)) .merge(loadDescriptionResult(it.ofType(MainAction.LoadDescription::class.java)), pullToRefreshResult(it.ofType(MainAction.PullToRefresh::class.java))) }

    Actions to Results - publish Flow
  65. fun Flow<MainAction>.actionsToResultTransformer(): Flow<MainResult> = publish { loadingResult(it.ofType(MainAction.LoadData::class.java)) .merge(loadDescriptionResult(it.ofType(MainAction.LoadDescription::class.java)), pullToRefreshResult(it.ofType(MainAction.PullToRefresh::class.java))) }

    Actions to Results - publish Flow kotlin-flow-extensions: https://github.com/akarnokd/kotlin-flow-extensions
  66. Merge Operator Actions to Results - broadcastIn inputActionsFlow .broadcastIn(viewModelScope).asFlow() .actionToResultTransformer()

    ... Flow
  67. Actions to Results fun Flow<MainAction>.actionsToResultTransformer(): Flow<MainResult> = flatMapMerge { flow

    { when(it) { is MainAction.PullToRefresh -> { ... } is MainAction.LoadData -> { try { emit(MainResult.Loading) val characters = mainRepository.fetchCharacters() emit(MainResult.LoadingComplete(characters)) } catch (exception: Exception) { navigate(MainNavigation.Snackbar(exception.message)) emit(MainResult.LoadingError(exception)) } } is MainAction.LoadDescription -> { … } } } }
  68. View ViewModel Data Intent newState Action Result previousState Reducer State

    Repository Network State View Intention (Rx)
  69. View ViewModel Data Intent newState Action Result previousState Reducer State

    View Intention (Co-routines) State Repository Network
  70. View ViewModel Data Intent newState Action Result previousState Reducer State

    View Intention (Co-routines) State Repository Network Room 2.2.0- alpha
  71. State View Intention 3 2 4 5 Actions to Results

    Reduce Results to new State State stream Plugging-in the pieces 1 Action Events same
  72. State View Intention 3 2 4 5 Actions to Results

    Reduce Results to new State State stream Plugging-in the pieces 1 Action Events same
  73. Reduce Results to new State data class MainViewState(val characters: List<CharacterItemState>,

    val emptyState: EmptyState, val loadingState: LoadingState): MviState sealed class EmptyState { object None: EmptyState() object NoData: EmptyState() object NoInternet: EmptyState() } sealed class LoadingState { object None: LoadingState() object Loading: LoadingState() object PullToRefreshing: LoadingState() } } State
  74. Reduce Results to new State fun reduce(result: MainResult) { return

    when (result) { is MainResult.Loading -> copy(loadingState = LoadingState.Loading) is MainResult.LoadingError -> copy(loadingState = LoadingState.None, emptyState = EmptyState.NoData) is MainResult.LoadingComplete -> { val characterStates = reduceCharactersList(null, result.characters, resources) copy(characterStates, loadingState = LoadingState.None, emptyState = EmptyState.None) } ... } RxReducer
  75. Reduce Results to new State FlowReducer fun reduce(result: MainResult) {

    return when (result) { is MainResult.Loading -> copy(loadingState = LoadingState.Loading) is MainResult.LoadingError -> copy(loadingState = LoadingState.None, emptyState = EmptyState.NoData) is MainResult.LoadingComplete -> { val characterStates = reduceCharactersList(null, result.characters, resources) copy(characterStates, loadingState = LoadingState.None, emptyState = EmptyState.None) } ... }
  76. State View Intention 1 Action Events 3 2 4 5

    Actions to Results Reduce Results to new State State stream Plugging-in the pieces same
  77. State View Intention 3 2 4 5 Actions to Results

    Reduce Results to new State Plugging-in the pieces State stream 1 Action Events same
  78. State View Intention 3 2 4 5 Actions to Results

    Reduce Results to new State Plugging-in the pieces State stream 1 Action Events same same
  79. State State stream data class MainViewState(val characters: List<CharacterItemState>, val emptyState:

    EmptyState, val loadingState: LoadingState): MviState sealed class EmptyState { object None: EmptyState() object NoData: EmptyState() object NoInternet: EmptyState() } sealed class LoadingState { object None: LoadingState() object Loading: LoadingState() object PullToRefreshing: LoadingState() } }
  80. State LiveData fun stateLiveData(): LiveData<MainViewState> = stateLiveData private val stateLiveData

    = MutableLiveData<MainViewState>() … stateLiveData.postValue(it) ViewModel
  81. State LiveData fun stateLiveData(): LiveData<MainViewState> = stateLiveData private val stateLiveData

    = MutableLiveData<MainViewState>() … stateLiveData.postValue(it) ViewModel viewModel.stateLiveData() .observe(viewLifecycleOwner, Observer { render(it) }) View
  82. State Flow private val stateChannel = ConflatedBroadcastChannel<MainViewState>() val stateFlow =

    stateChannel.asFlow() … stateChannel.offer(it) ViewModel
  83. State Flow private val stateChannel = ConflatedBroadcastChannel<MainViewState>() val stateFlow =

    stateChannel.asFlow() … stateChannel.offer(it) ViewModel View viewModel.stateFlow.collect { render(it) }
  84. ConflatedBroadcastChannel State Flow private val stateChannel = ConflatedBroadcastChannel<MainViewState>() val stateFlow

    = stateChannel.asFlow() … stateChannel.offer(it) ViewModel lifecycleScope.launch { viewModel.stateFlow.collect { render(it) } } View 2.2.0- alpha02
  85. ConflatedBroadcastChannel • BroadcastChannel - multiple receivers • Conflate - combine

    • Recent value is emitted • Like RxJava BehaviorSubject
  86. Render override fun render(state: MainViewState) { binding.refreshing = state.loadingState ==

    MainViewState.LoadingState.PullToRefreshing binding.loading = state.loadingState == MainViewState.LoadingState.Loading val characterModelList = state.characters.map { CharacterItem(it, this) } adapter.replaceItems(characterModelList, true) } View
  87. State View Intention 4 5 Plugging-in the pieces State stream

    3 2 Actions to Results Reduce Results to new State 1 Action Events same same
  88. State View Intention 1 Action Events 3 2 4 5

    Actions to Results Reduce Results to new State Plugging-in the pieces State stream 3 2 Actions to Results Reduce Results to new State 1 Action Events same same
  89. State View Intention 1 Action Events 3 2 4 5

    Actions to Results Reduce Results to new State Plugging-in the pieces State stream 3 2 Actions to Results Reduce Results to new State 1 Action Events same same same
  90. ViewModel Plugging-in the pieces interface ViewModel { fun onAction(action: MainAction)

    ... }
  91. RxViewModel Plugging-in the pieces val actionsProcessor: PublishProcessor<Action> = PublishProcessor.create() fun

    onAction(action: Action) { actionsProcessor.onNext(action) }
  92. FlowViewModel Plugging-in the pieces var broadcastChannel = ConflatedBroadcastChannel<MainAction>() fun onAction(action:

    MainAction) = broadcastChannel.offer(action) var actionsFlow = broadcastChannel.asFlow()
  93. fun processActions() { actionsProcessor .compose(actionToResultTransformer) .scan(initialState) { state, result ->

    reduce(state, result)} .subscribe({ stateLiveData.postValue(it) }, Timber::e) .bindToLifecycle() } fun processActions() { actionsProcessor .compose(actionToResultTransformer) .scan(initialState) { state, result: Result -> reduce(state, result)} .subscribe({ stateLiveData.postValue(it) }, Timber::e) .bindToLifecycle() } Plugging-in the pieces RxViewModel
  94. Plugging-in the pieces fun processActions() { actionsProcessor .compose(actionToResultTransformer) .scan(initialState) {

    state, result -> reduce(state, result)} .subscribe({ stateLiveData.postValue(it) }, Timber::e) .bindToLifecycle() } fun processActions() { viewModelScope.launch { actionsFlow .broadcastIn(viewModelScope).asFlow() .actionToResultTransformer() .scan(initialState) { state, result -> reduce(state, result) } .collect { stateLiveData.postValue(it) } } } RxViewModel FlowViewModel
  95. State View Intention 5 Plugging-in the pieces 1 Action Events

    3 2 4 Actions to Results Reduce Results to new State State stream 3 2 Actions to Results Reduce Results to new State 1 Action Events same same same
  96. State View Intention 5 Plugging-in the pieces 1 Action Events

    3 2 4 Actions to Results Reduce Results to new State State stream 3 2 Actions to Results Reduce Results to new State 1 Action Events same same same
  97. State View Intention Congratulations !!

  98. @ragdroid #droidconNYC Summary

  99. What Did we learn? Rx Coroutines Single / Completable suspend

    function Flowable/Observable Flow BehaviorSubject ConflatedBroadcastChannel (DataFlow proposal) Schedulers Dispatchers Disposables Scopes
  100. What Did we learn? Rx Flow just(“Hello”) flowOf(“Hello”) flatmap() flatmapMerge()

    subscribe() collect() publish() broadcastIn() startWith() onStart() onErrorReturn() catch() map() map() scan() scan() ... ...
  101. What Did we learn? Rx Flow subscribeOn() flowOn() observeOn() flowOn()

    ❌ ❌
  102. @ragdroid #droidconNYC Common Questions

  103. Why Flow? - Current State

  104. Why Flow? • First Party support • Android libraries have

    Flow support now like Room • Reducing complexity of business logic • Hopefully share more business logic
  105. RxJava Coroutines Interop • Yes org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.3.0 • Single.await(), Flowable.awaitX() •

    rxSingle { }, rxFlowable { } …
  106. Non coroutine APIs to Coroutine APIs • Yes • suspendCoroutine

    { } • Continuation.resumeWith( ) Continuation.resumeWithException()
  107. Testing? • Yes • TestDispatcher • Dispatchers.setMain() • Inject a

    DispatcherProvider?
  108. What Next? • kotlin-flow-extensions: https://github.com/akarnokd/kotlin-flow-extensions • CoroutineBinding: CoroutineBinding: https://github.com/satoshun/CoroutineBinding •

    Flowing in the Deep: https://www.droidcon.com/media-detail?video=352670453 • DataFlow: https://github.com/Kotlin/kotlinx.coroutines/pull/1354/files • Flow Guide: https://github.com/Kotlin/kotlinx.coroutines/blob/1.3.0/docs/flow.md • Flow vs Channel: https://github.com/manuelvicnt/MathCoroutinesFlow • Demo Project: https://github.com/ragdroid/klayground/tree/kotlin-flow
  109. References & Acknowledgements • https://github.com/brewin/mvi-coroutines • Manuel Vivo @manuelvicnt •

    Hannes Dorfmann @sockeqwe • Android Team @Over • Ritesh Gupta @_riteshhh • https://medium.com/@elizarov - Roman Elizarov • https://medium.com/@objcode - Sean McQuillan • https://www.youtube.com/watch?v=PXBXcHQeDLE - MVI for Android, Benoît Quenaudon
  110. @ragdroid #droidconNYC

  111. Questions?