vsync and TickerProvider

Medium PriorityAsked in ~60% of mid-level interviews

3 min read

Animations

vsync is a TickerProvider — it ties the animation to your widget's lifecycle so it pauses when the route is hidden, resumes when shown, and stops when the State is disposed. Without this binding, animations would tick forever even off-screen, burning CPU and battery.

Every active AnimationController is driven by a Ticker that subscribes to Flutter's vsync signal — one callback per frame (~60Hz). The TickerProvider:

Wires upEffect
Frame callbacksAnimation values advance once per frame
Route visibilityAuto-mutes when the route is off-screen (no CPU spent)
App lifecycleAuto-mutes when app is backgrounded
Widget disposalThrows "ticker was active when disposed" if you forget to dispose()

That last point is the safety net — Flutter screams in debug if you leak a Ticker, so you find leaks before they ship.


Code in action

// Single animation → SingleTickerProviderStateMixin
class _State extends State<MyAnim>
    with SingleTickerProviderStateMixin {
  late final _ctrl = AnimationController(
    vsync: this,                        // this State IS the TickerProvider
    duration: const Duration(seconds: 1),
  );

  @override
  void dispose() {
    _ctrl.dispose();                    // CRITICAL — ticker is on a global frame list
    super.dispose();
  }
}

// Multiple animations → TickerProviderStateMixin
class _MultiState extends State<MyAnims> with TickerProviderStateMixin {
  late final _fade  = AnimationController(vsync: this, duration: ...);
  late final _slide = AnimationController(vsync: this, duration: ...);

  @override
  void dispose() {
    _fade.dispose();
    _slide.dispose();
    super.dispose();
  }
}

Pick the right mixin

You have…Use
Exactly one AnimationControllerSingleTickerProviderStateMixin (lighter — one ticker, less overhead)
Two or more controllersTickerProviderStateMixin
A non-State class that needs to drive an animationImplement TickerProvider yourself, or pass one in
One-off custom timing without controllersTicker(onTick, _) ..start() (rarely needed)

Common mistakes to avoid

// ❌ Using SingleTickerProviderStateMixin with TWO controllers
class _S extends State<X> with SingleTickerProviderStateMixin {
  late final a = AnimationController(vsync: this, ...);
  late final b = AnimationController(vsync: this, ...);  // 💥 assertion failure
}
// ✅ Use TickerProviderStateMixin

// ❌ Forgetting dispose()
late final _ctrl = AnimationController(vsync: this, ...);
// "Ticker was active when its TickerProvider was disposed" in debug

// ❌ Creating an AnimationController in build()
@override
Widget build(BuildContext context) {
  final c = AnimationController(vsync: this, ...);    // recreated every rebuild → leak storm
}
// ✅ Create in initState (or `late final` field), dispose in dispose

// ❌ Re-using a disposed controller
_ctrl.dispose();
_ctrl.forward();                                       // 💥 use-after-dispose

// ❌ Manually pausing tickers when the screen goes off
// Flutter does this for you when the route is hidden — don't second-guess it

Interview follow-ups

  1. What's the difference between SingleTickerProviderStateMixin and TickerProviderStateMixin? SingleTicker… allocates one ticker and asserts you don't ask for more — slightly cheaper and a useful guardrail. TickerProvider… allows multiple controllers from the same State. Pick the strictest one that fits.

  2. What happens to an AnimationController when the screen is off-route? The ticker mutes — it stops receiving frame callbacks, so the animation effectively pauses. When the route comes back to the top, ticking resumes. That's the whole point of binding vsync to State.

  3. Why does Flutter assert "ticker was active when disposed"? To catch leaked controllers in debug. A live Ticker holds a reference to its TickerProvider (your State). If you don't dispose the controller, the State (and everything it captures) can't be garbage collected. The assertion makes the leak obvious.

  4. Can you drive an AnimationController without a TickerProvider? Technically yes — AnimationController.unbounded(value: 0, vsync: SomeOther) and Animation from a custom ValueListenable. But for almost any UI animation, you want the lifecycle-binding the State mixin gives you. Skip vsync only for animations that should genuinely run forever.


How helpful was this content?

Please sign in to rate this article.