Flowing Things, not so strange in the MVI world (BlrDroid meetup)

A3958eeb9a7f402b134c0c017d6614ee?s=47 ragdroid
November 17, 2019

Flowing Things, not so strange in the MVI world (BlrDroid meetup)

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

November 17, 2019
Tweet

Transcript

  1. FLOWING THINGS Not so strange in the MVI world

  2. Garima Jain @ragdroid

  3. @ragdroid @blrdroid Who?

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

    Things?
  5. @ragdroid @blrdroid What?

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

    Two : MVI • Chapter Three : 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 • Like Reactive Observable / Flowable • Experimental Stable APIs since 1.3.0 • Now 1.3.2
  12. Flow Builders ★ flowOf( ) ★ flow { }

  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

  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 THE BATTLE OF ACTIONS MVI

  18. Model View Intent

  19. Model View Intent State View Intention

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

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

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

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

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

    pattern to use Flow
  25. Demo

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

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

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

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

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

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

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

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

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

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

    marvelApi.getCharactersSingle( ... ).mergeWith(cache...) MarvelApi MainRepository
  39. Data Layer @GET("characters/{characterId}") fun getCharactersSingle(...): Single<List<CharacterMarvel>> MarvelApi - Rx

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

    2.6.0
  41. Data Layer fun fetchCharactersStream(): Flowable<List<CharacterMarvel>> = marvelApi.getCharactersSingle( ... ) .mergeWith(cache...)

    MainRepository - Rx
  42. Data Layer fun fetchCharactersFlow(): Flow<List<CharacterMarvel>> = flow { val characters

    = marvelApi.getCharacters( ... ) emit(characters) } .merge(cacheFlow) MainRepository - Flow Room 2.2.0- alpha
  43. Merge Operator fun fetchCharactersFlow(): Flow<List<CharacterMarvel>> = flow { val characters

    = marvelApi.getCharacters( ... ) emit(characters) } .merge(cacheFlow) Data Layer MainRepository - Flow
  44. Merge Operator • No merge operator at the time •

    Creating operators with coroutines is comparatively easy • Home Work (Hint Look into channelFlow)
  45. fun loadingResult(actions: Flowable<...>) : Flowable<MainResult> = actions .observeOn(Schedulers.io()) .flatMap {

    mainRepository.fetchCharactersStream() .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.fetchCharactersStream() .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.fetchCharactersStream() .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 { mainRepository.fetchCharactersFlow()) .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: Flowable<MainAction.LoadDescription>) : Flow<MainResult> = ... Actions to Results

    - Load Desciption FlowViewModel
  51. Repository emits Single? fun fetchCharactersStream(): Flowable<List<CharacterMarvel>> = marvelApi.getCharactersSingle( ... )

    .mergeWith(cache...) MainRepository - Rx
  52. fun fetchCharactersSingle(): Single<List<CharacterMarvel>> = marvelApi.getCharactersSingle( ... ) MainRepository - Rx

    Repository emits Single?
  53. Repository exposes suspend function suspend fun fetchCharacters(): List<CharacterMarvel> = marvelApi.getCharacters(

    ... ) MainRepository - Coroutines
  54. 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 -> { … } } } }
  55. View ViewModel Data Intent newState Action Result previousState Reducer State

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

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

    View Intention (Co-routines) State Repository Network
  58. 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
  59. 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
  60. 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
  61. Reduce Results to new State fun reduce(result: MainResult): MainViewState {

    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
  62. Reduce Results to new State FlowReducer fun reduce(result: MainResult): MainViewState

    { 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) } ... }
  63. 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
  64. 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
  65. 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
  66. 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() } }
  67. State LiveData fun stateLiveData(): LiveData<MainViewState> = stateLiveData private val stateLiveData

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

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

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

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

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

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

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

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

    reduce(state, result)} .subscribe({ stateLiveData.postValue(it) }, Timber::e) .bindToLifecycle() } Plugging-in the pieces RxViewModel fun processActions() { actionsProcessor .compose(actionToResultTransformer) .scan(initialState) { state, result: Result -> reduce(state, result)} .subscribe({ stateLiveData.postValue(it) }, Timber::e) .bindToLifecycle() }
  81. 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 .actionToResultTransformer() .scan(initialState) { state, result -> reduce(state, result) } .collect { stateLiveData.postValue(it) } } } RxViewModel FlowViewModel
  82. 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
  83. 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
  84. State View Intention Congratulations !!

  85. @ragdroid @blrdroid Summary

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

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

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

    ❌ ❌
  89. @ragdroid @blrdroid Common Questions

  90. Why Flow? - Current State

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

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

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

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

    DispatcherProvider?
  95. 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 • DroidconNYC: https://www.droidcon.com/media-detail?video=362742238
  96. 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
  97. @ragdroid @blrdroid

  98. Questions?