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
- Before a reactive function executes, the runtime pushes it onto a “current observer” stack.
- When a ref’s getter runs, it attaches the current observer to its subscriber list.
- Once the function finishes, the observer is popped.
- When
.valuemutates, 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:
- A setter marks the ref dirty and enqueues observers.
- Flutter Compositions deduplicates observers to avoid redundant work.
- 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
CompositionWidgetrunssetup()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
computedvalues so expensive derivations only re-run when their dependencies change. - Prefer small builders—
ComputedBuilderis 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 StatelessElementCompositionWidget→_CompositionElement extends StatelessElementCompositionBuilder→_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
Stateobject (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 ofsetState(), avoiding microtask scheduling overhead (~200-500 CPU cycles) - Reduced Overhead: No
setStateclosure 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 (replacesdidUpdateWidget) - InheritedWidget dependencies:
didChangeDependencies()method - Hot reload:
reassemble()method with automatic state preservation - Cleanup:
unmount()method (replacesdispose)
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.