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 Slide

  2. Garima Jain
    @ragdroid

    View Slide

  3. @ragdroid
    @blrdroid
    Who?

    View Slide

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

    View Slide

  5. @ragdroid
    @blrdroid
    What?

    View Slide

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

    View Slide

  7. CHAPTER ONE
    THE FLOW

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 Slide

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

    View Slide

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

    View 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 Slide

  15. Flow Constraints
    ★ Context Preservation

    View 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 Slide

  17. CHAPTER TWO
    THE BATTLE OF ACTIONS
    MVI

    View Slide

  18. Model View Intent

    View Slide

  19. Model View Intent
    State View Intention

    View Slide

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

    View Slide

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

    View 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 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 Slide

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

    View Slide

  25. Demo

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View 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 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
    same

    View Slide

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

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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
    }
    }
    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 Slide

  46. 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 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: Flowable)
    : Flow =
    ...
    Actions to Results - Load Desciption
    FlowViewModel

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

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

    View Slide

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

    View Slide

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

    View 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 Slide

  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

    View Slide

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

  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

    View Slide

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

    View Slide

  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

    View 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

    View Slide

  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

    View Slide

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

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

    stateLiveData.postValue(it)
    ViewModel

    View Slide

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

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

    View Slide

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

    stateChannel.offer(it)
    ViewModel

    View Slide

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

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

    View Slide

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

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

    View Slide

  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

    View Slide

  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

    View 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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View 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 Slide

  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

    View Slide

  84. State View Intention
    Congratulations !!

    View Slide

  85. @ragdroid
    @blrdroid
    Summary

    View Slide

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

    View Slide

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

    View Slide

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


    View Slide

  89. @ragdroid
    @blrdroid
    Common Questions

    View Slide

  90. Why Flow? - Current State

    View Slide

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

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  97. @ragdroid
    @blrdroid

    View Slide

  98. Questions?

    View Slide