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

  1. Framework-Governed Vsync Lifecycle & Zero Disposal Burden: Physical tickers are allocated strictly on demand when mounted via composed.animate and are synchronously torn down at the engine level upon element unmount. Calling dispose() manually is strictly redundant; never introduce StatefulWidget or manual .stop() cascades solely to reclaim tickers.
  2. 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.animate or reactive transitions) as pure consumers.
  3. 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.
  4. Safe Staged Motion: Programmatic animations (forward, reverse, animateTo, fling, etc.) triggered while dormant are safely staged as _StagedMotion and seamlessly executed upon remount.

Warning (Anti-Pattern Prevention):

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 simulation with 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 attaching vsync.

Operators

operator ==(Object other) → bool
The equality operator.
inherited