Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Flowing Things, not so strange in the MVI world...

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.

ragdroid

November 17, 2019
Tweet

More Decks by ragdroid

Other Decks in Programming

Transcript

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

    Two : MVI • Chapter Three : Rx to Flow • Summary
  2. Coroutines • Coroutine Scope ★ ViewModel Scope ★ Lifecycle Scope

    • Coroutine Builders ★ launch { } - fire and forget ★ async { } - await() result ★ within a scope, Structured Concurrency
  3. 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
  4. Flow Operators • Intermediate : ★ map, filter, take, zip,

    etc. • Terminal : ★ collect, single, reduce, etc.
  5. 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
  6. 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
  7. View ViewModel Data Intent newState Action Result previousState Reducer State

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

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

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

    Actions to Results Reduce Results to new State State stream Plugging-in the pieces
  11. 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
  12. 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) } }
  13. Data Layer fun fetchCharactersFlow(): Flow<List<CharacterMarvel>> = flow { val characters

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

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

    Creating operators with coroutines is comparatively easy • Home Work (Hint Look into channelFlow)
  16. 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
  17. 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
  18. 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 -> { … } } } }
  19. View ViewModel Data Intent newState Action Result previousState Reducer State

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

    View Intention (Co-routines) State Repository Network
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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) } ... }
  26. 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
  27. 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
  28. 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
  29. 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() } }
  30. State LiveData fun stateLiveData(): LiveData<MainViewState> = stateLiveData private val stateLiveData

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

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

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

    • Recent value is emitted • Like RxJava BehaviorSubject
  35. 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
  36. 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
  37. 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
  38. 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
  39. FlowViewModel Plugging-in the pieces var broadcastChannel = ConflatedBroadcastChannel<MainAction>() fun onAction(action:

    MainAction) = broadcastChannel.offer(action) var actionsFlow = broadcastChannel.asFlow()
  40. 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() }
  41. 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
  42. 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
  43. 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
  44. What Did we learn? Rx Coroutines Single / Completable suspend

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

    subscribe() collect() publish() broadcastIn() startWith() onStart() onErrorReturn() catch() map() map() scan() scan() ... ...
  46. Why Flow? • First Party support • Android libraries have

    Flow support now like Room • Reducing complexity of business logic • Hopefully share more business logic
  47. Non coroutine APIs to Coroutine APIs • Yes • suspendCoroutine

    { } • Continuation.resumeWith( ) Continuation.resumeWithException()
  48. 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
  49. 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