In Flutter, what does the vsync argument of an AnimationController do, and when do you use SingleTickerProviderStateMixin versus TickerProviderStateMixin?
answer
- a TickerProvider creates the Ticker
- one callback per frame
- TickerMode can mute it
- Single: exactly one ticker, ever
- replacing a controller needs the plural
basics
~20 svsync is the TickerProvider that creates the controller's Ticker, which calls it once per frame and can be muted by TickerMode. SingleTickerProviderStateMixin vends exactly one ticker per State; use TickerProviderStateMixin for several controllers or a replaced one.
solid answer
~40 sAn `AnimationController` does not own a timer. Its constructor calls `vsync.createTicker(...)`, and the resulting `Ticker` invokes the controller once per displayed frame with the elapsed time, from which the controller computes `value`. Passing `vsync: this` from a `State` that mixes in a ticker provider ties that ticker to the widget: when an ancestor `TickerMode` disables tickers, for example on a route covered by an opaque one, the ticker is muted. `SingleTickerProviderStateMixin` allows exactly one `createTicker` call over the State's life; a second call, even after disposing the first controller, fails an assertion saying multiple tickers were created. `TickerProviderStateMixin` keeps a set of tickers, so use it for several controllers or for replacing a controller later. Both assert at `dispose` that no ticker is still active, which is why controllers are disposed before `super.dispose()`.
code
dart · 43 linesimport 'package:flutter/material.dart';
class DictationIndicator extends StatefulWidget {
const DictationIndicator({super.key});
@override
State<DictationIndicator> createState() => _DictationIndicatorState();
}
// Two controllers, so the plural mixin.
class _DictationIndicatorState extends State<DictationIndicator>
with TickerProviderStateMixin {
late final AnimationController _pulse = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
)..repeat(reverse: true);
late final AnimationController _level = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 120),
);
// level is a microphone level already normalised to 0.0-1.0.
void onInputLevel(double level) => _level.animateTo(level);
@override
void dispose() {
_pulse.dispose();
_level.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: Tween<double>(begin: 1.0, end: 1.15).animate(_pulse),
child: SizeTransition(
sizeFactor: _level,
axis: Axis.horizontal,
child: const Icon(Icons.graphic_eq),
),
);
}
}go deeper
Recall that vsync: this needs a ticker mixin on the State and that the controller must be disposed.
Explain the Ticker's per-frame callback, how the mixins connect it to TickerMode, and the exact one-ticker rule of the single mixin.
Catch designs that recreate controllers, choose the mixin from the controller count, and prefer mutating duration over replacement.
Set a convention for animation-heavy screens: one State per animated concern, plural mixin only where the count is truly dynamic.
## What vsync is An **`AnimationController`** produces a `double` that moves between `lowerBound` and `upperBound` (0.0 and 1.0 by default) over a `duration`. It needs something to tell it **when a new frame is being drawn**, so that it computes one value per frame — no more, which would waste work, and no less, which would stutter. That something is a **`Ticker`**: an object that calls a callback once per frame with the time elapsed since it started. The controller does not create the ticker itself. Its constructor takes a required **`vsync`** parameter of type **`TickerProvider`** and calls `vsync.createTicker(_tick)`. The name refers to vertical sync: ticks line up with the display's frames. ## Why a State provides it Inside a widget you pass `vsync: this` from a `State` that mixes in a ticker provider. The mixin adds two behaviours on top of a bare `Ticker`: - **`TickerMode` awareness.** The mixin reads the nearest `TickerMode` ancestor and **mutes** its tickers when that ancestor disables them. The framework uses this for routes hidden under an opaque route, the fading-out child of an `AnimatedCrossFade`, and inactive `CupertinoTabScaffold` tabs, so off-screen animations stop costing frames. - **Lifecycle checks.** When the State is disposed, the mixin asserts that no ticker it created is still active, catching controllers that were never disposed. ## The two mixins | | `SingleTickerProviderStateMixin` | `TickerProviderStateMixin` | |---|---|---| | Tickers per State | exactly one, over the State's whole life | any number | | Second `createTicker` | assertion: "…is a SingleTickerProviderStateMixin but multiple tickers were created." | allowed | | Replacing a controller | not allowed, even after disposing the old one | allowed | | Bookkeeping | a single field | a set of tickers | | Typical use | one pulsing or looping animation | several controllers, or one recreated on configuration change | The single-ticker rule is stricter than it sounds. The mixin remembers that it already vended a ticker and never forgets, so this pattern fails: 1. `initState` creates `_controller` with `vsync: this`. 2. `didUpdateWidget` sees a new configuration, disposes `_controller`, and creates a new one with `vsync: this`. 3. The second `createTicker` call trips the assertion. Either switch to `TickerProviderStateMixin`, or — usually better — keep the one controller and change it in place: `controller.duration = newDuration` is a plain setter. ## A dictation-app example A dictation app has a record button that pulses while recording. It needs one controller for the pulse, so `SingleTickerProviderStateMixin` fits: ```dart class _RecordButtonState extends State<RecordButton> with SingleTickerProviderStateMixin { late final AnimationController _pulse = AnimationController( vsync: this, duration: const Duration(milliseconds: 900), ); // ... } ``` If a later design adds a second controller — say a waveform that reacts to input level — the State switches to `TickerProviderStateMixin`; nothing else changes. ## Common mistakes - **Using `SingleTickerProviderStateMixin` with two controllers** — the second constructor call throws in debug builds. - **Recreating a controller in `didUpdateWidget` or `build`** — under the single mixin it asserts; under the plural one it works, but building controllers in `build` is always wrong because `build` runs often. - **Creating the controller in a field initialiser that runs before `this` can serve as a provider** — `late final` defers it until first use, which is after the State is mounted. - **Forgetting that `vsync` also mutes.** A controller built with a provider that is not widget-aware ignores `TickerMode` and keeps running when its screen is covered. - **Calling `super.dispose()` before `_controller.dispose()`** — the mixin's `dispose` then finds an active ticker and asserts. ## Choosing in practice 1. Count the controllers the State will ever create, including any you might recreate. 2. Exactly one, created once: `SingleTickerProviderStateMixin`. 3. More than one, or a number that changes: `TickerProviderStateMixin`. 4. Tempted to recreate a controller to change its timing? Set `duration` or `reverseDuration` on the existing one instead; both are mutable fields. 5. Several unrelated animated parts on one screen? Consider splitting them into their own widgets, each with its own State and single mixin, which also keeps rebuilds local.
- Why does SingleTickerProviderStateMixin fail even when you dispose the first controller before creating the second?The mixin stores the ticker it vended and asserts that none was vended before; disposing the controller does not clear that record. It exists for the common case of exactly one controller for the State's life. Replacing controllers needs `TickerProviderStateMixin`, or better, reuse one controller and change its `duration`.
- What changes if you give a controller a TickerProvider that is not a State mixin?The ticker still fires once per frame, but nothing connects it to the widget tree, so `TickerMode` cannot mute it. It keeps running on a covered route or a hidden tab, and there is no dispose-time assertion to catch a controller left running.
- When would you pick SingleTickerProviderStateMixin over the plural one if both work?When the State has exactly one controller for its whole life. The single mixin keeps one field instead of a set, and its assertion documents and enforces the one-controller design.
A metronome handed out by the conductor: the controller only plays a note when the metronome clicks, and when the conductor mutes a section, its metronomes go quiet; the single mixin is a conductor who hands out exactly one metronome per player, ever.
saying these in an interview costs you the question
- vsync makes the animation wait for the GPU before each value change.
- SingleTickerProviderStateMixin allows a new controller once the old is disposed.
- TickerProviderStateMixin must be used whenever there is any animation.
- vsync only matters on iOS, where displays run at 120 Hz.
- The mixin disposes the AnimationController automatically.