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

Domic — Reactive Virtual DOM. Why we built it, how it works.

Domic — Reactive Virtual DOM. Why we built it, how it works.

In-depth technical talk explaining why Domic — Reactive Virtual DOM exists and how it works.

You'll learn how Domic:

- Uses diffing, debouncing, double buffering and other techniques to render changes efficiently
- Keeps state consistent with mutable Android DOM
- Uses advanced RxJava under the hood: sharing, custom operators, scheduling, subscription management
- Minimizes allocations and interactions with main thread
- Renders UI state in memory for unit/integration testing on JVM
- Integrates with existing design patterns: MVVM, MVI/Redux, MVP

1669655d7494301079bbae17c15675b7?s=128

Artem Zinnatullin

August 27, 2018
Tweet

Transcript

  1. None
  2. motivation

  3. MVC 0:

  4. MVC →MVP 0:

  5. MVC →MVP→MVP2 0:

  6. MVC →MVP→MVP2 →MVVM 0:

  7. MVC →MVP→MVP2 →MVVM →MVVM2 0:

  8. MVC →MVP→MVP2 →MVVM →MVVM2 →Redux goto 0 0:

  9. MVC →MVP→MVP2 →MVVM →MVVM2 →Redux goto 0 0:

  10. focus: MVVM2 & Redux

  11. “Domic — DOMe like”

  12. structure

  13. 3 main modules

  14. 3 main modules

  15. 3 main modules domic/api

  16. 3 main modules domic/api domic/android

  17. 3 main modules domic/api domic/android domic/test

  18. the api module

  19. the api module - Mirrors Android Widgets & Views but

    reactively - Contains only interfaces (okok and 1 inline function) - Type-safe
  20. the api module: Observe val textView: TextView = … textView.observe.textChanges:

    Observable textView.observe.clicks: Observable textView.observe.etc: Observable
  21. the api module: Observe val textView: TextView = … textView.observe.textChanges:

    Observable textView.observe.clicks: Observable textView.observe.etc: Observable Interface provided by Domic
  22. the api module: Change val textView: TextView = … textView.change.text(Observable)

    textView.change.enabled(Observable) textView.change.etc(Observable)
  23. the api module: Change val textView: TextView = … textView.change.text(Observable)

    textView.change.enabled(Observable) textView.change.etc(Observable) Interface provided by Domic
  24. the api module: type-safety View.Observe is read-only reference View.Change is

    write-only reference View is read-write reference
  25. Useful side-effects of reactive api - Threading becomes an implementation

    detail - Both subscribeOn and observeOn - Protects main thread from unnecessary work
  26. Useful side-effects of reactive api - Threading becomes an implementation

    detail - No easy way for consumer to do blocking work
  27. Useful side-effects of reactive api - Threading becomes an implementation

    detail - No easy way for consumer to do blocking work - Implementation can be complicated while being non-blocking
  28. the View interface

  29. the api module: View interface package com.lyft.domic.api interface View {

    val observe: Observe val change: Change interface Observe { val clicks: Observable<Any> val focus: Observable<Boolean> val longClicks: Observable<Any> } interface Change { fun alpha(alphaValues: Observable<Float>): Disposable fun enabled(enabledValues: Observable<Boolean>): Disposable fun visibility(visibilityValues: Observable<Visibility>): Disp } }
  30. the api module: Observe package com.lyft.domic.api interface View { val

    observe: Observe val change: Change interface Observe { val clicks: Observable<Any> val focus: Observable<Boolean> val longClicks: Observable<Any> } interface Change { fun alpha(alphaValues: Observable<Float>): Disposable fun enabled(enabledValues: Observable<Boolean>): Disposable fun visibility(visibilityValues: Observable<Visibility>): Disp } }
  31. the api module: Observe package com.lyft.domic.api interface View { val

    observe: Observe val change: Change interface Observe { val clicks: Observable<Any> val focus: Observable<Boolean> val longClicks: Observable<Any> } interface Change { fun alpha(alphaValues: Observable<Float>): Disposable fun enabled(enabledValues: Observable<Boolean>): Disposable fun visibility(visibilityValues: Observable<Visibility>): Disp } }
  32. API: Observe, real world example signInButton .observe .clicks .withLatestFrom(credentials) {

    _, creds -> creds } .flatMap { creds -> service.signIn(creds) }
  33. the api module: View interface package com.lyft.domic.api interface View {

    val observe: Observe val change: Change interface Observe { val clicks: Observable<Any> val focus: Observable<Boolean> val longClicks: Observable<Any> } interface Change { fun alpha(alphaValues: Observable<Float>): Disposable fun enabled(enabledValues: Observable<Boolean>): Disposable fun visibility(visibilityValues: Observable<Visibility>): Disp } }
  34. the api module: Change package com.lyft.domic.api interface View { val

    observe: Observe val change: Change interface Observe { val clicks: Observable<Any> val focus: Observable<Boolean> val longClicks: Observable<Any> } interface Change { fun alpha(alphaValues: Observable<Float>): Disposable fun enabled(enabledValues: Observable<Boolean>): Disposable fun visibility(visibilityValues: Observable<Visibility>): Disp } }
  35. the api module: Change package com.lyft.domic.api interface View { val

    observe: Observe val change: Change interface Observe { val clicks: Observable<Any> val focus: Observable<Boolean> val longClicks: Observable<Any> } interface Change { fun alpha(alphaValues: Observable<Float>): Disposable fun enabled(enabledValues: Observable<Boolean>): Disposable fun visibility(visibilityValues: Observable<Visibility>): Disp } }
  36. API: Change, real world example disposable += signInRequest .map {

    false } .subscribe(view.signInButton.change::enabled)
  37. 100% reactive API?

  38. 100% reactive api? - Observables as inputs

  39. 100% reactive api? - Observables as inputs - Observables as

    outputs
  40. 100% reactive api? - Observables as inputs - Observables as

    outputs - Proper cancellation - Property change can be cancelled within Frame window
  41. 100% reactive api? - Observables as inputs - Observables as

    outputs - Proper cancellation - No back-pressure tho, because UI generally can’t be back-pressured
  42. 3 main modules domic/api

  43. 3 main modules domic/api domic/android

  44. the android module

  45. the android module - Implements API module

  46. the android module - Implements API module - Binds to

    real Android DOM (Widgets, Views)
  47. the android module - Implements API module - Binds to

    real Android DOM (Widgets, Views) - Allows to reuse: - Layouts - Styles - Views/ViewGroups - Layout Preview - Support library - Any build system
  48. the android module - Implements API module - Binds to

    real Android DOM (Widgets, Views) - Takes care of threading - subscribeOn(mainThread())
  49. the android module - Implements API module - Binds to

    real Android DOM (Widgets, Views) - Takes care of threading - subscribeOn(mainThread()) - observeOn() custom rendering
  50. the android module - Implements API module - Binds to

    real Android DOM (Widgets, Views) - Takes care of threading - subscribeOn(mainThread()) - observeOn() custom rendering - Diffs each property’s new state against previous
  51. the android module - Implements API module - Binds to

    real Android DOM (Widgets, Views) - Takes care of threading - subscribeOn(mainThread()) - observeOn() custom rendering - Diffs each property’s new state against previous - Naming convention: “Android” prefix, then the API interface name - val text: TextView = AndroidTextView(…)
  52. the android module val component: VirtualType = BindingType(…)

  53. the android module val textView: TextView = AndroidTextView(…)

  54. the android module val textView: TextView = AndroidTextView(…) Interface provided

    by Domic
  55. the android module val textView: TextView = AndroidTextView(…) Implementation provided

    by Domic
  56. Binding to real Android DOM: RxBinding “What can be easier

    than using RxBinding? , right? Famous last words
  57. Binding to real Android DOM: RxBinding “What can be easier

    than using RxBinding? “Why would you even need a library on top of it?” , right? Famous last words
  58. Binding to real Android DOM: RxBinding - Observables are not

    shared!
  59. Binding to real Android DOM: RxBinding - Observables are not

    shared! - Some properties only allow one listener at a time! - ie RxView.clicks()
  60. Binding to real Android DOM: RxBinding - Observables are not

    shared! - Some properties only allow one listener at a time! - ie RxView.clicks() - Subscription time threading is not enforced
  61. Binding to real Android DOM: RxBinding - Observables are not

    shared! - Some properties only allow one listener at a time! - ie RxView.clicks() - Subscription time threading is not enforced - And it’s fine! RxBinding is a low-level, foundation layer
  62. Threading - subscribeOn(mainThread()) is okay

  63. Threading - subscribeOn(mainThread()) is okay - Async Handler? Maybe

  64. Threading - subscribeOn(mainThread()) is okay - Async Handler? Maybe -

    observeOn(mainThread()) is easy
  65. Threading - subscribeOn(mainThread()) is okay - Async Handler? Maybe -

    observeOn(mainThread()) is easy - But that’s too easy - If you often render partially same state, you might want something better
  66. rendering

  67. How a property change gets rendered 1. Domic subscribes to

    Observable<Value> This is how you get stable 60 fps!
  68. How a property change gets rendered 1. Domic subscribes to

    Observable<Value> 2. Value goes through sharedDistinctUntilChanged() This is how you get stable 60 fps! Reviewed by David Karnok
  69. How a property change gets rendered 1. Domic subscribes to

    Observable<Value> 2. Value goes through sharedDistinctUntilChanged() 1. It is a custom RxJava Operator 2. Uses AtomicReferenceArray as shared state container 3. Designed to handle multiple change streams bound to the same property at the same time This is how you get stable 60 fps! Reviewed by David Karnok
  70. How a property change gets rendered 1. Domic subscribes to

    Observable<Value> 2. Value goes through sharedDistinctUntilChanged() 3. Value maps to a Change object This is how you get stable 60 fps!
  71. How a property change gets rendered 1. Domic subscribes to

    Observable<Value> 2. Value goes through sharedDistinctUntilChanged() 3. Value maps to a Change object A. Optional optimizations, like PrecomputedText This is how you get stable 60 fps!
  72. How a property change gets rendered 1. Domic subscribes to

    Observable<Value> 2. Value goes through sharedDistinctUntilChanged() 3. Value maps to a Change object 4. Renderer observes the Change This is how you get stable 60 fps!
  73. How a property change gets rendered 1. Domic subscribes to

    Observable<Value> 2. Value goes through sharedDistinctUntilChanged() 3. Value maps to a Change object 4. Renderer observes the Change 5. Renderer adds or replaces the Change in Buffer This is how you get stable 60 fps!
  74. How a property change gets rendered 1. Domic subscribes to

    Observable<Value> 2. Value goes through sharedDistinctUntilChanged() 3. Value maps to a Change object 4. Renderer observes the Change 5. Renderer adds or replaces the Change in Buffer • Allows Domic “debounce” equal changes within frame interval! Basically a BackpressureStrategy.LATEST This is how you get stable 60 fps!
  75. How a property change gets rendered 1. Domic subscribes to

    Observable<Value> 2. Value goes through sharedDistinctUntilChanged() 3. Value maps to a Change object 4. Renderer observes Change 5. Renderer adds or replaces the Change in Buffer • Allows Domic “debounce” equal changes within frame interval! Basically a BackpressureStrategy.LATEST • Allows Domic to minimize jumps to the Main Thread. Basically a BackpressureStrategy.BUFFER This is how you get stable 60 fps!
  76. How a property change gets rendered 1. Domic subscribes to

    Observable<Value> 2. Value goes through sharedDistinctUntilChanged() 3. Value maps to a Change object 4. Renderer observes the Change 5. Renderer adds or replaces the Change in Buffer 6. Choreographer calls the Renderer This is how you get stable 60 fps!
  77. How a property change gets rendered 1. Domic subscribes to

    Observable<Value> 2. Value goes through sharedDistinctUntilChanged() 3. Value maps to a Change object 4. Renderer observes the Change 5. Renderer adds or replaces the Change in Buffer 6. Choreographer calls the Renderer 7. Renderer swaps the Buffer and renders current one This is how you get stable 60 fps!
  78. None
  79. Domic performance: baseline, no help from diffing

  80. the test module

  81. the test module - Implements API module in memory

  82. the test module - Implements API module in memory -

    Diffs the changes
  83. the test module - Implements API module in memory -

    Adds Simulate interface to each Widget
  84. the test module - Implements API module in memory -

    Adds Simulate interface to each Widget - Adds Check interface to each Widget
  85. the test module - Implements API module in memory -

    Adds Simulate interface to each Widget - Adds Check interface to each Widget - Designed to work on JVM
  86. the test module - Implements API module in memory -

    Adds Simulate interface to each Widget - Adds Check interface to each Widget - Designed to work on JVM - Allows to test ViewModels, Presenters, etc
  87. the test module - Implements API module in memory -

    Adds Simulate interface to each Widget - Adds Check interface to each Widget - Designed to work on JVM - Allows to test ViewModels, Presenters, etc - Extreme: ~whole app can be rendered in memory
  88. the test module @Test fun `sign in button enabled if

    valid credentials entered`() { view.emailEditText.simulate.text("some@email.com") view.passwordEditText.simulate.text("password") assertThat(view.signInButton.check.enabled).isTrue() }
  89. the test module @Test fun `sign in button enabled if

    valid credentials entered`() { view.emailEditText.simulate.text("some@email.com") view.passwordEditText.simulate.text("password") assertThat(view.signInButton.check.enabled).isTrue() }
  90. the test module @Test fun `sign in button enabled if

    valid credentials entered`() { view.emailEditText.simulate.text("some@email.com") view.passwordEditText.simulate.text("password") assertThat(view.signInButton.check.enabled).isTrue() }
  91. the test module @Test fun `sign in button enabled if

    valid credentials entered`() { view.emailEditText.simulate.text("some@email.com") view.passwordEditText.simulate.text("password") assertThat(view.signInButton.check.enabled).isTrue() }
  92. the test module @Test fun `sign in button enabled if

    valid credentials entered`() { view.emailEditText.simulate.text("some@email.com") view.passwordEditText.simulate.text("password") assertThat(view.signInButton.check.enabled).isTrue() }
  93. + MVVM2 + Redux/MVI + MVP2 Domic

  94. + MVVM2 Domic

  95. Domic + MVVM2: View interface SignInView { val emailEditText: EditText

    val passwordEditText: EditText val signInButton: Button val resultTextView: TextView }
  96. Domic + MVVM2: AndroidView class AndroidSignInView(root: ViewGroup, renderer: Renderer) :

    SignInView { override val emailEditText = AndroidEditText(root.findViewById(R.id.email_edit_text), renderer) override val passwordEditText = AndroidEditText(root.findViewById(R.id.password_edit_text), renderer) override val signInButton = AndroidButton(root.findViewById(R.id.sign_in_button), renderer) override val resultTextView = AndroidTextView(root.findViewById(R.id.sign_result_text_view), renderer) }
  97. Domic + MVVM2: Thoughts https://github.com/lyft/domic/tree/master/samples/mvvm - Often times emits same

    property values due to its reactive nature
  98. Domic + MVVM2: Thoughts https://github.com/lyft/domic/tree/master/samples/mvvm - Often times emits same

    property values due to its reactive nature - Spawns lots of individual rx streams which need to do proper threading
  99. Domic + MVVM2: Thoughts https://github.com/lyft/domic/tree/master/samples/mvvm - Often times emits same

    property values due to its reactive nature - Spawns lots of individual rx streams which need to do proper threading - Uses same rx streams from View multiple times, they need to be shared
  100. Domic + MVVM2: Thoughts https://github.com/lyft/domic/tree/master/samples/mvvm - Often times emits same

    property values due to its reactive nature - Spawns lots of individual rx streams which need to do proper threading - Uses same rx streams from View multiple times, they need to be shared - Reactive View layer becomes hard to swap with test implementation
  101. Domic + MVVM2: Thoughts https://github.com/lyft/domic/tree/master/samples/mvvm - Often times emits same

    property values due to its reactive nature - Spawns lots of individual rx streams which need to do proper threading - Uses same rx streams from View multiple times, they need to be shared - Reactive View layer becomes hard to swap with test implementation - Domic fits MVVM2 really well and compliments it in many ways
  102. Domic + MVVM2: Sample See: lyft/domic/samples/mvvm https://github.com/lyft/domic/tree/master/samples/mvvm

  103. + Redux Domic

  104. Domic + Redux: Thoughts - Most of the time emits

    state that is mostly equal to previous one https://github.com/lyft/domic/tree/master/samples/redux
  105. Domic + Redux: Thoughts - Most of the time emits

    state that is mostly equal to previous one - Spawns less rx streams compared to MVVM https://github.com/lyft/domic/tree/master/samples/redux
  106. Domic + Redux: Thoughts - Most of the time emits

    state that is mostly equal to previous one - Spawns less rx streams compared to MVVM - Input rx streams are merged into a single stream, RxBinding actually works well here https://github.com/lyft/domic/tree/master/samples/redux
  107. Domic + Redux: Thoughts - Most of the time emits

    state that is mostly equal to previous one - Spawns less rx streams compared to MVVM - Input rx streams are merged into a single stream, RxBinding actually works well here - Output rx stream is merged into a single one, threading is easy but still needs https://github.com/lyft/domic/tree/master/samples/redux
  108. Domic + Redux: Thoughts - Most of the time emits

    state that is mostly equal to previous one - Spawns less rx streams compared to MVVM - Input rx streams are merged into a single stream, RxBinding actually works well here - Output rx stream is merged into a single one, threading is easy but still needs - View layer is basically a render(State) function and an actions: Observable<Action> property https://github.com/lyft/domic/tree/master/samples/redux
  109. Domic + Redux: Thoughts - Most of the time emits

    state that is mostly equal to previous one - Spawns less rx streams compared to MVVM - Input rx streams are merged into a single stream, RxBinding actually works well here - Output rx stream is merged into a single one, threading is easy but still needs - View layer is basically a render(State) function and an actions: Observable<Action> property - Domic’s API as of now feels a bit off for Redux, but https://github.com/lyft/domic/tree/master/samples/redux
  110. Domic + Redux: Sample See: lyft/domic/samples/redux https://github.com/lyft/domic/tree/master/samples/redux Reviewed by Hannes

    Dorfmann
  111. the test module See: domic/samples/mvvm/src/test domic/samples/mvp/src/test https://github.com/lyft/domic/tree/master/samples domic/samples/redux/rxredux/src/test

  112. DSL/Declarative API?

  113. DSL/declarative API? - Web developers love React, especially with Redux

  114. DSL/declarative API? - Web developers love React, especially with Redux

    - Lots of trade-offs and corner cases
  115. DSL/declarative API? - Web developers love React, especially with Redux

    - Lots of trade-offs & corner cases - Layout files?
  116. DSL/declarative API? - Web developers love React, especially with Redux

    - Lots of corner-cases & trade-offs - Layout files? - Styling?
  117. DSL/declarative API? - Web developers love React, especially with Redux

    - Lots of corner-cases & trade-offs - Layout files? - Styling? - Reactive?
  118. DSL/declarative API? - Web developers love React, especially with Redux

    - Lots of corner-cases & trade-offs - Layout files? - Styling? - Reactive? - Maybe!** **Is not a public offer, other restrictions apply
  119. Domic … or?

  120. Domic or fb/Litho https://fblitho.com/

  121. Domic or fb/Litho - Controls the DOM - Declarative -

    Own Component system - Components can’t really use Android Layouts/Widgets - Component system is designed to be used with annotation processing - Uses crazy-awesome fb/Yoga for efficient rendering - Scalable for complicated screens like RecyclerView - Calculates diffs - Reactive? https://fblitho.com/
  122. Domic or fb/Litho airbnb/Epoxy https://github.com/airbnb/epoxy

  123. Domic or airbnb/Epoxy - Controls the DOM - Declarative -

    Own Component system - Components can use Android Layouts/Widgets/Views - Component system is designed to be used with annotation processing - Scalable like RecylerView and actually uses RV - Calculates diffs - Reactive? https://github.com/airbnb/epoxy
  124. Domic or fb/Litho airbnb/Epoxy fb/ReactNative https://facebook.github.io/react-native/

  125. Domic or fb/ReactNative - Controls the DOM - Declarative -

    Own Component system - Components can use Android Layouts/Widgets/Views - JavaScript - Calculates diffs - Reactive? https://facebook.github.io/react-native/
  126. Domic or fb/Litho airbnb/Epoxy fb/ReactNative passy/Konduit https://github.com/passsy/Konduit

  127. Domic or passy/Konduit - Controls the DOM - Declarative -

    Own Widget system - Widgets can use Android Layouts/Widgets/Views - Calculates diffs - Reactive? https://github.com/passsy/Konduit
  128. Domic … or?

  129. Should Domic …? - Control the DOM or only Bind

    to it?
  130. Should Domic …? - Control the DOM or only Bind

    to it? - Provide declarative/DSL-based API?
  131. Should Domic …? - Control the DOM or only Bind

    to it? - Provide declarative/DSL-based API? - Stay 100% reactive?
  132. Should Domic …? - Control the DOM or only Bind

    to it? - Provide declarative/DSL-based API? - Stay 100% reactive? - Wait, did I mention reactive animations?
  133. Should Domic …? - Control the DOM or only Bind

    to it? - Provide declarative/DSL-based API? - Be actually reactive? - Wait, did I mention reactive animations? Maybe yes!** Maybe no** **Is not a public offer, other restrictions apply
  134. Recap

  135. Recap Domic mirrors real DOM, but reactively

  136. Recap Domic applies diff of changes

  137. Recap Domic takes care of threading

  138. Recap Domic takes care of efficient rendering

  139. Recap Domic provides unified, reusable, type-safe API

  140. Recap Domic provides in-memory implementation for unit-testing

  141. Recap Why did I think that programming was a good

    idea?
  142. Public Availability

  143. https://github.com/lyft/domic Artem Zinnatullin, @artem_zin