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

How to cook a well done MVI for Android (GDG DevFest Pisa 2019)

How to cook a well done MVI for Android (GDG DevFest Pisa 2019)

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/Ls0uKLqNFz4

C50a1f407bc251b7395c0984be4327e9?s=128

Sergey Ryabov

April 13, 2019
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, ex-Backend • 8 years of Android, 4

    years of Kotlin • Kotlin User Group SPb • Android Academy SPb & Msk
  3. PROBLEMS OF A MODERN APP

  4. PROBLEMS OF A MODERN APP ▸ Big codebases

  5. PROBLEMS OF A MODERN APP ▸ Big codebases ▸ Lots

    of async interactions
  6. PROBLEMS OF A MODERN APP ▸ Big codebases ▸ Lots

    of async interactions ▸ Size * Async => Something changes Somewhere and it all crapped up
  7. PROBLEMS OF A MODERN APP ▸ Big codebases ▸ Lots

    of async interactions ▸ Size * Async => Something changes Somewhere and it all crapped up ▸ Pain In The Ass when looking for “where it all started to go wrong”
  8. COMMON STATE

  9. COMMON STATE ▸ Single source of truth

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

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

    check at any particular time ▸ Clear sequence of changes
  12. 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
  13. 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
  14. UNIDIRECTIONAL DATA FLOW

  15. UNIDIRECTIONAL DATA FLOW Intent

  16. UNIDIRECTIONAL DATA FLOW Model Intent

  17. UNIDIRECTIONAL DATA FLOW Model Intent View

  18. UNIDIRECTIONAL DATA FLOW Model Intent View

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

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

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

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

  23. REACTIVE STATE Search query SUBMIT

  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. 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") } )
  26. 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") } )
  27. None
  28. clicks()

  29. doOnNext()

  30. searchView.text

  31. doOnNext()

  32. subscribe()

  33. None
  34. 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() }
  35. 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") }. }<
  36. 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") }. }<
  37. 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") }. }<
  38. 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 -> showData(state.data) is Failure -> toast("Search failed") }.
  39. 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) }< }< }<
  40. 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)
  41. None
  42. SearchAction UiState

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

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

  45. What if we have more complex logic?

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

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

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

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

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

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

    = null, val error: Throwable? = null, val suggestions: List<String>? = null )
  52. 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() }
  53. 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() }
  54. fun bind(actions: Observable<Action>): Observable<UiState> { ... }

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

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

    Observable.merge<Action>() } } .ofType<ActionA> .publish Observable<Action> Observable<Result> .ofType<ActionB> .merge … …
  57. 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 … …
  58. 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) } }
  59. 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) } }
  60. fun bind(actions: Observable<Action>): Observable<UiState> { return actions.publish { shared ->

    Observable.merge<Action>( bind(shared.ofType<SearchAction>()), bind(shared.ofType<LoadSuggestionsAction>())) } }
  61. 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 -> ... } }
  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. 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 } } }
  67. 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 } } }
  68. 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 } } } }
  69. UNIDIRECTIONAL DATA FLOW

  70. UNIDIRECTIONAL DATA FLOW ▸ Redux

  71. UNIDIRECTIONAL DATA FLOW ▸ Redux ▸ Cycle.js

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

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

    Elm
  74. UNIDIRECTIONAL DATA FLOW ▸ Redux ▸ Cycle.js ▸ Flux ▸

    Elm programming language
  75. ELM COMPONENT

  76. ELM COMPONENT

  77. ELM COMPONENT <Action, State>

  78. ELM COMPONENT Update <Action, State>

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

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

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

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

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

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

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

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

  87. ELM

  88. ELM ▸ Update

  89. ELM ▸ Update ▸ Call

  90. ELM ▸ Update ▸ Call ▸ Component

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

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

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

    Middleware ▸ Component -> Store
  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 } } } }
  95. 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
  96. 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
  97. 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
  98. 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() }
  99. ELM COMPONENT Update <Action, State> <newState> Render Call <SideEffect> <Action>

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

    InternalAction
  101. REDUCER interface Reducer<S, A> { fun reduce(state: S, action: A):

    S }
  102. 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 } } }
  103. MIDDLEWARE interface Middleware<A, S> { fun bind(actions: Observable<A>, state: Observable<S>):

    Observable<A> }
  104. 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) } } }
  105. 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) } } }
  106. 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) } } }
  107. ELM COMPONENT Update <Action, State> <newState> Render Call <SideEffect> <Action>

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

  109. STORE - SEARCH COMPONENT

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

    val actions = PublishRelay.create<Action>()
  111. 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 }.
  112. 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 }.
  113. 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 }.
  114. 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 }
  115. 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 }
  116. 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 }.
  117. 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 }.
  118. 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 }
  119. 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 }
  120. 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 }
  121. 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 }
  122. 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 }
  123. 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 }
  124. 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 }
  125. 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 }
  126. STORE class SearchComponent( private val reducer: Reducer<UiState, Action>, private val

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

    middlewares: List<Middleware<A, S>>, private val initialState: S ) STORE
  128. 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 {} }
  129. BIND IT ALL!

  130. BIND IT ALL! ▸ Jetpack ViewModels

  131. BIND IT ALL! ▸ Jetpack ViewModels ▸ DI Scopes

  132. BIND IT ALL! ▸ Jetpack ViewModels ▸ DI Scopes ▸

    Others
  133. 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() } }
  134. 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() } }
  135. 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() } }
  136. 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() } }
  137. OUR UNIDIRECTIONAL DATA FLOW

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

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

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

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

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

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

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

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

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

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

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

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

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

  151. BACK TO REALITY ▸ Rendering Overflow

  152. ANY PROBLEMS WITH UI?

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

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

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

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

    May cause lots of redundant UI rerendering ▸ … ▸ I have libs for you!
  157. Litho

  158. ▸ Declarative Litho

  159. ▸ Declarative ▸ Recycle all the things Litho

  160. ▸ Declarative ▸ Recycle all the things ▸ Async layout

    Litho
  161. ▸ Declarative ▸ Recycle all the things ▸ Async layout

    Litho Domic
  162. ▸ Declarative ▸ Recycle all the things ▸ Async layout

    ▸ Same Android Widgets Litho Domic
  163. ▸ Declarative ▸ Recycle all the things ▸ Async layout

    ▸ Same Android Widgets ▸ Diffing changes Litho Domic
  164. ▸ Declarative ▸ Recycle all the things ▸ Async layout

    ▸ Same Android Widgets ▸ Diffing changes ▸ Reactive Litho Domic
  165. BACK TO REALITY ▸ Rendering Overflow

  166. BACK TO REALITY ▸ Rendering Overflow ▸ Testing

  167. TESTING

  168. 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"))
  169. TESTING VIEW // Out val uiState = UiState(loading = false,

    data = "TestData") realView.render(uiState) takeScreenshot()
  170. 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()
  171. BACK TO REALITY ▸ Rendering Overflow ▸ Testing ▸ Clean

    Architecture
  172. DO YOU EVEN CLEAN?

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

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

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

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

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

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

  179. TO WRAP UP ▸ MVI is about presentation logic

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

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

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

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

    Reactive flow ▸ Sealed & data classes for Actions & States ▸ Kotlin multiplatform usage options, but Rx
  184. TO WRAP UP ▸ MVI is about presentation logic ▸

    Reactive flow ▸ Sealed & data classes for Actions & States ▸ Kotlin multiplatform usage options, but Rx ▸ RxKotlin
  185. TO WRAP UP ▸ MVI is about presentation logic ▸

    Reactive flow ▸ Sealed & data classes for Actions & States ▸ Kotlin multiplatform usage options, but Rx ▸ RxKotlin Reaktive
  186. TO WRAP UP ▸ MVI is about presentation logic ▸

    Reactive flow ▸ Sealed & data classes for Actions & States ▸ Kotlin multiplatform usage options, but Rx ▸ RxKotlin Reaktive ▸ kotlinx.coroutines.flow
  187. TO WRAP UP ▸ MVI is about presentation logic ▸

    Reactive flow ▸ Sealed & data classes for Actions & States ▸ Kotlin multiplatform usage options, but Rx ▸ RxKotlin Reaktive ▸ kotlinx.coroutines.flow ▸ Time Travel Debugger
  188. UNIDIRECTIONAL DATA FLOW LIBS

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

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

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

    Mobius / Spotify github.com/spotify/mobius ▸ MvRx / AirBnb github.com/airbnb/MvRx
  192. 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
  193. 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
  194. 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
  195. MVICORE

  196. MVICORE ▸ Wish

  197. MVICORE ▸ Wish ▸ Effect

  198. MVICORE ▸ Wish ▸ Effect ▸ Actor

  199. MVICORE ▸ Wish ▸ Effect ▸ Actor ▸ Feature

  200. MVICORE ▸ Wish -> Action ▸ Effect ▸ Actor ▸

    Feature
  201. MVICORE ▸ Wish -> Action ▸ Effect -> Internal Action

    ▸ Actor ▸ Feature
  202. MVICORE ▸ Wish -> Action ▸ Effect -> Internal Action

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

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

    The Reactive Workflow Pattern by Ray Ryan youtu.be/mvBVkU2mCF4 ‣ Litho: A Declarative UI Framework for Android by Lucas Rocha youtu.be/uzCK4Vnme7o ‣ Litho Project fblitho.com ‣ Domic — Reactive Virtual DOM by Artem Zinnatullin youtu.be/Ce6phlHfKR8 ‣ Domic repo github.com/lyft/domic ‣ kotlinx-coroutines-flow kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow ‣ Reaktive repo github.com/badoo/Reaktive ‣ Reagent repo github.com/JakeWharton/Reagent ‣ Sample project github.com/colriot/talk-well-done-mvi ‣ Intro image nationalpostcom.files.wordpress.com/2018/03/gettyimages-594465522.jpg LINKS
  205. How to cook a well done MVI for Android Sergey

    Ryabov @colriot