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

How to MVI?

How to MVI?

Presented on April '18 at BlrDroid 101.
https://www.meetup.com/blrdroid/events/249149873/

Ragunath Jawahar

April 21, 2018
Tweet

More Decks by Ragunath Jawahar

Other Decks in Programming

Transcript

  1. How to MVI?
    Ragunath Jawahar
    ragunathjawahar / Medium • Twitter • GitHub

    View Slide

  2. *This statement is architecture / platform agnostic

    View Slide

  3. Why?
    Courtesy pexels.com/@goumbik

    View Slide

  4. Why?

    View Slide

  5. Background
    1. Several iterations since April ’17
    2. Maximize testability and minimize code
    3. Offline-first app with device capabilities
    4. Both Android and iOS

    View Slide

  6. Testing Goals
    UI Tests
    Instrumented Tests
    Unit Tests
    **Not to scale

    View Slide

  7. Testing Goals
    UI Tests
    Instrumented Tests
    Unit Tests
    **Not to scale

    View Slide

  8. Testing Goals
    UI Tests
    Instrumented Tests
    Unit Tests
    **Not to scale

    View Slide

  9. Testing Goals
    UI Tests
    Instrumented Tests
    Unit Tests
    **Not to scale

    View Slide

  10. Benefits
    1. Predictable
    2. Platform Agnostic
    3. User-centric
    4. Reactive (Responsive / Resilient / Elastic / Message Driven)
    5. Unidirectional

    View Slide

  11. Model
    Presenter
    View

    View Slide

  12. Toolbox (Subjective)
    • Kotlin
    • RxJava
    • RxAndroid
    • RxKotlin (Optional)
    • RxBinding
    • Retrofit
    • Room / SQL Brite
    • JUnit
    • Truth
    • Mockito
    • Espresso

    View Slide

  13. Model ● View ● Intention

    View Slide

  14. View
    Model
    Intention

    View Slide

  15. EVERYTHING IS A
    STREAM
    Courtesy parkerblog.wordpress.com

    View Slide

  16. Intention

    View Slide

  17. Intent(ion) is a component whose sole
    responsibility is to translate user input
    events into model-friendly events.
    —André Staltz

    View Slide

  18. Intent(ion) is a component whose sole
    responsibility is to translate user input
    events into model-friendly events.
    —André Staltz

    View Slide

  19. Intent(ion) is a component whose sole
    responsibility is to translate user input
    events into model-friendly events.
    —André Staltz

    View Slide

  20. View
    Model
    Intention

    View Slide

  21. View Slide

  22. View Slide

  23. CounterIntentions.kt

    View Slide

  24. class CounterIntentions(
    private val incrementClicks: Observable,
    private val incrementBy5Clicks: Observable,
    private val decrementClicks: Observable,
    private val resetClicks: Observable
    ) {
    fun increment(): Observable =
    incrementClicks.map { +1 }
    // increment by 5 …
    fun decrement(): Observable =
    decrementClicks.map { -1 }
    fun reset(): Observable =
    resetClicks
    }

    View Slide

  25. class CounterIntentions(
    private val incrementClicks: Observable,
    private val incrementBy5Clicks: Observable,
    private val decrementClicks: Observable,
    private val resetClicks: Observable
    ) {
    fun increment(): Observable =
    incrementClicks.map { +1 }
    // increment by 5 …
    fun decrement(): Observable =
    decrementClicks.map { -1 }
    fun reset(): Observable =
    resetClicks
    }

    View Slide

  26. class CounterIntentions(
    private val incrementClicks: Observable,
    private val incrementBy5Clicks: Observable,
    private val decrementClicks: Observable,
    private val resetClicks: Observable
    ) {
    fun increment(): Observable =
    incrementClicks.map { +1 }
    // increment by 5 …
    fun decrement(): Observable =
    decrementClicks.map { -1 }
    fun reset(): Observable =
    resetClicks
    }

    View Slide

  27. class CounterIntentions(
    private val incrementClicks: Observable,
    private val incrementBy5Clicks: Observable,
    private val decrementClicks: Observable,
    private val resetClicks: Observable
    ) {
    fun increment(): Observable =
    incrementClicks.map { +1 }
    // increment by 5 …
    fun decrement(): Observable =
    decrementClicks.map { -1 }
    fun reset(): Observable =
    resetClicks
    }

    View Slide

  28. class CounterIntentions(
    private val incrementClicks: Observable,
    private val incrementBy5Clicks: Observable,
    private val decrementClicks: Observable,
    private val resetClicks: Observable
    ) {
    fun increment(): Observable =
    incrementClicks.map { +1 }
    // increment by 5 …
    fun decrement(): Observable =
    decrementClicks.map { -1 }
    fun reset(): Observable =
    resetClicks
    }

    View Slide

  29. class CounterIntentions(
    private val incrementClicks: Observable,
    private val incrementBy5Clicks: Observable,
    private val decrementClicks: Observable,
    private val resetClicks: Observable
    ) {
    fun increment(): Observable =
    incrementClicks.map { +1 }
    // increment by 5 …
    fun decrement(): Observable =
    decrementClicks.map { -1 }
    fun reset(): Observable =
    resetClicks
    }

    View Slide

  30. class CounterIntentions(
    private val incrementClicks: Observable,
    private val incrementBy5Clicks: Observable,
    private val decrementClicks: Observable,
    private val resetClicks: Observable
    ) {
    fun increment(): Observable =
    incrementClicks.map { +1 }
    // increment by 5 …
    fun decrement(): Observable =
    decrementClicks.map { -1 }
    fun reset(): Observable =
    resetClicks
    }

    View Slide

  31. class CounterIntentions(
    private val incrementClicks: Observable,
    private val incrementBy5Clicks: Observable,
    private val decrementClicks: Observable,
    private val resetClicks: Observable
    ) {
    fun increment(): Observable =
    incrementClicks.map { +1 }
    // increment by 5 …
    fun decrement(): Observable =
    decrementClicks.map { -1 }
    fun reset(): Observable =
    resetClicks
    }

    View Slide

  32. class CounterIntentions(
    private val incrementClicks: Observable,
    private val incrementBy5Clicks: Observable,
    private val decrementClicks: Observable,
    private val resetClicks: Observable
    ) {
    fun increment(): Observable =
    incrementClicks.map { +1 }
    // increment by 5 …
    fun decrement(): Observable =
    decrementClicks.map { -1 }
    fun reset(): Observable =
    resetClicks
    }

    View Slide

  33. class CounterIntentions(
    private val incrementClicks: Observable,
    private val incrementBy5Clicks: Observable,
    private val decrementClicks: Observable,
    private val resetClicks: Observable
    ) {
    fun increment(): Observable =
    incrementClicks.map { +1 }
    // increment by 5 …
    fun decrement(): Observable =
    decrementClicks.map { -1 }
    fun reset(): Observable =
    resetClicks
    }

    View Slide

  34. class CounterIntentions(
    private val incrementClicks: Observable,
    private val incrementBy5Clicks: Observable,
    private val decrementClicks: Observable,
    private val resetClicks: Observable
    ) {
    fun increment(): Observable =
    incrementClicks.map { +1 }
    // increment by 5 …
    fun decrement(): Observable =
    decrementClicks.map { -1 }
    fun reset(): Observable =
    resetClicks
    }

    View Slide

  35. class CounterIntentions(
    private val incrementClicks: Observable,
    private val incrementBy5Clicks: Observable,
    private val decrementClicks: Observable,
    private val resetClicks: Observable
    ) {
    fun increment(): Observable =
    incrementClicks.map { +1 }
    // increment by 5 …
    fun decrement(): Observable =
    decrementClicks.map { -1 }
    fun reset(): Observable =
    resetClicks
    }

    View Slide

  36. class CounterIntentions(
    private val incrementClicks: Observable,
    private val incrementBy5Clicks: Observable,
    private val decrementClicks: Observable,
    private val resetClicks: Observable
    ) {
    fun increment(): Observable =
    incrementClicks.map { +1 }
    // increment by 5 …
    fun decrement(): Observable =
    decrementClicks.map { -1 }
    fun reset(): Observable =
    resetClicks
    }

    View Slide

  37. Testing?

    View Slide

  38. Model

    View Slide

  39. View
    Model
    Intention

    View Slide

  40. Courtesy 123Countries.com
    State Model

    View Slide

  41. (Single Atom) State
    • Represents the state of your model
    • Should be normalized
    • UI state can always be derived from SAS, but not
    the other way around

    View Slide

  42. State Reducer
    Reducer
    State
    New
    State
    Signal

    View Slide

  43. CounterState.kt

    View Slide

  44. @Parcelize data class CounterState(
    val counter: Int
    ) : Parcelable {
    companion object {
    val ZERO = CounterState(0)
    }
    fun add(number: Int): CounterState =
    this.copy(counter = counter + number)
    fun reset(): CounterState =
    CounterState.ZERO
    }

    View Slide

  45. @Parcelize data class CounterState(
    val counter: Int
    ) : Parcelable {
    companion object {
    val ZERO = CounterState(0)
    }
    fun add(number: Int): CounterState =
    this.copy(counter = counter + number)
    fun reset(): CounterState =
    CounterState.ZERO
    }

    View Slide

  46. @Parcelize data class CounterState(
    val counter: Int
    ) : Parcelable {
    companion object {
    val ZERO = CounterState(0)
    }
    fun add(number: Int): CounterState =
    this.copy(counter = counter + number)
    fun reset(): CounterState =
    CounterState.ZERO
    }

    View Slide

  47. @Parcelize data class CounterState(
    val counter: Int
    ) : Parcelable {
    companion object {
    val ZERO = CounterState(0)
    }
    fun add(number: Int): CounterState =
    this.copy(counter = counter + number)
    fun reset(): CounterState =
    CounterState.ZERO
    }

    View Slide

  48. @Parcelize data class CounterState(
    val counter: Int
    ) : Parcelable {
    companion object {
    val ZERO = CounterState(0)
    }
    fun add(number: Int): CounterState =
    this.copy(counter = counter + number)
    fun reset(): CounterState =
    CounterState.ZERO
    }

    View Slide

  49. @Parcelize data class CounterState(
    val counter: Int
    ) : Parcelable {
    companion object {
    val ZERO = CounterState(0)
    }
    fun add(number: Int): CounterState =
    this.copy(counter = counter + number)
    fun reset(): CounterState =
    CounterState.ZERO
    }

    View Slide

  50. @Parcelize data class CounterState(
    val counter: Int
    ) : Parcelable {
    companion object {
    val ZERO = CounterState(0)
    }
    fun add(number: Int): CounterState =
    this.copy(counter = counter + number)
    fun reset(): CounterState =
    CounterState.ZERO
    }

    View Slide

  51. @Parcelize data class CounterState(
    val counter: Int
    ) : Parcelable {
    companion object {
    val ZERO = CounterState(0)
    }
    fun add(number: Int): CounterState =
    this.copy(counter = counter + number)
    fun reset(): CounterState =
    CounterState.ZERO
    }

    View Slide

  52. @Parcelize data class CounterState(
    val counter: Int
    ) : Parcelable {
    companion object {
    val ZERO = CounterState(0)
    }
    fun add(number: Int): CounterState =
    this.copy(counter = counter + number)
    fun reset(): CounterState =
    CounterState.ZERO
    }

    View Slide

  53. @Parcelize data class CounterState(
    val counter: Int
    ) : Parcelable {
    companion object {
    val ZERO = CounterState(0)
    }
    fun add(number: Int): CounterState =
    this.copy(counter = counter + number)
    fun reset(): CounterState =
    CounterState.ZERO
    }

    View Slide

  54. @Parcelize data class CounterState(
    val counter: Int
    ) : Parcelable {
    companion object {
    val ZERO = CounterState(0)
    }
    fun add(number: Int): CounterState =
    this.copy(counter = counter + number)
    fun reset(): CounterState =
    CounterState.ZERO
    }

    View Slide

  55. @Parcelize data class CounterState(
    val counter: Int
    ) : Parcelable {
    companion object {
    val ZERO = CounterState(0)
    }
    fun add(number: Int): CounterState =
    this.copy(counter = counter + number)
    fun reset(): CounterState =
    CounterState.ZERO
    }

    View Slide

  56. @Parcelize data class CounterState(
    val counter: Int
    ) : Parcelable {
    companion object {
    val ZERO = CounterState(0)
    }
    fun add(number: Int): CounterState =
    this.copy(counter = counter + number)
    fun reset(): CounterState =
    CounterState.ZERO
    }

    View Slide

  57. @Parcelize data class CounterState(
    val counter: Int
    ) : Parcelable {
    companion object {
    val ZERO = CounterState(0)
    }
    fun add(number: Int): CounterState =
    this.copy(counter = counter + number)
    fun reset(): CounterState =
    CounterState.ZERO
    }

    View Slide

  58. @Parcelize data class CounterState(
    val counter: Int
    ) : Parcelable {
    companion object {
    val ZERO = CounterState(0)
    }
    fun add(number: Int): CounterState =
    this.copy(counter = counter + number)
    fun reset(): CounterState =
    CounterState.ZERO
    }

    View Slide

  59. @Parcelize data class CounterState(
    val counter: Int
    ) : Parcelable {
    companion object {
    val ZERO = CounterState(0)
    }
    fun add(number: Int): CounterState =
    this.copy(counter = counter + number)
    fun reset(): CounterState =
    CounterState.ZERO
    }

    View Slide

  60. @Parcelize data class CounterState(
    val counter: Int
    ) : Parcelable {
    companion object {
    val ZERO = CounterState(0)
    }
    fun add(number: Int): CounterState =
    this.copy(counter = counter + number)
    fun reset(): CounterState =
    CounterState.ZERO
    }

    View Slide

  61. @Parcelize data class CounterState(
    val counter: Int
    ) : Parcelable {
    companion object {
    val ZERO = CounterState(0)
    }
    fun add(number: Int): CounterState =
    this.copy(counter = counter + number)
    fun reset(): CounterState =
    CounterState.ZERO
    }

    View Slide

  62. @Parcelize data class CounterState(
    val counter: Int
    ) : Parcelable {
    companion object {
    val ZERO = CounterState(0)
    }
    fun add(number: Int): CounterState =
    this.copy(counter = counter + number)
    fun reset(): CounterState =
    CounterState.ZERO
    }

    View Slide

  63. @Parcelize data class CounterState(
    val counter: Int
    ) : Parcelable {
    companion object {
    val ZERO = CounterState(0)
    }
    fun add(number: Int): CounterState =
    this.copy(counter = counter + number)
    fun reset(): CounterState =
    CounterState.ZERO
    }

    View Slide

  64. CounterModel.kt

    View Slide

  65. object CounterModel {
    fun bind(
    intentions: CounterIntentions,
    bindings: Observable,
    states: Observable
    ): Observable {
    val numbers = Observable.merge(
    intentions.increment(), intentions.incrementBy5(),
    intentions.decrement()
    )
    return Observable.merge(
    newBindingUseCase(bindings),
    restoredBindingUseCase(bindings, states),
    incrementDecrementUseCase(numbers, states),
    resetUseCase(intentions.reset())
    )
    }
    // More functions…
    }

    View Slide

  66. object CounterModel {
    fun bind(
    intentions: CounterIntentions,
    bindings: Observable,
    states: Observable
    ): Observable {
    val numbers = Observable.merge(
    intentions.increment(), intentions.incrementBy5(),
    intentions.decrement()
    )
    return Observable.merge(
    newBindingUseCase(bindings),
    restoredBindingUseCase(bindings, states),
    incrementDecrementUseCase(numbers, states),
    resetUseCase(intentions.reset())
    )
    }
    // More functions…
    }

    View Slide

  67. object CounterModel {
    fun bind(
    intentions: CounterIntentions,
    bindings: Observable,
    states: Observable
    ): Observable {
    val numbers = Observable.merge(
    intentions.increment(), intentions.incrementBy5(),
    intentions.decrement()
    )
    return Observable.merge(
    newBindingUseCase(bindings),
    restoredBindingUseCase(bindings, states),
    incrementDecrementUseCase(numbers, states),
    resetUseCase(intentions.reset())
    )
    }
    // More functions…
    }

    View Slide

  68. object CounterModel {
    fun bind(
    intentions: CounterIntentions,
    bindings: Observable,
    states: Observable
    ): Observable {
    val numbers = Observable.merge(
    intentions.increment(), intentions.incrementBy5(),
    intentions.decrement()
    )
    return Observable.merge(
    newBindingUseCase(bindings),
    restoredBindingUseCase(bindings, states),
    incrementDecrementUseCase(numbers, states),
    resetUseCase(intentions.reset())
    )
    }
    // More functions…
    }

    View Slide

  69. object CounterModel {
    fun bind(
    intentions: CounterIntentions,
    bindings: Observable,
    states: Observable
    ): Observable {
    val numbers = Observable.merge(
    intentions.increment(), intentions.incrementBy5(),
    intentions.decrement()
    )
    return Observable.merge(
    newBindingUseCase(bindings),
    restoredBindingUseCase(bindings, states),
    incrementDecrementUseCase(numbers, states),
    resetUseCase(intentions.reset())
    )
    }
    // More functions…
    }

    View Slide

  70. object CounterModel {
    fun bind(
    intentions: CounterIntentions,
    bindings: Observable,
    states: Observable
    ): Observable {
    val numbers = Observable.merge(
    intentions.increment(), intentions.incrementBy5(),
    intentions.decrement()
    )
    return Observable.merge(
    newBindingUseCase(bindings),
    restoredBindingUseCase(bindings, states),
    incrementDecrementUseCase(numbers, states),
    resetUseCase(intentions.reset())
    )
    }
    // More functions…
    }

    View Slide

  71. object CounterModel {
    fun bind(
    intentions: CounterIntentions,
    bindings: Observable,
    states: Observable
    ): Observable {
    val numbers = Observable.merge(
    intentions.increment(), intentions.incrementBy5(),
    intentions.decrement()
    )
    return Observable.merge(
    newBindingUseCase(bindings),
    restoredBindingUseCase(bindings, states),
    incrementDecrementUseCase(numbers, states),
    resetUseCase(intentions.reset())
    )
    }
    // More functions…
    }

    View Slide

  72. object CounterModel {
    fun bind(
    intentions: CounterIntentions,
    bindings: Observable,
    states: Observable
    ): Observable {
    val numbers = Observable.merge(
    intentions.increment(), intentions.incrementBy5(),
    intentions.decrement()
    )
    return Observable.merge(
    newBindingUseCase(bindings),
    restoredBindingUseCase(bindings, states),
    incrementDecrementUseCase(numbers, states),
    resetUseCase(intentions.reset())
    )
    }
    // More functions…
    }

    View Slide

  73. object CounterModel {
    fun bind(
    intentions: CounterIntentions,
    bindings: Observable,
    states: Observable
    ): Observable {
    val numbers = Observable.merge(
    intentions.increment(), intentions.incrementBy5(),
    intentions.decrement()
    )
    return Observable.merge(
    newBindingUseCase(bindings),
    restoredBindingUseCase(bindings, states),
    incrementDecrementUseCase(numbers, states),
    resetUseCase(intentions.reset())
    )
    }
    // More functions…
    }

    View Slide

  74. object CounterModel {
    fun bind(
    intentions: CounterIntentions,
    bindings: Observable,
    states: Observable
    ): Observable {
    val numbers = Observable.merge(
    intentions.increment(), intentions.incrementBy5(),
    intentions.decrement()
    )
    return Observable.merge(
    newBindingUseCase(bindings),
    restoredBindingUseCase(bindings, states),
    incrementDecrementUseCase(numbers, states),
    resetUseCase(intentions.reset())
    )
    }
    // More functions…
    }

    View Slide

  75. object CounterModel {
    fun bind(
    intentions: CounterIntentions,
    bindings: Observable,
    states: Observable
    ): Observable {
    val numbers = Observable.merge(
    intentions.increment(), intentions.incrementBy5(),
    intentions.decrement()
    )
    return Observable.merge(
    newBindingUseCase(bindings),
    restoredBindingUseCase(bindings, states),
    incrementDecrementUseCase(numbers, states),
    resetUseCase(intentions.reset())
    )
    }
    // More functions…
    }

    View Slide

  76. object CounterModel {
    fun bind(
    intentions: CounterIntentions,
    bindings: Observable,
    states: Observable
    ): Observable {
    val numbers = Observable.merge(
    intentions.increment(), intentions.incrementBy5(),
    intentions.decrement()
    )
    return Observable.merge(
    newBindingUseCase(bindings),
    restoredBindingUseCase(bindings, states),
    incrementDecrementUseCase(numbers, states),
    resetUseCase(intentions.reset())
    )
    }
    // More functions…
    }

    View Slide

  77. object CounterModel {
    fun bind(
    intentions: CounterIntentions,
    bindings: Observable,
    states: Observable
    ): Observable {
    val numbers = Observable.merge(
    intentions.increment(), intentions.incrementBy5(),
    intentions.decrement()
    )
    return Observable.merge(
    newBindingUseCase(bindings),
    restoredBindingUseCase(bindings, states),
    incrementDecrementUseCase(numbers, states),
    resetUseCase(intentions.reset())
    )
    }
    // More functions…
    }

    View Slide

  78. Use Case - New Binding
    private fun newBindingUseCase(
    bindings: Observable
    ): Observable {
    return bindings
    .filter { it == Binding.NEW }
    .map { CounterState.ZERO }
    }

    View Slide

  79. Use Case - New Binding
    private fun newBindingUseCase(
    bindings: Observable
    ): Observable {
    return bindings
    .filter { it == Binding.NEW }
    .map { CounterState.ZERO }
    }

    View Slide

  80. Use Case - New Binding
    private fun newBindingUseCase(
    bindings: Observable
    ): Observable {
    return bindings
    .filter { it == Binding.NEW }
    .map { CounterState.ZERO }
    }

    View Slide

  81. Use Case - New Binding
    private fun newBindingUseCase(
    bindings: Observable
    ): Observable {
    return bindings
    .filter { it == Binding.NEW }
    .map { CounterState.ZERO }
    }

    View Slide

  82. Use Case - New Binding
    private fun newBindingUseCase(
    bindings: Observable
    ): Observable {
    return bindings
    .filter { it == Binding.NEW }
    .map { CounterState.ZERO }
    }

    View Slide

  83. Use Case - New Binding
    private fun newBindingUseCase(
    bindings: Observable
    ): Observable {
    return bindings
    .filter { it == Binding.NEW }
    .map { CounterState.ZERO }
    }

    View Slide

  84. Use Case - Restored Binding
    private fun restoredBindingUseCase(
    bindings: Observable,
    states: Observable
    ): ObservableSource {
    return bindings
    .filter { it == Binding.RESTORED }
    .withLatestFrom(states) {
    _, previousState -> previousState
    }
    }

    View Slide

  85. Use Case - Restored Binding
    private fun restoredBindingUseCase(
    bindings: Observable,
    states: Observable
    ): ObservableSource {
    return bindings
    .filter { it == Binding.RESTORED }
    .withLatestFrom(states) {
    _, previousState -> previousState
    }
    }

    View Slide

  86. Use Case - Restored Binding
    private fun restoredBindingUseCase(
    bindings: Observable,
    states: Observable
    ): ObservableSource {
    return bindings
    .filter { it == Binding.RESTORED }
    .withLatestFrom(states) {
    _, previousState -> previousState
    }
    }

    View Slide

  87. Use Case - Restored Binding
    private fun restoredBindingUseCase(
    bindings: Observable,
    states: Observable
    ): ObservableSource {
    return bindings
    .filter { it == Binding.RESTORED }
    .withLatestFrom(states) {
    _, previousState -> previousState
    }
    }

    View Slide

  88. Use Case - Restored Binding
    private fun restoredBindingUseCase(
    bindings: Observable,
    states: Observable
    ): ObservableSource {
    return bindings
    .filter { it == Binding.RESTORED }
    .withLatestFrom(states) {
    _, previousState -> previousState
    }
    }

    View Slide

  89. Use Case - Restored Binding
    private fun restoredBindingUseCase(
    bindings: Observable,
    states: Observable
    ): ObservableSource {
    return bindings
    .filter { it == Binding.RESTORED }
    .withLatestFrom(states) {
    _, previousState -> previousState
    }
    }

    View Slide

  90. Use Case - Increment / Decrement
    private fun incrementDecrementUseCase(
    numbers: Observable,
    states: Observable
    ): Observable {
    return numbers.withLatestFrom(states) {
    number, previousState -> previousState.add(number)
    }
    }

    View Slide

  91. Use Case - Increment / Decrement
    private fun incrementDecrementUseCase(
    numbers: Observable,
    states: Observable
    ): Observable {
    return numbers.withLatestFrom(states) {
    number, previousState -> previousState.add(number)
    }
    }

    View Slide

  92. Use Case - Increment / Decrement
    private fun incrementDecrementUseCase(
    numbers: Observable,
    states: Observable
    ): Observable {
    return numbers.withLatestFrom(states) {
    number, previousState -> previousState.add(number)
    }
    }

    View Slide

  93. Use Case - Increment / Decrement
    private fun incrementDecrementUseCase(
    numbers: Observable,
    states: Observable
    ): Observable {
    return numbers.withLatestFrom(states) {
    number, previousState -> previousState.add(number)
    }
    }

    View Slide

  94. Use Case - Increment / Decrement
    private fun incrementDecrementUseCase(
    numbers: Observable,
    states: Observable
    ): Observable {
    return numbers.withLatestFrom(states) {
    number, previousState -> previousState.add(number)
    }
    }

    View Slide

  95. Use Case - Increment / Decrement
    private fun incrementDecrementUseCase(
    numbers: Observable,
    states: Observable
    ): Observable {
    return numbers.withLatestFrom(states) {
    number, previousState -> previousState.add(number)
    }
    }

    View Slide

  96. Use Case - Reset
    private fun resetUseCase(
    reset: Observable
    ): Observable {
    return reset.map { CounterState.ZERO }
    }

    View Slide

  97. Use Case - Reset
    private fun resetUseCase(
    reset: Observable
    ): Observable {
    return reset.map { CounterState.ZERO }
    }

    View Slide

  98. Use Case - Reset
    private fun resetUseCase(
    reset: Observable
    ): Observable {
    return reset.map { CounterState.ZERO }
    }

    View Slide

  99. Use Case - Reset
    private fun resetUseCase(
    reset: Observable
    ): Observable {
    return reset.map { CounterState.ZERO }
    }

    View Slide

  100. Use Case - Reset
    private fun resetUseCase(
    reset: Observable
    ): Observable {
    return reset.map { CounterState.ZERO }
    }

    View Slide

  101. Don’t scan() (Recommendation)
    • Introduces additional abstraction because it requires
    homogeneous streams

    View Slide

  102. View

    View Slide

  103. View
    Model
    Intention

    View Slide

  104. CounterView.kt

    View Slide

  105. interface CounterView {
    fun displayCounter(value: Int)
    }

    View Slide

  106. interface CounterView {
    fun displayCounter(value: Int)
    }

    View Slide

  107. CounterViewDriver.kt

    View Slide

  108. class CounterViewDriver : ViewDriver {
    override fun render(view: CounterView, state: CounterState) {
    view.displayCounter(state.counter)
    }
    }

    View Slide

  109. class CounterViewDriver : ViewDriver {
    override fun render(view: CounterView, state: CounterState) {
    view.displayCounter(state.counter)
    }
    }

    View Slide

  110. class CounterViewDriver : ViewDriver {
    override fun render(view: CounterView, state: CounterState) {
    view.displayCounter(state.counter)
    }
    }

    View Slide

  111. class CounterViewDriver : ViewDriver {
    override fun render(view: CounterView, state: CounterState) {
    view.displayCounter(state.counter)
    }
    }

    View Slide

  112. class CounterViewDriver : ViewDriver {
    override fun render(view: CounterView, state: CounterState) {
    view.displayCounter(state.counter)
    }
    }

    View Slide

  113. class CounterViewDriver : ViewDriver {
    override fun render(view: CounterView, state: CounterState) {
    view.displayCounter(state.counter)
    }
    }

    View Slide

  114. Learnings
    • RxJava yields better results with a reactive architecture
    • You don’t need lifecycle events (most of the time)
    • UI tests are necessary
    • Collaboration between mobile teams is possible and fun

    View Slide

  115. Pros
    • Wholistic
    • Testable
    • Easy to debug
    • Reduction in the number of unknowns

    View Slide

  116. Cons

    View Slide

  117. Cons In Words
    • Learning curve
    • Buy-in from team / tech leaders
    • Hiring (as of today)

    View Slide

  118. View Slide

  119. References
    • https://www.youtube.com/watch?v=1zj7M1LnJV4
    • https://cycle.js.org/getting-started.html
    • https://egghead.io/courses/cycle-js-fundamentals
    • https://redux.js.org
    • https://egghead.io/courses/getting-started-with-redux
    • https://github.com/ragunathjawahar/kitchen-sink

    View Slide

  120. Medium • Twitter • GitHub
    @ragunathjawahar

    View Slide