vsync and TickerProvider
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 up | Effect |
|---|---|
| Frame callbacks | Animation values advance once per frame |
| Route visibility | Auto-mutes when the route is off-screen (no CPU spent) |
| App lifecycle | Auto-mutes when app is backgrounded |
| Widget disposal | Throws "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 AnimationController | SingleTickerProviderStateMixin (lighter — one ticker, less overhead) |
| Two or more controllers | TickerProviderStateMixin |
| A non-State class that needs to drive an animation | Implement TickerProvider yourself, or pass one in |
| One-off custom timing without controllers | Ticker(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
-
What's the difference between
SingleTickerProviderStateMixinandTickerProviderStateMixin?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. -
What happens to an
AnimationControllerwhen 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. -
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.
-
Can you drive an
AnimationControllerwithout aTickerProvider? Technically yes —AnimationController.unbounded(value: 0, vsync: SomeOther)andAnimationfrom a customValueListenable. 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.