Skip to main content

useAnimationController

Creates an AnimationController whose ticker provider and disposal are managed for you. It returns the controller directly, ready to drive transitions.

class FadeInLogo extends HookWidget {
const FadeInLogo({super.key});


Widget build(BuildContext context) {
// Ticker provider and disposal are handled for you.
final controller = useAnimationController(duration: const Duration(milliseconds: 300));

useEffect(() {
controller.forward(); // <- Effects run after the build, so this is safe
return null;
}, const []);

return FadeTransition(
opacity: controller, // <- AnimationController is itself an Animation<double>
child: const FlutterLogo(size: 96),
);
}
}

Signature

AnimationController useAnimationController({
Duration? duration,
Duration? reverseDuration,
String? debugLabel,
double initialValue = 0,
double lowerBound = 0,
double upperBound = 1,
TickerProvider? vsync,
AnimationBehavior animationBehavior = AnimationBehavior.normal,
});

The parameters mirror the AnimationController constructor. When vsync is omitted, the hook calls useSingleTickerProvider for you, so a single controller works with no extra setup. duration and reverseDuration are watched: changing them on a later build updates the live controller rather than recreating it; changing vsync re-syncs it.

Use cases

  • Any widget that owns an animation and would otherwise need a StatefulWidget with SingleTickerProviderStateMixin and a dispose override.

  • Staggered animations - the staggered extension on AnimationController maps one Tween onto a sub-interval [start, end] of the controller, so several properties animate from one controller:

    class StaggeredCard extends HookWidget {
    const StaggeredCard({super.key});


    Widget build(BuildContext context) {
    final controller = useAnimationController(duration: const Duration(milliseconds: 400));

    // Each staggered() maps one tween onto a sub-interval of the same controller.
    final fade = controller.staggered(tween: Tween(begin: 0.0, end: 1.0), start: 0.0, end: 0.6);
    final slide = controller.staggered(
    tween: Tween(begin: const Offset(0, 0.1), end: Offset.zero),
    start: 0.3,
    end: 1.0,
    curve: Curves.easeOut,
    );

    useEffect(() {
    controller.forward();
    return null;
    }, const []);

    return FadeTransition(
    opacity: fade,
    child: SlideTransition(position: slide, child: const Card(child: SizedBox(height: 120))),
    );
    }
    }

Caveats

  • Don't call forward(), repeat(), or animateTo() during the build. Like writing a useState, starting the animation in the build body fights the framework - drive it from an effect or a callback.

    final controller = useAnimationController(duration: const Duration(milliseconds: 300));

    // DON'T
    controller.forward(); // <- Mutating during build

    // DO
    useEffect(() {
    controller.forward(); // <- Effects run after the build
    return null;
    }, const []);
  • This is a thin wrapper over useMemoized(AnimationController.new, [], dispose) plus an automatic vsync. You could write that useMemoized by hand, but the dedicated hook also forwards every constructor argument, re-syncs the ticker, and pushes duration changes onto the live controller - prefer it over rolling your own.

  • For more than one controller in the same widget, pass an explicit vsync from a single TickerProviderStateMixin source, or accept that each useAnimationController provisions its own single ticker. The default ticker is single, matching SingleTickerProviderStateMixin.

See also