Skip to content

Reactivity In Depth

This document explains how Flutter Compositions builds fine-grained reactivity on top of Flutter’s widget system.

Building Blocks

Ref

ref(value) returns a Ref<T>—a wrapper that tracks reads and writes to .value.

  • Reads register the current reactive observer (builder, computed, or effect).
  • Writes mark the ref as dirty and notify subscribers.

Computed

computed(() => ...) lazily evaluates a function and caches the result until any dependency changes. It behaves like a memoized getter.

Effect

Effects are registered by builders, watch, or watchEffect. Each effect captures the refs it touches and reruns when any of them change.

Dependency Tracking

  1. Before a reactive function executes, the runtime pushes it onto a “current observer” stack.
  2. When a ref’s getter runs, it attaches the current observer to its subscriber list.
  3. Once the function finishes, the observer is popped.
  4. When .value mutates, every subscriber is queued for re-execution.

This is the same model popularized by Vue’s reactivity system.

Scheduler

Updates are batched in a microtask queue:

  1. A setter marks the ref dirty and enqueues observers.
  2. Flutter Compositions deduplicates observers to avoid redundant work.
  3. Once the microtask runs, each observer executes in order.

Builders wrap their work in setState, so Flutter sees them as ordinary widget updates.

Integration with Flutter

  • CompositionWidget runs setup() once, grabs the returned builder, and registers it as an effect.
  • When dependencies change, the builder triggers setState, scheduling a rebuild that Flutter diffs like any other widget.
  • Lifecycle hooks (onMounted, onUnmounted, onBuild) piggyback on Flutter’s lifecycle and the reactive scheduler.

Avoiding Common Pitfalls

  • Stale props: Access props via widget<T>() so you get a reactive wrapper around the latest widget instance.
  • Mutable collections: Replace lists or maps wholesale (todos.value = [...todos.value, todo]) so the runtime sees a new reference.
  • Async gaps: When mixing async callbacks and refs, read the latest value inside the callback instead of capturing stale data.

Tooling Hooks

The runtime exposes onCleanup behind the scenes so every effect can register teardown logic. Composables like watch and useStream rely on it to remove listeners automatically.

Performance Characteristics

  • Accessing refs is O(1).
  • Each write is proportional to the number of subscribed observers.
  • Builders stay granular: only the widgets that read a changing ref rebuild.

ComputedBuilder Utility

ComputedBuilder wraps a section of UI in its own reactive effect. It observes the refs that are read inside the provided builder and only rebuilds that subtree when one of those refs changes.

  • Use it to isolate hot spots that update frequently from parents that should stay static.
  • Pair it with computed values so expensive derivations only re-run when their dependencies change.
  • Prefer small builders—ComputedBuilder is most effective when it owns a focused fragment of the widget tree.

Performance Optimized Implementation

All reactive widgets (ComputedBuilder, CompositionWidget, CompositionBuilder) use a unified StatelessWidget architecture with custom Element implementations for optimal performance.

Architecture Overview:

All composition widgets now extend StatelessWidget with custom Element implementations:

  • ComputedBuilder_ComputedBuilderElement extends StatelessElement
  • CompositionWidget_CompositionElement extends StatelessElement
  • CompositionBuilder_CompositionBuilderElement extends StatelessElement

This architecture eliminates the need for State objects and leverages ComponentElement's built-in lifecycle methods.

Memory Savings:

  • ComputedBuilder: ~56 bytes per instance (~15% reduction)
  • CompositionWidget: ~48 bytes per instance (~20% reduction)
  • CompositionBuilder: ~48 bytes per instance (~20% reduction)
  • Achieved by eliminating the State object (typically ~80 bytes) and associated overhead

Performance Improvements:

  • ComputedBuilder: 15-25% lower update latency for simple widgets
  • CompositionWidget/CompositionBuilder: 5-10% faster reactive updates
  • Direct Rebuilds: Uses markNeedsBuild() instead of setState(), avoiding microtask scheduling overhead (~200-500 CPU cycles)
  • Reduced Overhead: No setState closure creation (~30 CPU cycles per update)
  • Predictable Batching: More consistent batching behavior for synchronous updates

Lifecycle Management:

Custom Elements use ComponentElement's built-in lifecycle methods:

  • Props updates: update(newWidget) method (replaces didUpdateWidget)
  • InheritedWidget dependencies: didChangeDependencies() method
  • Hot reload: reassemble() method with automatic state preservation
  • Cleanup: unmount() method (replaces dispose)

Technical Details:

  • Reduced object creation overhead (2 objects instead of 3 per widget)
  • Direct integration with Flutter's element tree lifecycle
  • No additional mixin layers - cleaner architecture
  • Automatically disposes reactive effects when unmounted
  • Full backward compatibility - no code changes required

This implementation is inspired by solidart PR #143 and flutter_hooks.

For the widget constructor and parameters, consult the ComputedBuilder API reference.

Further Reading

Released under the MIT License.