$30 off During Our Annual Pro Sale. View Details »

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.

ragdroid

April 21, 2020
Tweet

More Decks by ragdroid

Other Decks in Programming

Transcript

  1. FLOWING
    THINGS
    Not so strange in the MVI world

    View Slide

  2. Garima Jain
    @ragdroid

    View Slide

  3. Garima Jain
    @ragdroid

    View Slide

  4. @ragdroid
    @AndroidMakersFR
    Who?

    View Slide

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

    View Slide

  6. @ragdroid
    @AndroidMakersFR
    What?

    View Slide

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

    View Slide

  8. CHAPTER ONE
    THE FLOW

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  12. Flow
    • Cold asynchronous stream that sequentially emits values
    • Utilizes coroutines and channels
    • Experimental Stable APIs since 1.3.0
    • Now 1.3.5

    View Slide

  13. Flow Builders
    ★ flowOf( )
    ★ flow { }
    ★ channelFlow { }

    View Slide

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

    View Slide

  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

    View Slide

  16. Flow Constraints
    ★ Context Preservation
    ★ Exception Transparency

    View Slide

  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

    View Slide

  18. CHAPTER TWO
    THE BATTLE OF ACTIONS
    MVI

    View Slide

  19. Model View Intent

    View Slide

  20. Model View Intent
    State View Intention

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  26. Demo

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View 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

    View Slide

  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

    View Slide

  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

    View Slide

  36. 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 Slide

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

    View Slide

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

    View Slide

  39. Data Layer
    @GET("characters/{characterId}")
    fun getCharactersSingle(...): Single>
    fun fetchCharactersSingle(): Single> =
    marvelApi.getCharactersSingle( ... )
    MarvelApi
    MainRepository

    View Slide

  40. Actions to Results - LoadData
    fun loadingResult(loadDataActionStream: Flowable)
    : Flowable {
    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

    View Slide

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

    View Slide

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

    View Slide

  43. Data Layer
    fun fetchCharactersSingle(): Single> =
    marvelApi.getCharactersSingle( ... )
    MainRepository - Rx

    View Slide

  44. Data Layer
    suspend fun fetchCharacters(): List =
    marvelApi.getCharacters( ... )
    MainRepository - Coroutines

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  54. Merge Operator
    • No merge operator at the time
    • Creating operators with coroutines is comparatively easy
    • Let's create one

    View Slide

  55. fun Flow.merge(other: Flow): Flow = 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

    View Slide

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

    View Slide

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

    View Slide

  58. 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  64. 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 Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  70. 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 Slide

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

    stateLiveData.postValue(it)
    ViewModel

    View Slide

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

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

    View Slide

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

    stateChannel.offer(it)
    ViewModel

    View Slide

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

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

    View Slide

  75. 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 Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  88. State View Intention
    Congratulations !!

    View Slide

  89. @ragdroid
    @AndroidMakersFR
    Summary

    View Slide

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

    View Slide

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

    View Slide

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


    View Slide

  93. @ragdroid
    @AndroidMakersFR
    Common Questions

    View Slide

  94. Why Flow? - Current State

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  101. @ragdroid
    @AndroidMakersFR

    View Slide

  102. Questions?

    View Slide