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

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

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. FLOWING
    THINGS
    Not so strange in the MVI world

    View full-size slide

  2. Garima Jain
    @ragdroid

    View full-size slide

  3. @ragdroid
    @blrdroid
    Who?

    View full-size slide

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

    View full-size slide

  5. @ragdroid
    @blrdroid
    What?

    View full-size slide

  6. What?
    • Chapter One : Coroutines and Flow
    • Chapter Two : MVI
    • Chapter Three : Rx to Flow
    • Summary

    View full-size slide

  7. CHAPTER ONE
    THE FLOW

    View full-size slide

  8. Coroutines
    • Lightweight thread
    • Run computations without blocking
    • Can be suspended

    View full-size slide

  9. Coroutines
    • Coroutine Scope
    ★ ViewModel Scope
    ★ Lifecycle Scope
    • Coroutine Builders
    ★ launch { } - fire and forget
    ★ async { } - await() result
    ★ within a scope, Structured Concurrency

    View full-size slide

  10. Channels
    • Communication between different coroutines
    • Similar to BlockingQueue

    View full-size slide

  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

    View full-size slide

  12. Flow Builders
    ★ flowOf( )
    ★ flow { }

    View full-size slide

  13. Flow Operators
    • Intermediate :
    ★ map, filter, take, zip, etc.
    • Terminal :
    ★ collect, single, reduce, etc.

    View full-size slide

  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

    View full-size slide

  15. Flow Constraints
    ★ Context Preservation

    View full-size slide

  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

    View full-size slide

  17. CHAPTER TWO
    THE BATTLE OF ACTIONS
    MVI

    View full-size slide

  18. Model View Intent

    View full-size slide

  19. Model View Intent
    State View Intention

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  22. View
    ViewModel Data
    Intent
    newState
    Action
    Result
    previousState
    Reducer
    State View Intention
    State
    4
    3
    5
    1 2
    MVI on top of MVVM

    View full-size slide

  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

    View full-size slide

  24. CHAPTER THREE
    VANISHING OF Rx FLOWABLES
    Migrating an Rx MVI pattern to use Flow

    View full-size slide

  25. Actions
    sealed class MainAction {
    object PullToRefresh
    object LoadData
    data class LoadDescription(val characterId: Long)
    }

    View full-size slide

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

    View full-size slide

  27. Actions - ViewModel
    interface ViewModel {
    fun onAction(action: MainAction)
    ...
    }
    Flow

    View full-size slide

  28. Action Events
    viewModel.onAction(MainAction.LoadData)
    viewModel.onAction(..)

    View full-size slide

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

    View full-size slide

  30. viewModel.onAction(MainAction.LoadData)
    viewModel.onAction(..)
    override fun onCharacterDescriptionClicked(itemId: Long) {
    viewModel.onAction(MainAction.LoadDescription(itemId))
    }
    refreshLayout.setOnRefreshListener {
    viewModel.onAction(MainAction.PullToRefresh)
    }
    Action Events

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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
    same

    View full-size slide

  34. Actions to Results
    sealed class MainResult {
    object Loading
    object LoadingError
    data class LoadingComplete(val characters: List)
    object PullToRefreshing: MainResult()
    object PullToRefreshError
    data class PullToRefreshComplete(val characters: List)
    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)
    }
    }

    View full-size slide

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

    View full-size slide

  36. View ViewModel
    Data
    Intent
    newState
    Action Result
    previousState
    Reducer
    State View Intention
    State
    Repository Network

    View full-size slide

  37. Data Layer
    @GET("characters/{characterId}")
    fun getCharactersSingle(...): Single>
    fun fetchCharactersStream(): Flowable> =
    marvelApi.getCharactersSingle( ... ).mergeWith(cache...)
    MarvelApi
    MainRepository

    View full-size slide

  38. Data Layer
    @GET("characters/{characterId}")
    fun getCharactersSingle(...): Single>
    MarvelApi - Rx

    View full-size slide

  39. Data Layer
    @GET("characters/{characterId}")
    suspend fun getCharacters(…): List
    MarvelApi - Coroutines
    2.6.0

    View full-size slide

  40. Data Layer
    fun fetchCharactersStream(): Flowable> =
    marvelApi.getCharactersSingle( ... )
    .mergeWith(cache...)
    MainRepository - Rx

    View full-size slide

  41. Data Layer
    fun fetchCharactersFlow(): Flow> = flow {
    val characters = marvelApi.getCharacters( ... )
    emit(characters)
    }
    .merge(cacheFlow)
    MainRepository - Flow
    Room
    2.2.0-
    alpha

    View full-size slide

  42. Merge Operator
    fun fetchCharactersFlow(): Flow> = flow {
    val characters = marvelApi.getCharacters( ... )
    emit(characters)
    }
    .merge(cacheFlow)
    Data Layer
    MainRepository - Flow

    View full-size slide

  43. Merge Operator
    • No merge operator at the time
    • Creating operators with coroutines is comparatively easy
    • Home Work (Hint Look into channelFlow)

    View full-size slide

  44. fun loadingResult(actions: Flowable<...>)
    : Flowable =
    actions
    .observeOn(Schedulers.io())
    .flatMap {
    mainRepository.fetchCharactersStream()
    .map { MainResult.LoadingComplete }
    .startWith(MainResult.Loading)
    .onErrorReturn { error ->
    navigate(MainNavigation.Snackbar)
    MainResult.LoadingError
    }
    }
    fun loadingResult(actions: Flowable)
    : Flowable =
    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

    View full-size slide

  45. fun loadingResult(actions: Flowable<...>)
    : Flowable =
    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)
    : Flow =
    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

    View full-size slide

  46. fun pullToRefreshResult(actions: Flowable)
    : Flowable =
    ...
    Actions to Results - Pull To Refresh
    RxViewModel

    View full-size slide

  47. fun pullToRefreshResult(actions: Flowable)
    : Flow =
    ...
    Actions to Results - Pull To Refresh
    FlowViewModel

    View full-size slide

  48. fun loadDesciptionResult(actions: Flowable)
    : Flowable =
    ...
    Actions to Results - Load Desciption
    RxViewModel

    View full-size slide

  49. fun loadDesciptionResult(actions: Flowable)
    : Flow =
    ...
    Actions to Results - Load Desciption
    FlowViewModel

    View full-size slide

  50. Repository emits Single?
    fun fetchCharactersStream(): Flowable> =
    marvelApi.getCharactersSingle( ... )
    .mergeWith(cache...)
    MainRepository - Rx

    View full-size slide

  51. fun fetchCharactersSingle(): Single> =
    marvelApi.getCharactersSingle( ... )
    MainRepository - Rx
    Repository emits Single?

    View full-size slide

  52. Repository exposes suspend function
    suspend fun fetchCharacters(): List =
    marvelApi.getCharacters( ... )
    MainRepository - Coroutines

    View full-size slide

  53. Actions to Results
    fun Flow.actionsToResultTransformer(): Flow =
    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 -> { … }
    }
    }
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  59. Reduce Results to new State
    data class MainViewState(val characters: List,
    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

    View full-size slide

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

    View full-size slide

  61. 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)
    }
    ...
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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
    same

    View full-size slide

  65. State
    State stream
    data class MainViewState(val characters: List,
    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()
    }
    }

    View full-size slide

  66. State LiveData
    fun stateLiveData(): LiveData = stateLiveData
    private val stateLiveData = MutableLiveData()

    stateLiveData.postValue(it)
    ViewModel

    View full-size slide

  67. State LiveData
    fun stateLiveData(): LiveData = stateLiveData
    private val stateLiveData = MutableLiveData()

    stateLiveData.postValue(it)
    ViewModel
    viewModel.stateLiveData()
    .observe(viewLifecycleOwner,
    Observer { render(it) })
    View

    View full-size slide

  68. State Flow
    private val stateChannel = ConflatedBroadcastChannel()
    val stateFlow = stateChannel.asFlow()

    stateChannel.offer(it)
    ViewModel

    View full-size slide

  69. State Flow
    private val stateChannel = ConflatedBroadcastChannel()
    val stateFlow = stateChannel.asFlow()

    stateChannel.offer(it)
    ViewModel
    View
    viewModel.stateFlow.collect {
    render(it)
    }

    View full-size slide

  70. ConflatedBroadcastChannel
    State Flow
    private val stateChannel = ConflatedBroadcastChannel()
    val stateFlow = stateChannel.asFlow()

    stateChannel.offer(it)
    ViewModel
    lifecycleScope.launch {
    viewModel.stateFlow.collect {
    render(it)
    }
    }
    View
    2.2.0-
    alpha02

    View full-size slide

  71. ConflatedBroadcastChannel
    • BroadcastChannel - multiple receivers
    • Conflate - combine
    • Recent value is emitted
    • Like RxJava BehaviorSubject

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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
    same

    View full-size slide

  76. ViewModel
    Plugging-in the pieces
    interface ViewModel {
    fun onAction(action: MainAction)
    ...
    }

    View full-size slide

  77. RxViewModel
    Plugging-in the pieces
    val actionsProcessor: PublishProcessor =
    PublishProcessor.create()
    fun onAction(action: Action) {
    actionsProcessor.onNext(action)
    }

    View full-size slide

  78. FlowViewModel
    Plugging-in the pieces
    var broadcastChannel = ConflatedBroadcastChannel()
    fun onAction(action: MainAction) = broadcastChannel.offer(action)
    var actionsFlow = broadcastChannel.asFlow()

    View full-size slide

  79. 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()
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  83. State View Intention
    Congratulations !!

    View full-size slide

  84. @ragdroid
    @blrdroid
    Summary

    View full-size slide

  85. What Did we learn?
    Rx Coroutines
    Single / Completable suspend function
    Flowable/Observable Flow
    BehaviorSubject
    ConflatedBroadcastChannel
    (DataFlow proposal)
    Schedulers Dispatchers
    Disposables Scopes

    View full-size slide

  86. What Did we learn?
    Rx Flow
    just(“Hello”) flowOf(“Hello”)
    flatmap() flatmapMerge()
    subscribe() collect()
    publish() broadcastIn()
    startWith() onStart()
    onErrorReturn() catch()
    map() map()
    scan() scan()
    ... ...

    View full-size slide

  87. What Did we learn?
    Rx Flow
    subscribeOn() flowOn()
    observeOn() flowOn()


    View full-size slide

  88. @ragdroid
    @blrdroid
    Common Questions

    View full-size slide

  89. Why Flow? - Current State

    View full-size slide

  90. Why Flow?
    • First Party support
    • Android libraries have Flow support now like Room
    • Reducing complexity of business logic
    • Hopefully share more business logic

    View full-size slide

  91. RxJava Coroutines Interop
    • Yes org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.3.0
    • Single.await(), Flowable.awaitX()
    • rxSingle { }, rxFlowable { } …

    View full-size slide

  92. Non coroutine APIs to Coroutine APIs
    • Yes
    • suspendCoroutine { }
    • Continuation.resumeWith( ) Continuation.resumeWithException()

    View full-size slide

  93. Testing?
    • Yes
    • TestDispatcher
    • Dispatchers.setMain()
    • Inject a DispatcherProvider?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  96. @ragdroid
    @blrdroid

    View full-size slide