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

How to cook a well done MVI for Android (Mobius 2018)

How to cook a well done MVI for Android (Mobius 2018)

Reactive programming has firmly become one of the modern Android developer's tools. But most developers only use it in specific application parts concerning background operations. But what if we build an application solely on reactive patterns, what if we model our app as a single data stream? You've probably heard of Redux from the web world. It seems that per se this approach doesn't fit Android, however, we can still borrow something useful from it.

In this talk, we'll discuss how we can adapt Unidirectional Data Flow to modern Android development and how Kotlin and its key features can help us in that. We'll also take a look at examples of typical use cases implementation for this approach.

Video: https://youtu.be/hBkQkjWnAjg

C50a1f407bc251b7395c0984be4327e9?s=128

Sergey Ryabov

December 09, 2018
Tweet

More Decks by Sergey Ryabov

Other Decks in Programming

Transcript

  1. How to cook a well done MVI for Android Sergey

    Ryabov
  2. • Android Engineer & Mobile Consultant • Kotlin User Group

    SPb • Android Academy SPb & Msk • Bla-bla-bla • Digital Nomad
  3. None
  4. PROBLEMS OF A MODERN APP

  5. PROBLEMS OF A MODERN APP ▸ Lots of asynchronicity: REST,

    WebSockets, Pushes, …
  6. PROBLEMS OF A MODERN APP ▸ Lots of asynchronicity: REST,

    WebSockets, Pushes, … ▸ State updates from random places
  7. PROBLEMS OF A MODERN APP ▸ Lots of asynchronicity: REST,

    WebSockets, Pushes, … ▸ State updates from random places ▸ Big size
  8. PROBLEMS OF A MODERN APP ▸ Lots of asynchronicity: REST,

    WebSockets, Pushes, … ▸ State updates from random places ▸ Big size ▸ Async * Size => Something changes Somewhere and it all F***ed up
  9. PROBLEMS OF A MODERN APP ▸ Lots of asynchronicity: REST,

    WebSockets, Pushes, … ▸ State updates from random places ▸ Big size ▸ Async * Size => Something changes Somewhere and it all F***ed up ▸ Pain In The Ass when looking for “where it all started to go wrong”
  10. COMMON STATE

  11. COMMON STATE ▸ Single source of truth

  12. COMMON STATE ▸ Single source of truth ▸ Easy to

    check at any particular time
  13. COMMON STATE ▸ Single source of truth ▸ Easy to

    check at any particular time ▸ Clear sequence of changes
  14. COMMON STATE ▸ Single source of truth ▸ Easy to

    check at any particular time ▸ Clear sequence of changes ▸ Easy to find the cause of the change
  15. COMMON STATE ▸ Single source of truth ▸ Easy to

    check at any particular time ▸ Clear sequence of changes ▸ Easy to find the cause of the change ▸ Easy decoupled testing
  16. COMMON STATE Benefits are clear, but the arch implementation is

    still hard
  17. UNIDIRECTIONAL DATA FLOW

  18. UNIDIRECTIONAL DATA FLOW view( model( intent() ) )

  19. UNIDIRECTIONAL DATA FLOW render( state( actions() ) )

  20. Доклад Вартанова

  21. REACTIVE STATE Search query SUBMIT

  22. REACTIVE STATE submitBtn.clicks() .doOnNext { submitBtn.isEnabled = false progressView.visibility =

    VISIBLE } .flatMap { api.search(searchView.text.toString()) } .observeOn(uiScheduler) .doOnNext { progressView.visibility = GONE } .subscribe( { data -> showData(data) }, { submitBtn.isEnabled = true toast("Search failed") } )
  23. REACTIVE STATE submitBtn.clicks() .doOnNext { submitBtn.isEnabled = false progressView.visibility =

    VISIBLE } .flatMap { api.search(searchView.text.toString()) } .observeOn(uiScheduler) .doOnNext { progressView.visibility = GONE } .subscribe( { data -> showData(data) }, { submitBtn.isEnabled = true toast("Search failed") } )
  24. REACTIVE STATE submitBtn.clicks() .doOnNext { submitBtn.isEnabled = false progressView.visibility =

    VISIBLE } .flatMap { api.search(searchView.text.toString()) } .observeOn(uiScheduler) .doOnNext { progressView.visibility = GONE } .subscribe( { data -> showData(data) }, { submitBtn.isEnabled = true toast("Search failed") } )
  25. None
  26. clicks()

  27. doOnNext()

  28. searchView.text

  29. doOnNext()

  30. subscribe()

  31. None
  32. REACTIVE STATE sealed class UiAction { class SearchAction(val query: String)

    : UiAction() } sealed class UiState { object Loading : UiState() class Success(val data: Data) : UiState() class Failure(val error: Throwable) : UiState() }
  33. REACTIVE STATE submitBtn.clicks() .map { SearchAction(searchView.text.toString()) } .flatMap { action

    -> api.search(action.query) .map<UiState> { result -> Success(result) } .onErrorReturn { e -> Failure(e) } .observeOn(uiScheduler) .startWith(Loading) } .subscribe { state -> submitBtn.isEnabled = state !is Loading progressView.visibility = if (state is Loading) VISIBLE else GONE when (state) { is Success -> showData(state.data) is Failure -> toast("Search failed") } }
  34. REACTIVE STATE val actions = submitBtn.clicks() .map { SearchAction(searchView.text.toString()) }

    actions.flatMap { action -> api.search(action.query) .map<UiState> { result -> Success(result) } .onErrorReturn { e -> Failure(e) } .observeOn(uiScheduler) .startWith(Loading) } .subscribe { state -> submitBtn.isEnabled = state !is Loading progressView.visibility = if (state is Loading) VISIBLE else GONE when (state) { is Success -> showData(state.data) is Failure -> toast("Search failed") } }
  35. REACTIVE STATE val actions = submitBtn.clicks() .map { SearchAction(searchView.text.toString()) }

    val states = actions.flatMap { action -> api.search(action.query) .map<UiState> { result -> Success(result) } .onErrorReturn { e -> Failure(e) } .observeOn(uiScheduler) .startWith(Loading) } states.subscribe { state -> submitBtn.isEnabled = state !is Loading progressView.visibility = if (state is Loading) VISIBLE else GONE when (state) { is Success -> showData(state.data) is Failure -> toast("Search failed") } }
  36. REACTIVE STATE val actions = submitBtn.clicks() .map { SearchAction(searchView.text.toString()) }

    val states = actions.flatMap { action -> api.search(action.query) .map<UiState> { result -> Success(result) } .onErrorReturn { e -> Failure(e) } .observeOn(uiScheduler) .startWith(Loading) } states.subscribe(::render) private fun render(state: UiState) { submitBtn.isEnabled = state !is Loading progressView.visibility = if (state is Loading) VISIBLE else GONE when (state) { is Success -> finish() is Failure -> toast("Search failed") }
  37. REACTIVE STATE class SearchComponent(private val api: Api, val uiScheduler: Scheduler)

    { fun bind(actions: Observable<SearchAction>): Observable<UiState> { return actions.flatMap { action -> api.search(action.query) .map<UiState> { result -> Success(result) } .onErrorReturn { e -> Failure(e) } .observeOn(uiScheduler) .startWith(Loading) } } }
  38. REACTIVE STATE class SearchComponent(private val api: Api, val uiScheduler: Scheduler)

    { fun bind(actions: Observable<SearchAction>): Observable<UiState> { return actions.flatMap { action -> api.search(action.query) .map<UiState> { result -> Success(result) } .onErrorReturn { e -> Failure(e) } .observeOn(uiScheduler) .startWith(Loading) } } } // View part searchComponent.bind(actions).subscribe(::render)
  39. None
  40. SearchAction UiState

  41. searchComponent.bind(actions).subscribe(::render)

  42. render( state( actions() ) ) searchComponent.bind(actions).subscribe(::render)

  43. What if we have more complex logic?

  44. What if we have more complex logic? How about two

    network calls?
  45. REACTIVE STATE Fanta| SUBMIT Fantastic Four Fantastic Beasts Final Fantasy

  46. sealed class UiAction : Action { class SearchAction(val query: String)

    : UiAction() }
  47. sealed class UiAction : Action { class SearchAction(val query: String)

    : UiAction() class LoadSuggestionsAction(val query: String) : UiAction() }
  48. sealed class UiState { object Loading : UiState() class Success(val

    data: Data) : UiState() class Failure(val error: Throwable) : UiState() }
  49. class UiState( val loading: Boolean = false, val data: String?

    = null, val error: Throwable? = null, val suggestions: List<String>? = null )
  50. sealed class InternalAction : Action { object SearchLoadingAction : InternalAction()

    class SearchSuccessAction(val data: String) : InternalAction() class SearchFailureAction(val error: Throwable) : InternalAction() class SuggestionsLoadedAction(val suggestions: List<String>) : InternalAction() }
  51. sealed class InternalAction : Action { object SearchLoadingAction : InternalAction()

    class SearchSuccessAction(val data: String) : InternalAction() class SearchFailureAction(val error: Throwable) : InternalAction() class SuggestionsLoadedAction(val suggestions: List<String>) : InternalAction() }
  52. fun bind(actions: Observable<Action>): Observable<UiState> { ... }

  53. fun bind(actions: Observable<Action>): Observable<UiState> { ... } .ofType<ActionA> .publish Observable<Action>

    Observable<Result> .ofType<ActionB> .merge … …
  54. fun bind(actions: Observable<Action>): Observable<UiState> { return actions.publish { shared ->

    Observable.merge<Action>() } } .ofType<ActionA> .publish Observable<Action> Observable<Result> .ofType<ActionB> .merge … …
  55. fun bind(actions: Observable<Action>): Observable<UiState> { return actions.publish { shared ->

    Observable.merge<Action>( bind(shared.ofType<SearchAction>()), bind(shared.ofType<LoadSuggestionsAction>())) } } .ofType<ActionA> .publish Observable<Action> Observable<Result> .ofType<ActionB> .merge … …
  56. fun bind(actions: Observable<SearchAction>): Observable<InternalAction> { return actions.flatMap { action ->

    api.search(action.query) .map { result -> SearchSuccessAction(result) } .onErrorReturn { e -> SearchFailureAction(e) } .observeOn(uiScheduler) .startWith(SearchLoadingAction) } } fun bind(actions: Observable<LoadSuggestionsAction>): Observable<InternalAction> { return actions.flatMap { action -> api.suggestions(action.query) .onErrorReturnItem(emptyList()) .map { result -> SuggestionsLoadedAction(result) } .observeOn(uiScheduler) } }
  57. fun bind(actions: Observable<SearchAction>): Observable<InternalAction> { return actions.flatMap { action ->

    api.search(action.query) .map { result -> SearchSuccessAction(result) } .onErrorReturn { e -> SearchFailureAction(e) } .observeOn(uiScheduler) .startWith(SearchLoadingAction) } } fun bind(actions: Observable<LoadSuggestionsAction>): Observable<InternalAction> { return actions.flatMap { action -> api.suggestions(action.query) .onErrorReturnItem(emptyList()) .map { result -> SuggestionsLoadedAction(result) } .observeOn(uiScheduler) } }
  58. fun bind(actions: Observable<Action>): Observable<UiState> { return actions.publish { shared ->

    Observable.merge<Action>( bind(shared.ofType<SearchAction>()), bind(shared.ofType<LoadSuggestionsAction>())) } }
  59. fun bind(actions: Observable<Action>): Observable<UiState> { return actions.publish { shared ->

    Observable.merge<Action>( bind(shared.ofType<SearchAction>()), bind(shared.ofType<LoadSuggestionsAction>())) } .scan(UiState()) { state, action -> ... } }
  60. bind(shared.ofType<LoadSuggestionsAction>())) } .scan(UiState()) { state, action -> when (action) {

    SearchLoadingAction -> state.copy( loading = true, error = null, suggestions = null) is SearchSuccessAction -> state.copy( loading = false, data = newData, error = null, suggestions = null) is SearchFailureAction -> state.copy( loading = false, error = action.error) is SuggestionsLoadedAction -> state.copy( suggestions = action.suggestions) is SearchAction, is LoadSuggestionsAction -> state } } }
  61. bind(shared.ofType<LoadSuggestionsAction>())) } .scan(UiState()) { state, action -> when (action) {

    SearchLoadingAction -> state.copy( loading = true, error = null, suggestions = null) is SearchSuccessAction -> state.copy( loading = false, data = newData, error = null, suggestions = null) is SearchFailureAction -> state.copy( loading = false, error = action.error) is SuggestionsLoadedAction -> state.copy( suggestions = action.suggestions) is SearchAction, is LoadSuggestionsAction -> state } } }
  62. bind(shared.ofType<LoadSuggestionsAction>())) } .scan(UiState()) { state, action -> when (action) {

    SearchLoadingAction -> state.copy( loading = true, error = null, suggestions = null) is SearchSuccessAction -> state.copy( loading = false, data = newData, error = null, suggestions = null) is SearchFailureAction -> state.copy( loading = false, error = action.error) is SuggestionsLoadedAction -> state.copy( suggestions = action.suggestions) is SearchAction, is LoadSuggestionsAction -> state } } }
  63. bind(shared.ofType<LoadSuggestionsAction>())) } .scan(UiState()) { state, action -> when (action) {

    SearchLoadingAction -> state.copy( loading = true, error = null, suggestions = null) is SearchSuccessAction -> state.copy( loading = false, data = newData, error = null, suggestions = null) is SearchFailureAction -> state.copy( loading = false, error = action.error) is SuggestionsLoadedAction -> state.copy( suggestions = action.suggestions) is SearchAction, is LoadSuggestionsAction -> state } } }
  64. bind(shared.ofType<LoadSuggestionsAction>())) } .scan(UiState()) { state, action -> when (action) {

    SearchLoadingAction -> state.copy( loading = true, error = null, suggestions = null) is SearchSuccessAction -> state.copy( loading = false, data = newData, error = null, suggestions = null) is SearchFailureAction -> state.copy( loading = false, error = action.error) is SuggestionsLoadedAction -> state.copy( suggestions = action.suggestions) is SearchAction, is LoadSuggestionsAction -> state } } }
  65. bind(shared.ofType<LoadSuggestionsAction>())) } .scan(UiState()) { state, action -> when (action) {

    SearchLoadingAction -> state.copy( loading = true, error = null, suggestions = null) is SearchSuccessAction -> state.copy( loading = false, data = newData, error = null, suggestions = null) is SearchFailureAction -> state.copy( loading = false, error = action.error) is SuggestionsLoadedAction -> state.copy( suggestions = action.suggestions) is SearchAction, is LoadSuggestionsAction -> state } } }
  66. REACTIVE STATE class SearchComponent(private val api: Api, private val uiScheduler:

    Scheduler) { fun bind(actions: Observable<Action>): Observable<UiState> { return actions.publish { shared -> Observable.merge<Action>( bind(shared.ofType<SearchAction>()), bind(shared.ofType<LoadSuggestionsAction>())) } .scan(UiState()) { state, action -> when (action) { SearchLoadingAction -> state.copy(...) is SearchSuccessAction -> state.copy(...) is SearchFailureAction -> state.copy(...) is SuggestionsLoadedAction -> state.copy(...) is SearchAction, is LoadSuggestionsAction -> state } } } }
  67. UNIDIRECTIONAL DATA FLOW

  68. UNIDIRECTIONAL DATA FLOW ▸ Redux

  69. UNIDIRECTIONAL DATA FLOW ▸ Redux ▸ Cycle.js

  70. UNIDIRECTIONAL DATA FLOW ▸ Redux ▸ Cycle.js ▸ Flux

  71. UNIDIRECTIONAL DATA FLOW ▸ Redux ▸ Cycle.js ▸ Flux ▸

    Elm
  72. ELM COMPONENT

  73. ELM COMPONENT

  74. ELM COMPONENT <Action, State>

  75. ELM COMPONENT Update <Action, State>

  76. ELM COMPONENT Update <Action, State> <newState>

  77. ELM COMPONENT Update <Action, State> <newState> Render

  78. ELM COMPONENT Update <Action, State> <newState> Render

  79. ELM COMPONENT Update <Action, State> <newState> Render <SideEffect>

  80. ELM COMPONENT Update <Action, State> <newState> Render Call <SideEffect>

  81. ELM COMPONENT Update <Action, State> <newState> Render Call <SideEffect> <Action>

  82. ELM COMPONENT Update <Action, State> <newState> Render Call <SideEffect> <Action>

  83. ELM COMPONENT Update <Action, State> <newState> Render Call <SideEffect> <Action>

  84. ELM

  85. ELM ▸ Update

  86. ELM ▸ Update ▸ Call

  87. ELM ▸ Update ▸ Call ▸ Component

  88. ELM - REDUX ▸ Update -> Reducer ▸ Call ▸

    Component
  89. ELM - REDUX ▸ Update -> Reducer ▸ Call ->

    Middleware ▸ Component
  90. ELM - REDUX ▸ Update -> Reducer ▸ Call ->

    Middleware ▸ Component -> Store
  91. REACTIVE STATE class SearchComponent(private val api: Api, private val uiScheduler:

    Scheduler) { fun bind(actions: Observable<Action>): Observable<UiState> { return actions.publish { shared -> Observable.merge<Action>( bind(shared.ofType<SearchAction>()), bind(shared.ofType<LoadSuggestionsAction>())) } .scan(UiState()) { state, action -> when (action) { SearchLoadingAction -> state.copy(...) is SearchSuccessAction -> state.copy(...) is SearchFailureAction -> state.copy(...) is SuggestionsLoadedAction -> state.copy(...) is SearchAction, is LoadSuggestionsAction -> state } } } }
  92. REACTIVE STATE class SearchComponent(private val api: Api, private val uiScheduler:

    Scheduler) { fun bind(actions: Observable<Action>): Observable<UiState> { return actions.publish { shared -> Observable.merge<Action>( bind(shared.ofType<SearchAction>()), bind(shared.ofType<LoadSuggestionsAction>())) } .scan(UiState()) { state, action -> when (action) { SearchLoadingAction -> state.copy(...) is SearchSuccessAction -> state.copy(...) is SearchFailureAction -> state.copy(...) is SuggestionsLoadedAction -> state.copy(...) is SearchAction, is LoadSuggestionsAction -> state } } } } Reducer
  93. REACTIVE STATE class SearchComponent(private val api: Api, private val uiScheduler:

    Scheduler) { fun bind(actions: Observable<Action>): Observable<UiState> { return actions.publish { shared -> Observable.merge<Action>( bind(shared.ofType<SearchAction>()), bind(shared.ofType<LoadSuggestionsAction>())) } .scan(UiState()) { state, action -> when (action) { SearchLoadingAction -> state.copy(...) is SearchSuccessAction -> state.copy(...) is SearchFailureAction -> state.copy(...) is SuggestionsLoadedAction -> state.copy(...) is SearchAction, is LoadSuggestionsAction -> state } } } } Reducer Middleware
  94. REACTIVE STATE class SearchComponent(private val api: Api, private val uiScheduler:

    Scheduler) { fun bind(actions: Observable<Action>): Observable<UiState> { return actions.publish { shared -> Observable.merge<Action>( bind(shared.ofType<SearchAction>()), bind(shared.ofType<LoadSuggestionsAction>())) } .scan(UiState()) { state, action -> when (action) { SearchLoadingAction -> state.copy(...) is SearchSuccessAction -> state.copy(...) is SearchFailureAction -> state.copy(...) is SuggestionsLoadedAction -> state.copy(...) is SearchAction, is LoadSuggestionsAction -> state } } } } Reducer Middleware Store
  95. REDUCER interface Reducer<S, A> { fun reduce(state: S, action: A):

    S }
  96. REDUCER class SearchReducer : Reducer<UiState, Action> { override fun reduce(state:

    UiState, action: Action): UiState { return when (action) { SearchLoadingAction -> state.copy(...) is SearchSuccessAction -> state.copy(...) is SearchFailureAction -> state.copy(...) is SuggestionsLoadedAction -> state.copy(...) is SearchAction, is LoadSuggestionsAction -> state } } }
  97. MIDDLEWARE interface Middleware<A, S> { fun bind(actions: Observable<A>, state: Observable<S>):

    Observable<A> }
  98. MIDDLEWARE class SearchMiddleware(val api: Api) : Middleware<Action, UiState> { override

    fun bind(actions: Observable<Action>): Observable<Action> { return actions.ofType<SearchAction>() .flatMap { action -> api.search(action.query) .map<InternalAction> { result -> SearchSuccessAction(result) } .onErrorReturn { e -> SearchFailureAction(e) } .startWith(SearchLoadingAction) } } }
  99. REACTIVE STATE class SearchMiddleware(val api: Api) : Middleware<Action, UiState> {

    override fun bind(actions: Observable<Action>, state: Observable<UiState>) : Observable<Action> { return actions.ofType<SearchAction>() .withLatestFrom(state) { action, currentState -> action to currentState } .flatMap { (action, state) -> api.search(action.query) .map<InternalAction> { result -> SearchSuccessAction(result) } .onErrorReturn { e -> SearchFailureAction(e) } .startWith(SearchLoadingAction) } } }
  100. REACTIVE STATE class SearchMiddleware(val api: Api) : Middleware<Action, UiState> {

    override fun bind(actions: Observable<Action>, state: Observable<UiState>) : Observable<Action> { return actions.ofType<SearchAction>() .withLatestFrom(state) { action, currentState -> action to currentState } .flatMap { (action, state) -> api.search(action.query) .map<InternalAction> { result -> SearchSuccessAction(result) } .onErrorReturn { e -> SearchFailureAction(e) } .startWith(SearchLoadingAction) } } }
  101. STORE - SEARCH COMPONENT

  102. STORE - SEARCH COMPONENT private val state = BehaviorRelay.createDefault<UiState>(UiState()) private

    val actions = PublishRelay.create<Action>()
  103. STORE - SEARCH COMPONENT private val state = BehaviorRelay.createDefault<UiState>(UiState()) private

    val actions = PublishRelay.create<Action>() fun wire(): Disposable { val disposable = CompositeDisposable() disposable += actions .withLatestFrom(state) { action, state -> SearchReducer().reduce(state, action) } .subscribe(state::accept) return disposable }
  104. STORE - SEARCH COMPONENT private val state = BehaviorRelay.createDefault<UiState>(UiState()) private

    val actions = PublishRelay.create<Action>() fun wire(): Disposable { val disposable = CompositeDisposable() disposable += actions .withLatestFrom(state) { action, state -> SearchReducer().reduce(state, action) } .distinctUntilChanged() .subscribe(state::accept) return disposable }
  105. STORE - SEARCH COMPONENT private val state = BehaviorRelay.createDefault<UiState>(UiState()) private

    val actions = PublishRelay.create<Action>() fun wire(): Disposable { val disposable = CompositeDisposable() disposable += actions .withLatestFrom(state) { action, state -> SearchReducer().reduce(state, action) } .distinctUntilChanged() .subscribe(state::accept) disposable += Observable.merge<Action>( SearchMiddleware(api).bind(actions, state), SuggestionsMiddleware(api).bind(actions, state) ).subscribe(actions::accept) return disposable }
  106. STORE - SEARCH COMPONENT private val state = BehaviorRelay.createDefault<UiState>(UiState()) private

    val actions = PublishRelay.create<Action>() fun wire(): Disposable { val disposable = CompositeDisposable() disposable += actions .withLatestFrom(state) { action, state -> SearchReducer().reduce(state, action) } .distinctUntilChanged() .subscribe(state::accept) disposable += Observable.merge<Action>( SearchMiddleware(api).bind(actions, state), SuggestionsMiddleware(api).bind(actions, state) ).subscribe(actions::accept) return disposable }
  107. STORE fun bind(actions: Observable<Action>, render: (UiState) -> Unit): Disposable {

    val disposable = CompositeDisposable() disposable += state.observeOn(uiScheduler).subscribe(render) disposable += actions.subscribe(actions::accept) return disposable }
  108. STORE fun bind(actions: Observable<Action>, render: (UiState) -> Unit): Disposable {

    val disposable = CompositeDisposable() disposable += state.observeOn(uiScheduler).subscribe(render) disposable += actions.subscribe(actions::accept) return disposable }
  109. STORE interface MviView<A, S> { val actions: Observable<A> fun render(state:

    S) } // SearchComponent fun bind(actions: Observable<Action>, render: (UiState) -> Unit): Disposable { val disposable = CompositeDisposable() disposable += state.observeOn(uiScheduler).subscribe(render) disposable += actions.subscribe(actions::accept) return disposable }
  110. STORE interface MviView<A, S> { val actions: Observable<A> fun render(state:

    S) } // SearchComponent fun bind(view: MviView<Action, UiState>): Disposable { val disposable = CompositeDisposable() disposable += state.observeOn(uiScheduler).subscribe(view::render) disposable += view.actions.subscribe(actions::accept) return disposable }
  111. STORE interface MviView<A, S> { val actions: Observable<A> fun render(state:

    S) } // SearchComponent fun bind(view: MviView<Action, UiState>): Disposable { val disposable = CompositeDisposable() disposable += state.observeOn(uiScheduler).subscribe(view::render) disposable += view.actions.subscribe(actions::accept) return disposable }
  112. STORE private val state = BehaviorRelay.createDefault<UiState>(UiState()) private val actions =

    PublishRelay.create<Action>() fun wire(): Disposable { val disposable = CompositeDisposable() disposable += actions .withLatestFrom(state) { action, state -> SearchReducer().reduce(state, action) } .distinctUntilChanged() .subscribe(state::accept) disposable += Observable.merge<Action>( SearchMiddleware(api).bind(actions, state), SuggestionsMiddleware(api).bind(actions, state) ).subscribe(actions::accept) return disposable }
  113. STORE private val state = BehaviorRelay.createDefault<UiState>(UiState()) private val actions =

    PublishRelay.create<Action>() fun wire(): Disposable { val disposable = CompositeDisposable() disposable += actions .withLatestFrom(state) { action, state -> SearchReducer().reduce(state, action) } .distinctUntilChanged() .subscribe(state::accept) disposable += Observable.merge<Action>( SearchMiddleware(api).bind(actions, state), SuggestionsMiddleware(api).bind(actions, state) ).subscribe(actions::accept) return disposable }
  114. STORE private val state = BehaviorRelay.createDefault<UiState>(initialState) private val actions =

    PublishRelay.create<Action>() fun wire(): Disposable { val disposable = CompositeDisposable() disposable += actions .withLatestFrom(state) { action, state -> reducer.reduce(state, action) } .distinctUntilChanged() .subscribe(state::accept) disposable += Observable.merge<Action>( middlewares.map { it.bind(actions, state) } ).subscribe(actions::accept) return disposable }
  115. STORE class SearchComponent( private val reducer: Reducer<UiState, Action>, private val

    middlewares: List<Middleware<Action, UiState>>, private val initialState: UiState )
  116. class Store<A, S>( private val reducer: Reducer<S, A>, private val

    middlewares: List<Middleware<A, S>>, private val initialState: S ) STORE
  117. CORE interface MviView<A, S> { val actions: Observable<A> fun render(state:

    S) } interface Reducer<S, A> { fun reduce(state: S, action: A): S } interface Middleware<A, S> { fun bind(actions: Observable<A>, state: Observable<S>): Observable<A> } class Store<A, S>( private val reducer: Reducer<S, A>, private val middlewares: List<Middleware<A, S>>, private val initialState: S ) { fun wire(): Disposable {} fun bind(view: MviView<Action, UiState>): Disposable {} }
  118. BIND IT ALL!

  119. BIND IT ALL! ▸ Android Arch ViewModels

  120. BIND IT ALL! ▸ Android Arch ViewModels ▸ DI Scopes

  121. BIND IT ALL! ▸ Android Arch ViewModels ▸ DI Scopes

    ▸ Others
  122. BIND IT ALL! class SearchViewModel<A, S> @Inject constructor (private val

    store: Store<A, S>) : ViewModel() { private val wiring = store.wire() private var viewBinding: Disposable? = null override fun onCleared() { wiring.dispose() } fun bind(view: MviView<A, S>) { viewBinding = store.bind(view) } fun unbind() { viewBinding?.dispose() } }
  123. BIND IT ALL! class SearchViewModel<A, S> @Inject constructor (private val

    store: Store<A, S>) : ViewModel() { private val wiring = store.wire() private var viewBinding: Disposable? = null override fun onCleared() { wiring.dispose() } fun bind(view: MviView<A, S>) { viewBinding = store.bind(view) } fun unbind() { viewBinding?.dispose() } }
  124. BIND IT ALL! class SearchViewModel<A, S> @Inject constructor (private val

    store: Store<A, S>) : ViewModel() { private val wiring = store.wire() private var viewBinding: Disposable? = null override fun onCleared() { wiring.dispose() } fun bind(view: MviView<A, S>) { viewBinding = store.bind(view) } fun unbind() { viewBinding?.dispose() } }
  125. BIND IT ALL! class SearchViewModel<A, S> @Inject constructor (private val

    store: Store<A, S>) : ViewModel() { private val wiring = store.wire() private var viewBinding: Disposable? = null override fun onCleared() { wiring.dispose() } fun bind(view: MviView<A, S>) { viewBinding = store.bind(view) } fun unbind() { viewBinding?.dispose() } }
  126. OUR UNIDIRECTIONAL DATA FLOW

  127. OUR UNIDIRECTIONAL DATA FLOW ::render Middleware Middleware Middleware Reducer

  128. OUR UNIDIRECTIONAL DATA FLOW ::render Middleware Middleware Middleware Reducer SearchAction

  129. OUR UNIDIRECTIONAL DATA FLOW < , > SearchLoadingAction State.Empty ::render

    Middleware Middleware Middleware Reducer State.Empty SearchAction
  130. OUR UNIDIRECTIONAL DATA FLOW < , > SearchLoadingAction State.Empty ::render

    Middleware Middleware Middleware Reducer State.Empty SearchAction
  131. OUR UNIDIRECTIONAL DATA FLOW < , > SearchLoadingAction State.Empty ::render

    Middleware Middleware Middleware Reducer State.Empty SearchAction
  132. OUR UNIDIRECTIONAL DATA FLOW < , > SearchLoadingAction ::render Middleware

    Middleware Middleware Reducer State.Empty State(loading = true)
  133. OUR UNIDIRECTIONAL DATA FLOW < , > SearchLoadingAction ::render Middleware

    Middleware Middleware Reducer State.Empty State(loading = true)
  134. OUR UNIDIRECTIONAL DATA FLOW < , > SearchLoadingAction State(loading =

    true) SearchSuccessAction ::render Middleware Middleware Middleware Reducer
  135. OUR UNIDIRECTIONAL DATA FLOW < , > SearchLoadingAction State(loading =

    true) SearchSuccessAction ::render Middleware Middleware Middleware Reducer
  136. OUR UNIDIRECTIONAL DATA FLOW < , > SearchLoadingAction State(loading =

    true) SearchSuccessAction ::render Middleware Middleware Middleware Reducer
  137. OUR UNIDIRECTIONAL DATA FLOW < , > SearchSuccessAction ::render Middleware

    Middleware Middleware Reducer State(loading = true) State(loading = false, 
 data = newData)
  138. OUR UNIDIRECTIONAL DATA FLOW < , > SearchSuccessAction ::render Middleware

    Middleware Middleware Reducer State(loading = true) State(loading = false, 
 data = newData)
  139. BACK TO REALITY

  140. BACK TO REALITY ▸ Rendering Overflow

  141. ANY PROBLEMS WITH UI?

  142. ANY PROBLEMS WITH UI? ▸ Lots of State updates

  143. ANY PROBLEMS WITH UI? ▸ Lots of State updates ▸

    May cause lots of redundant UI rerendering
  144. ANY PROBLEMS WITH UI? ▸ Lots of State updates ▸

    May cause lots of redundant UI rerendering ▸ …
  145. ANY PROBLEMS WITH UI? ▸ Lots of State updates ▸

    May cause lots of redundant UI rerendering ▸ … ▸ Domic!
  146. WHAT IS DOMIC

  147. WHAT IS DOMIC ▸ Diffing changes

  148. WHAT IS DOMIC ▸ Diffing changes ▸ Same Android Widgets

  149. WHAT IS DOMIC ▸ Diffing changes ▸ Same Android Widgets

    ▸ Threading
  150. WHAT IS DOMIC ▸ Diffing changes ▸ Same Android Widgets

    ▸ Threading
  151. BACK TO REALITY ▸ Rendering Overflow ▸ Testing

  152. TESTING

  153. TESTING VIEW // In val observer = TestObserver.create<UiAction>() realView.actions.subscribe(observer) onView(withId(R.id.search_edit)).perform(typeText("Query"))

    onView(withId(R.id.submit_btn)).perform(click()) observer.assertValue(SearchAction("Query")) // Out val uiState = UiState(loading = false, data = "TestData") realView.render(uiState) takeScreenshot()
  154. TESTING LOGIC val viewModel = provide<SearchViewModel<Action, UiState>> viewModel.bind(fakeView) actions.onNext(SearchAction("Query")) states.assertValue(UiState(loading

    = true)) viewModel.unbind() actions.onNext(SearchAction("AnotherQuery")) states.assertNoValues()
  155. BACK TO REALITY ▸ Rendering Overflow ▸ Testing ▸ Paging

  156. youtu.be/h5afEeuI0GQ PAGING

  157. BACK TO REALITY ▸ Rendering Overflow ▸ Testing ▸ Paging

    ▸ SingleLiveEvents
  158. SINGLE LIVE EVENTS

  159. SINGLE LIVE EVENTS <Action, State> <newState> Render Call <SideEffect> <Action>

    Update
  160. SINGLE LIVE EVENTS Subscriptions <Action, State> <newState> Render Call <SideEffect>

    <Action> Update
  161. BACK TO REALITY ▸ Rendering Overflow ▸ Testing ▸ Paging

    ▸ SingleLiveEvents ▸ Clean Architecture
  162. DO YOU EVEN CLEAN?

  163. DO YOU EVEN CLEAN? MviView Api Store Reducer Middleware Middleware

    Middleware
  164. DO YOU EVEN CLEAN? MviView Store Reducer Middleware Middleware Middleware

    Repository
  165. DO YOU EVEN CLEAN? MviView Repository Store Reducer Middleware Middleware

    Middleware
  166. DO YOU EVEN CLEAN? MviView Repository Store Reducer Middleware Middleware

    Middleware Interactor?
  167. DO YOU EVEN CLEAN? MviView Repository Store Reducer Middleware Middleware

    Middleware Presentation Layer Domain Layer Data Layer
  168. TO WRAP UP

  169. TO WRAP UP ▸ MVI is about presentation logic

  170. TO WRAP UP ▸ MVI is about presentation logic ▸

    Reactive flow
  171. TO WRAP UP ▸ MVI is about presentation logic ▸

    Reactive flow ▸ Sealed classes for Actions & States
  172. TO WRAP UP ▸ MVI is about presentation logic ▸

    Reactive flow ▸ Sealed classes for Actions & States ▸ Kotlin multiplatform usage options
  173. TO WRAP UP ▸ MVI is about presentation logic ▸

    Reactive flow ▸ Sealed classes for Actions & States ▸ Kotlin multiplatform usage options ▸ RxKotlin (for real)
  174. TO WRAP UP ▸ MVI is about presentation logic ▸

    Reactive flow ▸ Sealed classes for Actions & States ▸ Kotlin multiplatform usage options ▸ RxKotlin (for real) ▸ Reagent-like
  175. TO WRAP UP ▸ MVI is about presentation logic ▸

    Reactive flow ▸ Sealed classes for Actions & States ▸ Kotlin multiplatform usage options ▸ RxKotlin (for real) ▸ Reagent-like ▸ Time Travel Debugger
  176. UNIDIRECTIONAL DATA FLOW LIBS

  177. UNIDIRECTIONAL DATA FLOW LIBS ▸ RxRedux / Freeletics github.com/freeletics/RxRedux

  178. UNIDIRECTIONAL DATA FLOW LIBS ▸ RxRedux / Freeletics github.com/freeletics/RxRedux ▸

    Mobius / Spotify github.com/spotify/mobius
  179. UNIDIRECTIONAL DATA FLOW LIBS ▸ RxRedux / Freeletics github.com/freeletics/RxRedux ▸

    Mobius / Spotify github.com/spotify/mobius ▸ MvRx / AirBnb github.com/airbnb/MvRx
  180. UNIDIRECTIONAL DATA FLOW LIBS ▸ RxRedux / Freeletics github.com/freeletics/RxRedux ▸

    Mobius / Spotify github.com/spotify/mobius ▸ MvRx / AirBnb github.com/airbnb/MvRx ▸ MVICore / Badoo github.com/badoo/MVICore
  181. UNIDIRECTIONAL DATA FLOW LIBS ▸ RxRedux / Freeletics github.com/freeletics/RxRedux ▸

    Mobius / Spotify github.com/spotify/mobius ▸ MvRx / AirBnb github.com/airbnb/MvRx ▸ MVICore / Badoo github.com/badoo/MVICore ▸ Grox / Groupon github.com/groupon/grox
  182. UNIDIRECTIONAL DATA FLOW LIBS ▸ RxRedux / Freeletics github.com/freeletics/RxRedux ▸

    Mobius / Spotify github.com/spotify/mobius ▸ MvRx / AirBnb github.com/airbnb/MvRx ▸ MVICore / Badoo github.com/badoo/MVICore ▸ Grox / Groupon github.com/groupon/grox ▸ Suas / Zendesk github.com/zendesk/Suas-Android
  183. MVICORE

  184. MVICORE ▸ Wish

  185. MVICORE ▸ Wish ▸ Effect

  186. MVICORE ▸ Wish ▸ Effect ▸ Actor

  187. MVICORE ▸ Wish ▸ Effect ▸ Actor ▸ News

  188. MVICORE ▸ Wish ▸ Effect ▸ Actor ▸ News ▸

    Feature
  189. MVICORE ▸ Wish -> Action ▸ Effect ▸ Actor ▸

    News ▸ Feature
  190. MVICORE ▸ Wish -> Action ▸ Effect -> Internal Action

    ▸ Actor ▸ News ▸ Feature
  191. MVICORE ▸ Wish -> Action ▸ Effect -> Internal Action

    ▸ Actor -> Middleware ▸ News ▸ Feature
  192. MVICORE ▸ Wish -> Action ▸ Effect -> Internal Action

    ▸ Actor -> Middleware ▸ News -> Subscriptions ▸ Feature
  193. MVICORE ▸ Wish -> Action ▸ Effect -> Internal Action

    ▸ Actor -> Middleware ▸ News -> Subscriptions ▸ Feature -> Store
  194. ‣ Managing State with RxJava by Jake Wharton youtu.be/0IKHxjkgop4 ‣

    The Reactive Workflow Pattern by Ray Ryan youtu.be/mvBVkU2mCF4 ‣ Domic — Reactive Virtual DOM by Artem Zinnatullin youtu.be/Ce6phlHfKR8 ‣ Domic repo github.com/lyft/domic ‣ Reagent repo github.com/JakeWharton/Reagent LINKS
  195. How to cook a well done MVI for Android Sergey

    Ryabov @colriot