DurableAnimationController class sealed
A root controller interface governing animation timelines with state durability, dormancy virtualization, and automatic lifecycle coupling.
DurableAnimationController operates on an Inversion of Control (IoC) Dormancy Model
where physical Ticker instances are allocated strictly on demand when mounted in the widget tree
(via composed.animate) and automatically torn down upon element unmount.
System Architectural Flow
┌────────────────────────────────────────────────────────────────────────┐
│ DurableAnimationController │
│ │
│ • Governs durable timeline across dormant unmounts/remounts │
│ • Stores DurabilityPolicy (resume, catchUp, reset) │
│ • Stages motions programmatically while dormant without ticker errors│
└───────────────────────────────────┬────────────────────────────────────┘
│ mounts via composed.animate(...)
▼
┌────────────────────────────────────────────────────────────────────────┐
│ ScopedAnimationController │
│ │
│ • Ephemeral proxy scoped strictly to mounted element lifecycle │
│ • Exposes .view and .drive (auto-purged on unmount to prevent leaks) │
└───────────────────────────────────┬────────────────────────────────────┘
│ provisions on-demand vsync
▼
┌────────────────────────────────────────────────────────────────────────┐
│ _DurableVsyncSocketElementMixin │
│ │
│ • Single VSYNC socket host driving the controller │
│ • Reclaims Ticker synchronously on unmount (zero disposal burden) │
└────────────────────────────────────────────────────────────────────────┘
Key Invariants & Architectural Benefits
- Framework-Governed Vsync Lifecycle & Zero Disposal Burden: Physical tickers are allocated
strictly on demand when mounted via
composed.animateand are synchronously torn down at the engine level upon element unmount. Callingdispose()manually is strictly redundant; never introduceStatefulWidgetor manual.stop()cascades solely to reclaim tickers. - Single VSYNC Authority & Multi-Widget Orchestration: A DurableAnimationController governs
a single, continuous timeline driven by a single host socket. Child widgets observe the controller's
reactive state (e.g., via
composed.animateor reactive transitions) as pure consumers. - Heterogeneous TickerMode Isolation: If two widgets require independent clock lifecycles (for instance, one widget pauses its animation via TickerMode while another continues running), they MUST NOT share a single controller. Each independent lifecycle domain must possess its own DurableAnimationController instance.
- Safe Staged Motion: Programmatic animations (
forward,reverse,animateTo,fling, etc.) triggered while dormant are safely staged as_StagedMotionand seamlessly executed upon remount.
Warning (Anti-Pattern Prevention):
- Do NOT create
StatefulWidgetwrappers orSingleTickerProviderStateMixinmerely to host or dispose a DurableAnimationController. Hold the controller in stores, view models, or widget fields, and let framework-managed primitives (BloomAnimatedWidget orcomposed.animate) manage ticker allocation. - Do NOT escape ScopedAnimationController or its ScopedAnimationController.view outside the builder closure.
Architectural Best Practices: Boundary vs. Local Leaf Animation
When integrating a DurableAnimationController with UI widgets, select between
BloomAnimatedWidget and composed.animate based on the architectural scope:
Scenario A: Independent Animated Component Boundary
When creating a dedicated, reusable animated widget whose entire responsibility is animating its visual representation:
GOOD (Class-Based Component Boundary):
// [Recommended] Encapsulates animation lifecycle directly with single Element allocation
base class RadarScanner extends BloomAnimatedWidget {
RadarScanner({
super.key,
required DurableAnimationController controller,
}) : super(controller: controller);
@override
Widget build(
BuildContext context,
BloomVia via,
ScopedAnimationController anim,
) {
return RotationTransition(
turns: anim.view,
child: const Icon(Icons.radar),
);
}
}
BAD (Redundant 1:1 Wrapping):
// [Anti-Pattern] Unnecessary StatelessWidget wrapper (triggers prefer_bloom_widget lint)
class RadarScanner extends StatelessWidget {
final DurableAnimationController controller;
const RadarScanner({super.key, required this.controller});
@override
Widget build(BuildContext context) {
return composed.animate(controller, (context, via, anim) {
return RotationTransition(turns: anim.view, child: const Icon(Icons.radar));
});
}
}
Scenario B: Localized Leaf/Subtree Animation inside a Layout
When an animated effect occupies a sub-branch of a composite screen or layout:
GOOD (Localized Leaf Animation with composed.animate.child):
// [Recommended] Confines 60/120fps ticks strictly to the animated leaf.
// Outer layout, dynamic header, and cached child subtree do NOT rebuild on ticks.
class ExpandableProfileCard extends StatelessWidget {
final String title;
final DurableAnimationController expandController;
const ExpandableProfileCard({
super.key,
required this.title,
required this.expandController,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
ProfileHeader(title: title), // Built once, never rebuilt on animation ticks
composed.animate.child(
expandController,
(context, via, anim, child) => SizeTransition(
sizeFactor: anim.view,
child: child,
),
// Evaluated once here, then cached across all 60/120fps animation ticks:
child: ExpensiveProfileDetails(title: title),
),
],
);
}
}
BAD (Global Layout Rebuild via Blanket BloomAnimatedWidget):
// [Anti-Pattern] Escalating the entire composite layout to BloomAnimatedWidget
// forces ExpandableProfileCard.build() to re-execute on every 120fps tick,
// re-evaluating Column, allocating new child lists, and rebuilding dynamic headers!
base class ExpandableProfileCard extends BloomAnimatedWidget {
final String title;
ExpandableProfileCard({
super.key,
required this.title,
required DurableAnimationController expandController,
}) : super(controller: expandController);
@override
Widget build(
BuildContext context,
BloomVia via,
ScopedAnimationController anim,
) {
// Entire layout rebuilds 60~120 times per second during animation:
return Column(
children: [
ProfileHeader(title: title), // Rebuilt on every tick because parent re-runs build()!
SizeTransition(
sizeFactor: anim.view,
child: ExpensiveProfileDetails(title: title),
),
],
);
}
}
BAD (Missing Subtree Caching via composed.animate):
// [Anti-Pattern] Inlining a dynamic child subtree directly inside the closure creates
// a new widget instance on every 120fps frame, forcing the child element to rebuild
// (triggers prefer_composed_child lint).
class ExpandableProfileCard extends StatelessWidget {
final String title;
final DurableAnimationController expandController;
const ExpandableProfileCard({
super.key,
required this.title,
required this.expandController,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
ProfileHeader(title: title),
composed.animate(expandController, (context, via, anim) {
// Re-instantiated on every 120fps tick! Use composed.animate.child instead.
return SizeTransition(
sizeFactor: anim.view,
child: ExpensiveProfileDetails(title: title),
);
}),
],
);
}
}
Scenario C: Legacy Ticker Provider Anti-Pattern
BAD (Manual Ticker Ceremony & Disposal):
// [Anti-Pattern] Manual ticker ceremony, redundant disposal, and escaped view
class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin {
late final AnimationController controller; // Requires manual dispose()!
@override
void initState() {
super.initState();
controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));
}
@override
void dispose() {
controller.dispose(); // Burden of manual lifecycle management
super.dispose();
}
}
- Implementers
- Available extensions
Constructors
- DurableAnimationController({DurabilityPolicy policy = DurabilityPolicy.resume, double? value, Duration? duration, Duration? reverseDuration, String? debugLabel, double lowerBound = 0.0, double upperBound = 1.0, AnimationBehavior animationBehavior = AnimationBehavior.normal, DateTime clock() = DateTime.now})
-
Creates a state-durable animation controller with bounded limits.
factory
- DurableAnimationController.unbounded({DurabilityPolicy policy = DurabilityPolicy.resume, double value = 0.0, Duration? duration, Duration? reverseDuration, String? debugLabel, AnimationBehavior animationBehavior = AnimationBehavior.preserve, DateTime clock() = DateTime.now})
-
Creates a state-durable animation controller with unbounded limits.
factory
Properties
- animationBehavior → AnimationBehavior
-
The behavior of the controller when accessibility settings disable animations.
no setter
- debugLabel → String?
-
A label that is used in the toString output. Intended to aid with
identifying animation controller instances in debug output.
no setter
- duration ↔ Duration?
-
The length of time this animation should last.
getter/setter pair
- hashCode → int
-
The hash code for this object.
no setterinherited
- isActive → bool
-
Whether the underlying animation controller engine is currently active.
no setter
- isAnimating → bool
-
Whether this animation is currently animating.
no setter
- isCompleted → bool
-
Whether this animation is stopped at the end (status is AnimationStatus.completed).
no setter
- isDismissed → bool
-
Whether this animation is stopped at the beginning (status is AnimationStatus.dismissed).
no setter
- isDormant → bool
-
Whether this controller is currently in a dormant state (hibernated / inactive without an active TickerProvider).
no setter
- isForwardOrCompleted → bool
-
Whether the current aim of the animation is toward completion.
no setter
- lastElapsedDuration → Duration?
-
The amount of time that has elapsed since the animation started.
no setter
- lowerBound → double
-
The value at which this animation is deemed to be dismissed.
no setter
- policy → DurabilityPolicy
-
The durability policy governing timeline preservation across dormant deactivations.
no setter
- reverseDuration ↔ Duration?
-
The length of time this animation should last when going in reverse.
getter/setter pair
- runtimeType → Type
-
A representation of the runtime type of the object.
no setterinherited
- status → AnimationStatus
-
The current status of this animation.
no setter
- upperBound → double
-
The value at which this animation is deemed to be completed.
no setter
- value ↔ double
-
The current value of the animation.
getter/setter pair
- velocity → double
-
The rate of change of value per second.
no setter
-
view
→ Animation<
double> -
Available on DurableAnimationController, provided by the DurableAnimationTestingExtension extension
Returns the underlying raw view strictly for unit testing harnesses.no setter
Methods
-
animateBack(
double target, {Duration? duration, Curve curve = Curves.linear}) → TickerFuture - Drives the animation from its current value to the given target, "backward".
-
animateBackWith(
Simulation simulation) → TickerFuture -
Drives the animation according to the given
simulationwith a status of reverse. -
animateTo(
double target, {Duration? duration, Curve curve = Curves.linear}) → TickerFuture - Drives the animation from its current value to the given target, "forward".
-
animateWith(
Simulation simulation) → TickerFuture -
Drives the animation according to the given
simulation. -
attachVsync(
TickerProvider vsync) → void -
Available on DurableAnimationController, provided by the DurableAnimationTestingExtension extension
Attaches a TickerProvider as an active vsync holder. -
detachVsync(
TickerProvider vsync) → bool -
Available on DurableAnimationController, provided by the DurableAnimationTestingExtension extension
Detaches a TickerProvider from the active vsync holder pool. -
drive<
U> (Animatable< U> child) → Animation<U> -
Available on DurableAnimationController, provided by the DurableAnimationTestingExtension extension
Drives an animatable using the raw view strictly for unit testing harnesses. -
fling(
{double velocity = 1.0, SpringDescription? springDescription, AnimationBehavior? animationBehavior}) → TickerFuture - Drives the animation with a spring and initial velocity.
-
forward(
{double? from}) → TickerFuture - Starts running this animation forwards (towards the end).
-
hibernate(
) → void -
Available on DurableAnimationController, provided by the DurableAnimationTestingExtension extension
Hibernates the controller, stopping the active ticker and clearing vsync holders. -
noSuchMethod(
Invocation invocation) → dynamic -
Invoked when a nonexistent method or property is accessed.
inherited
-
observe(
{bool distinct = true, bool equals(double previous, double next)?, Graceful? graceful}) → Bloom< double> -
Available on DurableAnimationController, provided by the DurableAnimationControllerObserveExtension extension
Observes this controller's animated value as a reactive Bloom<double>. -
observeStatus(
{bool distinct = true, Graceful? graceful}) → Bloom< AnimationStatus> -
Available on DurableAnimationController, provided by the DurableAnimationControllerObserveExtension extension
Observes this controller's animation status as a reactive Bloom of AnimationStatus. -
repeat(
{double? min, double? max, bool reverse = false, Duration? period, int? count}) → TickerFuture - Starts running this animation in the forward direction, and restarts when complete.
-
reset(
) → void - Sets the controller's value to lowerBound, stopping the animation.
-
reverse(
{double? from}) → TickerFuture - Starts running this animation in reverse (towards the beginning).
-
stop(
{bool canceled = true}) → void - Stops running this animation.
-
toggle(
{double? from}) → TickerFuture - Toggles the direction of this animation.
-
toString(
) → String -
A string representation of this object.
inherited
-
toStringDetails(
) → String - Returns detailed diagnostic string representation for debug output.
-
wake(
TickerProvider vsync) → void -
Available on DurableAnimationController, provided by the DurableAnimationTestingExtension extension
Wakes this controller by attachingvsync.
Operators
-
operator ==(
Object other) → bool -
The equality operator.
inherited