Flowing Things, not so strange in the MVI world

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

April 21, 2020
Tweet

Transcript

  1. FLOWING THINGS Not so strange in the MVI world

  2. Garima Jain @ragdroid

  3. Garima Jain @ragdroid

  4. @ragdroid @AndroidMakersFR Who?

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

    Things?
  6. @ragdroid @AndroidMakersFR What?

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

    Two : MVI • Chapter Three : Rx to Flow • Summary
  8. CHAPTER ONE THE FLOW

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

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

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

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

    Utilizes coroutines and channels • Experimental Stable APIs since 1.3.0 • Now 1.3.5
  13. Flow Builders ★ flowOf( ) ★ flow { } ★

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

    etc. • Terminal : ★ collect, single, reduce, etc.
  15. 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
  16. Flow Constraints ★ Context Preservation ★ Exception Transparency

  17. 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
  18. CHAPTER TWO THE BATTLE OF ACTIONS MVI

  19. Model View Intent

  20. Model View Intent State View Intention

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

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

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

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

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

    pattern to use Flow
  26. Demo

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

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

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

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

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

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

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

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

    Actions to Results Reduce Results to new State State stream Plugging-in the pieces
  35. 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
  36. 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) } }
  37. View ViewModel Data Intent newState Action Result previousState Reducer State

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

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

    marvelApi.getCharactersSingle( ... ) MarvelApi MainRepository
  40. 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
  41. Data Layer @GET("characters/{characterId}") fun getCharactersSingle(...): Single<List<CharacterMarvel>> MarvelApi - Rx

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

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

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

    MainRepository - Coroutines
  45. 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
  46. 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
  47. fun pullToRefreshResult(actions: Flowable<MainAction.PullToRefresh>) : Flowable<MainResult> = ... Actions to Results

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

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

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

    - Load Desciption FlowViewModel
  51. Actions to Results fun Flowable<…>.actionsToResultTransformer(): Flowable<…> = flatMap { Flowable.merge(loadingResult(ofType(MainAction.LoadData::class.java)),

    loadDescriptionResult(ofType(MainAction.LoadDescription::class.java)), pullToRefreshResult(ofType(MainAction.PullToRefresh::class.java))) } RxViewModel
  52. Merge Operator Actions to Results fun Flow<MainAction>.actionsToResultTransformer(): Flow<MainResult> = flatMapMerge

    { loadingResult(ofType(MainAction.LoadData::class.java)) .merge(loadDescriptionResult(ofType(MainAction.LoadDescription::class.java)), pullToRefreshResult(ofType(MainAction.PullToRefresh::class.java))) } Flow
  53. Merge Operator Actions to Results fun Flow<MainAction>.actionsToResultTransformer(): Flow<MainResult> = flatMapMerge

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

    Creating operators with coroutines is comparatively easy • Let's create one
  55. 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
  56. Merge Operator Actions to Results fun Flow<MainAction>.actionsToResultTransformer(): Flow<MainResult> = flatMapMerge

    { loadingResult(ofType(MainAction.LoadData::class.java)) .merge(loadDescriptionResult(ofType(MainAction.LoadDescription::class.java)), pullToRefreshResult(ofType(MainAction.PullToRefresh::class.java))) } Flow
  57. View ViewModel Data Intent newState Action Result previousState Reducer State

    Repository Network State View Intention (Rx)
  58. 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 -> { … } } } }
  59. View ViewModel Data Intent newState Action Result previousState Reducer State

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

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

    View Intention (Co-routines) State Repository Network Room 2.2.0- alpha
  62. 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
  63. 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
  64. 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
  65. 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
  66. 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) } ... }
  67. 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
  68. 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
  69. 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
  70. 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() } }
  71. State LiveData fun stateLiveData(): LiveData<MainViewState> = stateLiveData private val stateLiveData

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

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

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

    stateChannel.asFlow() … stateChannel.offer(it) ViewModel View viewModel.stateFlow.collect { render(it) }
  75. 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
  76. ConflatedBroadcastChannel • BroadcastChannel - multiple receivers • Conflate - combine

    • Recent value is emitted • Like RxJava BehaviorSubject
  77. 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
  78. 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
  79. 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
  80. 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
  81. ViewModel Plugging-in the pieces interface ViewModel { fun onAction(action: MainAction)

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

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

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

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

    state, result -> reduce(state, result)} .subscribe({ stateLiveData.postValue(it) }, Timber::e) .bindToLifecycle() } fun processActions() { viewModelScope.launch { actionsFlow .actionToResultTransformer() .scan(initialState) { state, result -> reduce(state, result) } .collect { stateLiveData.postValue(it) } } } RxViewModel FlowViewModel
  86. 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
  87. 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
  88. State View Intention Congratulations !!

  89. @ragdroid @AndroidMakersFR Summary

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

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

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

    ❌ ❌
  93. @ragdroid @AndroidMakersFR Common Questions

  94. Why Flow? - Current State

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

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

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

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

    DispatcherProvider?
  99. 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
  100. 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
  101. @ragdroid @AndroidMakersFR

  102. Questions?