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

Sergey Ryabov

April 13, 2019
Tweet

More Decks by Sergey Ryabov

Other Decks in Programming

Transcript

  1. • Android Engineer, ex-Backend • 8 years of Android, 4

    years of Kotlin • Kotlin User Group SPb • Android Academy SPb & Msk
  2. PROBLEMS OF A MODERN APP ▸ Big codebases ▸ Lots

    of async interactions ▸ Size * Async => Something changes Somewhere and it all crapped up
  3. 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”
  4. COMMON STATE ▸ Single source of truth ▸ Easy to

    check at any particular time ▸ Clear sequence of changes
  5. 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
  6. 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
  7. 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") } )
  8. 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") } )
  9. 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") } )
  10. 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() }
  11. 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") }. }<
  12. 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") }. }<
  13. 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") }. }<
  14. 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") }.
  15. 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) }< }< }<
  16. 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)
  17. sealed class UiAction : Action { class SearchAction(val query: String)

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

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

    = null, val error: Throwable? = null, val suggestions: List<String>? = null )
  20. 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() }
  21. 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() }
  22. fun bind(actions: Observable<Action>): Observable<UiState> { return actions.publish { shared ->

    Observable.merge<Action>() } } .ofType<ActionA> .publish Observable<Action> Observable<Result> .ofType<ActionB> .merge … …
  23. 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 … …
  24. 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) } }
  25. 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) } }
  26. fun bind(actions: Observable<Action>): Observable<UiState> { return actions.publish { shared ->

    Observable.merge<Action>( bind(shared.ofType<SearchAction>()), bind(shared.ofType<LoadSuggestionsAction>())) } }
  27. 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 -> ... } }
  28. 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 } } }
  29. 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 } } }
  30. 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 } } }
  31. 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 } } }
  32. 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 } } }
  33. 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 } } }
  34. 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 } } } }
  35. ELM

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

    Middleware ▸ Component -> Store
  37. 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 } } } }
  38. 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
  39. 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
  40. 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
  41. 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() }
  42. 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 } } }
  43. 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) } } }
  44. 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) } } }
  45. 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) } } }
  46. 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 }.
  47. 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 }.
  48. 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 }.
  49. 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 }
  50. 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 }
  51. 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 }.
  52. 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 }.
  53. 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 }
  54. 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 }
  55. 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 }
  56. 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 }
  57. 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 }
  58. 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 }
  59. 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 }
  60. 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 }
  61. STORE class SearchComponent( private val reducer: Reducer<UiState, Action>, private val

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

    middlewares: List<Middleware<A, S>>, private val initialState: S ) STORE
  63. 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 {} }
  64. 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() } }
  65. 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() } }
  66. 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() } }
  67. 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() } }
  68. OUR UNIDIRECTIONAL DATA FLOW < , > SearchLoadingAction State.Empty ::render

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

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

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

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

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

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

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

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

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

    Middleware Middleware Reducer State(loading = true) State(loading = false, 
 data = newData)
  78. ANY PROBLEMS WITH UI? ▸ Lots of State updates ▸

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

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

    May cause lots of redundant UI rerendering ▸ … ▸ I have libs for you!
  81. ▸ Declarative ▸ Recycle all the things ▸ Async layout

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

    ▸ Same Android Widgets ▸ Diffing changes ▸ Reactive Litho Domic
  83. TESTING VIEW // Out val uiState = UiState(loading = false,

    data = "TestData") realView.render(uiState) takeScreenshot()
  84. DO YOU EVEN CLEAN? MviView Repository Store Reducer Middleware Middleware

    Middleware Presentation Layer Domain Layer Data Layer
  85. TO WRAP UP ▸ MVI is about presentation logic ▸

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

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

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

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

    Reactive flow ▸ Sealed & data classes for Actions & States ▸ Kotlin multiplatform usage options, but Rx ▸ RxKotlin Reaktive
  90. 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
  91. 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
  92. UNIDIRECTIONAL DATA FLOW LIBS ▸ RxRedux / Freeletics github.com/freeletics/RxRedux ▸

    Mobius / Spotify github.com/spotify/mobius ▸ MvRx / AirBnb github.com/airbnb/MvRx
  93. 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
  94. 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
  95. 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
  96. MVICORE ▸ Wish -> Action ▸ Effect -> Internal Action

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

    ▸ Actor -> Middleware ▸ Feature -> Store
  98. ‣ 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