Irbisa · Cheatsheet

Animations & Motion

AnimationController, Ticker, Tween, Curves, AnimatedBuilder, staggered motion, Lottie and Rive

Middle Developer · 21 questions

  1. 01

    What are Animation, AnimationController, Tween and Ticker in Flutter, and how do they work together?

    An AnimationController produces a number that moves from 0 to 1 over a duration, advanced once per frame by a Ticker; a Tween maps that 0..1 onto the range you need, and the result is an Animation<T> that transition widgets listen to and redraw from.

    class _PopInState extends State<PopIn> with SingleTickerProviderStateMixin {  // vsync: this -> the mixin creates the Ticker that drives the controller  late final AnimationController _controller = AnimationController(    vsync: this,    duration: const Duration(milliseconds: 400),  )..forward();…
  2. 02

    How does an implicit animation widget like AnimatedContainer or AnimatedOpacity know what to animate?

    You rebuild it with a new property value; it compares that value with the previous one and animates from what is on screen now to the new target over its duration and curve, with a controller it creates and disposes itself.

    class _ToggleCardState extends State<ToggleCard> {  bool _open = false;   @override  Widget build(BuildContext context) {    return GestureDetector(…
  3. 03

    What is wrong with this State, and why does its widget test fail with "was disposed with an active Ticker"?

    class _FaderState extends State<Fader> with SingleTickerProviderStateMixin {  @override  Widget build(BuildContext context) {    final controller = AnimationController(      vsync: this,      duration: widget.duration,    )..forward();    return FadeTransition(opacity: controller, child: widget.child);  }}

    The controller is created in build and never disposed.

  4. 04

    Why does an AnimationController need vsync, and when do you need TickerProviderStateMixin instead of SingleTickerProviderStateMixin?

    vsync supplies the Ticker that advances the controller once per frame and mutes it when its subtree is hidden.

  5. 05

    What does this code print, and why does stop() not change the controller's status?

    final c = AnimationController(  vsync: this,  duration: const Duration(milliseconds: 400),)..addStatusListener((s) => debugPrint(s.name)); c.forward();await Future<void>.delayed(const Duration(milliseconds: 200));c.stop();debugPrint('${c.status.name} animating=${c.isAnimating}');c.reverse();

    It prints forward, then forward animating=false, then reverse, and about 200 ms later dismissed.

  6. 06

    Find the bug: the like heart pops on the first tap, but every later tap toggles the icon without any animation.

    class _LikeState extends State<Like> with SingleTickerProviderStateMixin {  late final _pop = AnimationController( // disposed in dispose()      vsync: this, duration: const Duration(milliseconds: 300));  late final _scale = _pop.drive(TweenSequence([    TweenSequenceItem(tween: Tween(begin: 1.0, end: 1.3), weight: 1),    TweenSequenceItem(tween: Tween(begin: 1.3, end: 1.0), weight: 1),  ]));  @override  Widget build(BuildContext context) => IconButton(        onPressed: () {          widget.onToggle();          _pop.forward();        },        icon: ScaleTransition(scale: _scale, child: Icon(widget.icon)),      );}

    forward() runs the controller toward its upper bound from the current value.

  7. 07

    How does a Tween turn a controller's 0..1 into a Color, an Offset or a TextStyle, and what does tween.animate(controller) return?

    A Tween is a formula, not an animation: give it begin and end, and transform(t) returns the value at t between them.

    class _ProfileCardState extends State<ProfileCard>    with SingleTickerProviderStateMixin {  late final _c = AnimationController(      vsync: this, duration: const Duration(milliseconds: 350))..forward();   // Offset has + - *, so the base Tween works…
  8. 08

    Why does Curves.easeOutBack break an Opacity animation, and how do you keep the overshoot where it is allowed?

    Curves.easeOutBack and the elastic curves deliberately go past 1 before settling (the in-curves also dip below 0), so downstream tweens receive values like 1.09.

    late final _c = AnimationController(    vsync: this, duration: const Duration(milliseconds: 420)); // Scale may overshoot: easeOutBack peaks above 1.0late final _pop = CurvedAnimation(  parent: _c,…
  9. 09

    How do you write a custom animation curve, and what must it return at t = 0 and t = 1?

    Subclass Curve and override transformInternal(double t) to map time 0..1 to progress.

    // A designer's cubic-bezier(0.2, 0, 0, 1): no subclass neededconst emphasized = Cubic(0.2, 0.0, 0.0, 1.0); // CSS steps(n): jumps in n equal steps, e.g. a ticking clock handclass StepsCurve extends Curve {  const StepsCurve(this.steps) : assert(steps > 0);…
  10. 10

    When do you use AnimatedBuilder, a custom AnimatedWidget, or a built-in *Transition widget to show an explicit animation?

    All three listen to an Animation and redraw on its ticks, so you never call setState yourself.

    // One-off effect: AnimatedBuilder, with the static subtree passed as childAnimatedBuilder(  animation: _controller,  child: const ProductImage(), // built once, not on every tick  builder: (context, child) => Transform(    alignment: Alignment.center,…
  11. 11

    You need to pop a screen exactly when its exit animation ends: await controller.reverse() or a status listener, and what if the user interrupts it?

    forward() and reverse() return a TickerFuture that completes only if the animation reaches its end.

    // wrong: after an interruption this never returns, so callers awaiting close() hang and finally blocks never runFuture<void> closeNaive() async {  await _c.reverse();  Navigator.of(context).pop();}…
  12. 12

    Find the bug: after the third wrong PIN, a single shake logs the pin_error_shown analytics event three times.

    class _PinFieldState extends State<PinField> with SingleTickerProviderStateMixin {  late final _shake = AnimationController(vsync: this, duration: Durations.medium4);  void onWrongPin() {    _shake.addStatusListener((s) {      if (s.isCompleted) widget.onShaken();    });    _shake.forward(from: 0);  }  @override  Widget build(BuildContext context) => AnimatedBuilder(        animation: _shake,        builder: (_, child) => Transform.translate(            offset: Offset(sin(_shake.value * pi * 6) * 12, 0), child: child),        child: widget.child,      );} // dispose() disposes _shake

    onWrongPin adds a new status listener on every call and never removes it.

  13. 13

    A card fades in, then its title slides up, then the button pops: one controller with Intervals or three controllers, and how do you keep them in sync?

    One controller: a stagger is a single timeline, the controller is that timeline, and each Interval says which slice of it a property owns.

    // One clock for the whole sequence: 300 + 300 + 300 mslate final _c = AnimationController(    vsync: this, duration: const Duration(milliseconds: 900))..forward(); // Each slice is a FRACTION of that durationlate final Animation<double> _cardFade = _c.drive(…
  14. 14

    A progress badge animates toward a value its parent passes down, and a new value lands mid-flight: what does TweenAnimationBuilder do with your tween?

    It takes over your tween: on every new end it sets begin to the value currently on screen, end to the new target, and restarts its internal controller from 0.

    class UploadBadge extends StatelessWidget {  const UploadBadge({super.key, required this.progress});   final double progress; // the parent may pass a new target at any time   @override…
  15. 15

    Your AnimatedSwitcher swaps the text but never animates: how does it decide the child is new, and how do you fix it?

    AnimatedSwitcher decides "this is a new child" with Widget.canUpdate: same runtime type and same key means the same widget.

    // wrong: same type, no key -> Widget.canUpdate says "same widget", no animationAnimatedSwitcher(  duration: const Duration(milliseconds: 250),  child: Text('$_count'),);…
  16. 16

    Why does AnimatedList.removeItem need a builder for a row you already removed from your data, and what must happen in the same step?

    Because the deleted row must stay on screen while it animates out, and your data no longer holds it, so AnimatedList asks you to hand the widget back through the builder you pass to removeItem.

    class _InboxState extends State<Inbox> {  final _listKey = GlobalKey<AnimatedListState>();  final List<Mail> _mails = List.of(seedMails);   Widget _row(Mail mail, Animation<double> animation) => SizeTransition(        sizeFactor: animation,…
  17. 17

    A designer hands you a four-second celebration animation: when do you choose Lottie, Rive or a hand-built Flutter animation, and how do you play it from code?

    Lottie plays a fixed After Effects timeline you start, loop or seek; Rive runs a state machine whose inputs you set from Dart, so it suits animations that react to input; hand-built Flutter motion is ordinary code that follows your theme and app state and is easy to test.

    late final _c = AnimationController(vsync: this); Lottie.asset(  'assets/celebrate.json',  controller: _c,  onLoaded: (composition) {…
  18. 18

    A card grows when a details section opens and its height is unknown until layout runs: how do you animate a size you cannot write a Tween for?

    Let layout produce the numbers and animate the container instead of the value: AnimatedSize measures its child, and when that measurement changes it animates its own size from the old one to the new one; you never write a begin or an end.

    // The height comes out of layout, so animate the box, not a numberAnimatedSize(  duration: const Duration(milliseconds: 220),  curve: Curves.easeOut,  alignment: Alignment.topCenter,  child: Column(…
  19. 19

    How do you build your own implicit AnimatedX widget: what does ImplicitlyAnimatedWidget make you implement, and what is the contract of forEachTween?

    Extend ImplicitlyAnimatedWidget, put its State on AnimatedWidgetBaseState, and implement one method, forEachTween: you hand the framework each tween you keep and store back the one it returns.

    class AnimatedRing extends ImplicitlyAnimatedWidget {  const AnimatedRing({    super.key,    required this.color,    required this.thickness,    required super.duration,…
  20. 20

    A bottom sheet has to follow the user's thumb and finish the motion on its own when they let go: how do you wire an AnimationController to a drag?

    While the finger is down you write controller.value yourself on every drag update; when it lifts you pass the gesture's velocity to controller.fling(velocity: …), and the controller finishes the motion with a spring at the speed the thumb left behind.

    class _SheetState extends State<Sheet> with SingleTickerProviderStateMixin {  static const double _extent = 320; // sheet height in logical pixels   // 0 = hidden, 1 = open  late final _c = AnimationController(      vsync: this, duration: const Duration(milliseconds: 300));…
  21. 21

    The card snaps back with easeOut and testers say the motion feels dead: when does a physics simulation beat any curve you could pick?

    A curve has a fixed shape and a fixed duration, so it cannot use the speed the card already has; a Simulation starts from the real velocity and physical constants, and its duration falls out of the physics.

    // unbounded: an underdamped spring passes its target,// and a 0..1 controller would clamp that overshoot awaylate final _c = AnimationController.unbounded(vsync: this); static final _spring = SpringDescription.withDampingRatio(  mass: 1,…