$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. 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
  2. Benefits 1. Predictable 2. Platform Agnostic 3. User-centric 4. Reactive

    (Responsive / Resilient / Elastic / Message Driven) 5. Unidirectional
  3. Toolbox (Subjective) • Kotlin • RxJava • RxAndroid • RxKotlin

    (Optional) • RxBinding • Retrofit • Room / SQL Brite • JUnit • Truth • Mockito • Espresso
  4. Intent(ion) is a component whose sole responsibility is to translate

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

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

    user input events into model-friendly events. —André Staltz
  7. class CounterIntentions( private val incrementClicks: Observable<Unit>, private val incrementBy5Clicks: Observable<Unit>,

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

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

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

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

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

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

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

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

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

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

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

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

    private val decrementClicks: Observable<Unit>, private val resetClicks: Observable<Unit> ) { fun increment(): Observable<Int> = incrementClicks.map { +1 } // increment by 5 … fun decrement(): Observable<Int> = decrementClicks.map { -1 } fun reset(): Observable<Unit> = resetClicks }
  20. (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
  21. @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 }
  22. @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 }
  23. @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 }
  24. @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 }
  25. @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 }
  26. @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 }
  27. @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 }
  28. @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 }
  29. @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 }
  30. @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 }
  31. @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 }
  32. @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 }
  33. @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 }
  34. @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 }
  35. @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 }
  36. @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 }
  37. @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 }
  38. @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 }
  39. @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 }
  40. @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 }
  41. object CounterModel { fun bind( intentions: CounterIntentions, bindings: Observable<Binding>, states:

    Observable<CounterState> ): Observable<CounterState> { 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… }
  42. object CounterModel { fun bind( intentions: CounterIntentions, bindings: Observable<Binding>, states:

    Observable<CounterState> ): Observable<CounterState> { 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… }
  43. object CounterModel { fun bind( intentions: CounterIntentions, bindings: Observable<Binding>, states:

    Observable<CounterState> ): Observable<CounterState> { 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… }
  44. object CounterModel { fun bind( intentions: CounterIntentions, bindings: Observable<Binding>, states:

    Observable<CounterState> ): Observable<CounterState> { 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… }
  45. object CounterModel { fun bind( intentions: CounterIntentions, bindings: Observable<Binding>, states:

    Observable<CounterState> ): Observable<CounterState> { 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… }
  46. object CounterModel { fun bind( intentions: CounterIntentions, bindings: Observable<Binding>, states:

    Observable<CounterState> ): Observable<CounterState> { 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… }
  47. object CounterModel { fun bind( intentions: CounterIntentions, bindings: Observable<Binding>, states:

    Observable<CounterState> ): Observable<CounterState> { 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… }
  48. object CounterModel { fun bind( intentions: CounterIntentions, bindings: Observable<Binding>, states:

    Observable<CounterState> ): Observable<CounterState> { 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… }
  49. object CounterModel { fun bind( intentions: CounterIntentions, bindings: Observable<Binding>, states:

    Observable<CounterState> ): Observable<CounterState> { 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… }
  50. object CounterModel { fun bind( intentions: CounterIntentions, bindings: Observable<Binding>, states:

    Observable<CounterState> ): Observable<CounterState> { 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… }
  51. object CounterModel { fun bind( intentions: CounterIntentions, bindings: Observable<Binding>, states:

    Observable<CounterState> ): Observable<CounterState> { 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… }
  52. object CounterModel { fun bind( intentions: CounterIntentions, bindings: Observable<Binding>, states:

    Observable<CounterState> ): Observable<CounterState> { 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… }
  53. object CounterModel { fun bind( intentions: CounterIntentions, bindings: Observable<Binding>, states:

    Observable<CounterState> ): Observable<CounterState> { 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… }
  54. Use Case - New Binding private fun newBindingUseCase( bindings: Observable<Binding>

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

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

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

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

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

    ): Observable<CounterState> { return bindings .filter { it == Binding.NEW } .map { CounterState.ZERO } }
  60. Use Case - Restored Binding private fun restoredBindingUseCase( bindings: Observable<Binding>,

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

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

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

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

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

    states: Observable<CounterState> ): ObservableSource<CounterState> { return bindings .filter { it == Binding.RESTORED } .withLatestFrom(states) { _, previousState -> previousState } }
  66. Use Case - Increment / Decrement private fun incrementDecrementUseCase( numbers:

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

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

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

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

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

    Observable<Int>, states: Observable<CounterState> ): Observable<CounterState> { return numbers.withLatestFrom(states) { number, previousState -> previousState.add(number) } }
  72. Use Case - Reset private fun resetUseCase( reset: Observable<Unit> ):

    Observable<CounterState> { return reset.map { CounterState.ZERO } }
  73. Use Case - Reset private fun resetUseCase( reset: Observable<Unit> ):

    Observable<CounterState> { return reset.map { CounterState.ZERO } }
  74. Use Case - Reset private fun resetUseCase( reset: Observable<Unit> ):

    Observable<CounterState> { return reset.map { CounterState.ZERO } }
  75. Use Case - Reset private fun resetUseCase( reset: Observable<Unit> ):

    Observable<CounterState> { return reset.map { CounterState.ZERO } }
  76. Use Case - Reset private fun resetUseCase( reset: Observable<Unit> ):

    Observable<CounterState> { return reset.map { CounterState.ZERO } }
  77. 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
  78. Pros • Wholistic • Testable • Easy to debug •

    Reduction in the number of unknowns
  79. Cons In Words • Learning curve • Buy-in from team

    / tech leaders • Hiring (as of today)