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

A Pragmatic Reactive Architecture for Android

A Pragmatic Reactive Architecture for Android

Presented on Mar '18 at Android Meetup at F22 Labs, Chennai.
https://www.meetup.com/Google-IO-Extended-Chennai/events/248296192/

Ragunath Jawahar

March 24, 2018
Tweet

More Decks by Ragunath Jawahar

Other Decks in Programming

Transcript

  1. Why? 1. Predictable 2. Platform Agnostic 3. User-centric 4. Reactive

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

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

    user input events into model-friendly events. —André Staltz
  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. 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 }
  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. (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
  20. @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 }
  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. 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… }
  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. Use Case - New Binding private fun newBindingUseCase( bindings: Observable<Binding>

    ): Observable<CounterState> { return bindings .filter { it == Binding.NEW } .map { CounterState.ZERO } }
  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 - Restored Binding private fun restoredBindingUseCase( bindings: Observable<Binding>,

    states: Observable<CounterState> ): ObservableSource<CounterState> { return bindings .filter { it == Binding.RESTORED } .withLatestFrom(states) { _, previousState -> previousState } }
  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 - Increment / Decrement private fun incrementDecrementUseCase( numbers:

    Observable<Int>, states: Observable<CounterState> ): Observable<CounterState> { return numbers.withLatestFrom(states) { number, previousState -> previousState.add(number) } }
  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 - Reset private fun resetUseCase( reset: Observable<Unit> ):

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

    • Reduction in the number of unknowns