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

Artem Zinnatullin

August 27, 2018
Tweet

More Decks by Artem Zinnatullin

Other Decks in Programming

Transcript

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

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

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

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

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

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

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

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

    detail - No easy way for consumer to do blocking work
  9. 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
  10. 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 } }
  11. 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 } }
  12. 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 } }
  13. API: Observe, real world example signInButton .observe .clicks .withLatestFrom(credentials) {

    _, creds -> creds } .flatMap { creds -> service.signIn(creds) }
  14. 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 } }
  15. 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 } }
  16. 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 } }
  17. API: Change, real world example disposable += signInRequest .map {

    false } .subscribe(view.signInButton.change::enabled)
  18. 100% reactive api? - Observables as inputs - Observables as

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

    outputs - Proper cancellation - No back-pressure tho, because UI generally can’t be back-pressured
  20. the android module - Implements API module - Binds to

    real Android DOM (Widgets, Views)
  21. 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
  22. the android module - Implements API module - Binds to

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

    real Android DOM (Widgets, Views) - Takes care of threading - subscribeOn(mainThread()) - observeOn() custom rendering
  24. 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
  25. 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(…)
  26. Binding to real Android DOM: RxBinding “What can be easier

    than using RxBinding? , right? Famous last words
  27. 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
  28. Binding to real Android DOM: RxBinding - Observables are not

    shared! - Some properties only allow one listener at a time! - ie RxView.clicks()
  29. 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
  30. 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
  31. 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
  32. How a property change gets rendered 1. Domic subscribes to

    Observable<Value> This is how you get stable 60 fps!
  33. 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
  34. 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
  35. 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!
  36. 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!
  37. 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!
  38. 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!
  39. 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!
  40. 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!
  41. 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!
  42. 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!
  43. the test module - Implements API module in memory -

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

    Adds Simulate interface to each Widget - Adds Check interface to each Widget
  45. 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
  46. 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
  47. 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
  48. the test module @Test fun `sign in button enabled if

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

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

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

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

    valid credentials entered`() { view.emailEditText.simulate.text("[email protected]") view.passwordEditText.simulate.text("password") assertThat(view.signInButton.check.enabled).isTrue() }
  53. Domic + MVVM2: View interface SignInView { val emailEditText: EditText

    val passwordEditText: EditText val signInButton: Button val resultTextView: TextView }
  54. 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) }
  55. 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
  56. 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
  57. 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
  58. 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
  59. 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
  60. 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
  61. 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
  62. 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
  63. 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
  64. 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
  65. DSL/declarative API? - Web developers love React, especially with Redux

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

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

    - Lots of corner-cases & trade-offs - Layout files? - Styling? - Reactive?
  68. 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
  69. 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/
  70. 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
  71. 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/
  72. 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
  73. Should Domic …? - Control the DOM or only Bind

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

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

    to it? - Provide declarative/DSL-based API? - Stay 100% reactive? - Wait, did I mention reactive animations?
  76. 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