drive<T> method
Drives a reactive Bloom<T> from this TickerProvider by evaluating onTick on every animation frame tick.
Integrates Flutter's continuous display VSYNC ticker directly into the reactive LPDAG pipeline, creating a lifecycle-managed, self-disposing continuous frame pulse stream.
System Architectural Flow
┌────────────────────────────────────────────────────────────────────────┐
│ TickerProvider.drive<T> Pipeline Execution │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ [ Downstream Subscriber Subscribes ] │
│ │ │
│ │ (Activates managed<_TickerDriver<T>>) │
│ ▼ │
│ [ managed: create ] ──► Instantiates _TickerDriver<T> │
│ │ │
│ ├─► seeds initial state via onTick(Duration.zero) │
│ │ │
│ ▼ │
│ [ vsync.createTicker() ] ──► Starts Ticking at VSYNC (60Hz / 120Hz) │
│ │ │
│ ▼ (Continuous Frame Tick) │
│ onTick(elapsed: Duration) ──► Computes Output Value [T] │
│ │ │
│ ▼ │
│ [ Distinct Filter: value == previous? ] │
│ ├─► (Yes) ──► Drop Invalidation Pulse (0-Rebuild) │
│ │ Saves CPU, Battery, and Layout Passes │
│ │ │
│ └─► (No) ──► Push-Dirty to Terminal (Surgical Update) │
│ │
│ [ Downstream Subscribers Unmount / Detach ] │
│ │ │
│ │ (managed: dispose with optional Graceful delay) │
│ ▼ │
│ [ _TickerDriver.tryDispose ] ──► Stops & Disposes Ticker │
│ Empties Reactive Node Controller │
└────────────────────────────────────────────────────────────────────────┘
Architectural Benefits
- Zero-Leak Lifecycle Coupling: Automatically spawns the ticker only when downstream consumers subscribe, and disposes the ticker synchronously when deactivated.
- Hardware-Synchronized Frame Gating: Eliminates asynchronous timer drift by aligning evaluations directly with the device's display refresh rate.
- Built-In Distinct Filtering: When
distinctistrue, suppresses dirty invalidations when consecutiveonTickcalculations yield equal results. - Fail-Fast Error Trapping: Exceptions thrown inside
onTickimmediately halt the ticker and transition the reactive node into an error state (BloomSnapshot.error), preventing runaway 120fps error cascades. - Microtask Remount Deferral: Supports Graceful policies (e.g.
Graceful.microtask()) to prevent rapid ticker restarts during widget tree reshuffles. - Guarded Dynamic Delegation: Uses switched with
.hasData.guarded(...)to isolate uninitialized or torn-down states without runtime null dereference panics.
onTick: Callback computing state of typeTfrom the elapsed frame Duration. Evaluated synchronously with Duration.zero during driver creation to seed the initial state, and subsequently on each display frame tick.distinct: Whentrue, suppresses dirty invalidation if consecutiveonTickevaluations produce equal values. Defaults tofalse.equals: Custom equality comparator used whendistinctis enabled.graceful: Optional Graceful policy deferring ticker teardown across rapid remounts.
Requires:
- If
equalsis provided,distinctMUST be set totrue. onTickshould be a deterministic function and avoid synchronous mutations to external state.
Ensures:
- Returns a reactive Bloom<T> node driven continuously by display frame ticks.
Throws:
- Throws an AssertionError in debug mode if
equalsis provided whiledistinctisfalse.
Warning (Anti-Pattern Prevention):
- Do NOT invoke
vsync.drive(...)repeatedly insidebuild()methods without caching. Invokingdrive(...)on every build recreates the underlying managed resource pipeline, spawning redundant tickers and causing resource thrashing. - For stateful widgets with SingleTickerProviderStateMixin, cache the driven node in a
late finalfield of theStateclass. - For inline widget rendering without a
StatefulWidget, usecomposed.drive(...)instead.
BAD:
// [Anti-Pattern] Spawning new frame ticker pipeline on every widget build pass
class ClockState extends State<ClockWidget> with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
final secondBloom = drive((elapsed) => elapsed.inSeconds % 60); // Allocates every frame!
return composed((context, via) => Text('Second: ${via(secondBloom)}'));
}
}
GOOD:
// [Recommended] Cache once in a late final field of State
class ClockState extends State<ClockWidget> with SingleTickerProviderStateMixin {
late final secondBloom = drive(
(elapsed) => elapsed.inSeconds % 60,
distinct: true,
);
@override
Widget build(BuildContext context) {
return composed((context, via) => Text('Second: ${via(secondBloom)}'));
}
}
Examples
1. Continuous 120Hz Animation Driver
class SpinnerState extends State<SpinnerWidget> with SingleTickerProviderStateMixin {
late final angleBloom = drive(
(elapsed) => (elapsed.inMicroseconds % 1000000) / 1000000.0 * 2 * math.pi,
);
@override
Widget build(BuildContext context) {
return composed((context, via) => Transform.rotate(
angle: via(angleBloom),
child: const Icon(Icons.autorenew),
));
}
}
2. Throttled Second Counter with Microtask Remount Protection
class TimerState extends State<TimerWidget> with SingleTickerProviderStateMixin {
late final secondBloom = drive(
(elapsed) => elapsed.inSeconds,
distinct: true,
graceful: Graceful.microtask(),
);
@override
Widget build(BuildContext context) {
return composed((context, via) => Text('Elapsed: ${via(secondBloom)}s'));
}
}
Implementation
Bloom<T> drive<T>(
T Function(Duration elapsed) onTick, {
bool distinct = false,
bool Function(T previous, T next)? equals,
Graceful? graceful,
}) {
assert(
equals == null || distinct,
'If [equals] is provided, [distinct] MUST be set to true.',
);
final resourceNode = managed<_TickerDriver<T>>(
eager: true,
create: (via) {
return _TickerDriver<T>(
this,
onTick: onTick,
distinct: distinct,
equals: equals,
);
},
dispose: (driver) {
driver.tryDispose(this);
},
graceful: graceful,
);
return switched(
(via) => via(resourceNode).bloom,
guard: (via) => via.state(resourceNode).hasData.guarded(
untracked.state(resourceNode).emptyReason ?? 'TickerNotInitialized',
),
eager: true,
);
}