vsync_provider Animation Ticker Guide
vsync_provider provides a TickerProvider to descendant Flutter widgets using package:provider. This allows developers to initialize AnimationControllers cleanly without creating boilerplate StatefulWidgets mixed with SingleTickerProviderStateMixin.
Guidelines
- Mounting the Provider:
- Wrap the animated section with
VsyncProvider(child: ...). - By default,
isSingleTicker: trueis used (wrappingSingleTickerProviderStateMixin). If multiple concurrent animations require separate tickers within the same subtree, setisSingleTicker: false.
- Wrap the animated section with
- Retrieving the Ticker:
- Inside descendant builder callbacks or child widgets, retrieve the ticker using
VsyncProvider.of(context)(orcontext.read<TickerProvider>()).
- Inside descendant builder callbacks or child widgets, retrieve the ticker using
- Instantiating Animation Controllers:
- Pass the retrieved
TickerProvidertoAnimationController(vsync: ticker, duration: ...). - Ensure the created
AnimationControlleris disposed when its lifecycle ends (for example, combining it withDisposableProviderorProxyProvider).
- Pass the retrieved
Examples
1. Providing Ticker to AnimationController via MultiProvider
import 'package:disposable_provider/disposable_provider.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:vsync_provider/vsync_provider.dart';
class FadeAnimationController implements Disposable {
FadeAnimationController({required TickerProvider vsync})
: animationController = AnimationController(
vsync: vsync,
duration: const Duration(milliseconds: 500),
);
final AnimationController animationController;
void play() => animationController.forward();
@override
void dispose() {
animationController.dispose();
}
}
class AnimatedScreen extends StatelessWidget {
const AnimatedScreen({super.key});
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
const VsyncProvider(),
DisposableProvider<FadeAnimationController>(
create: (context) => FadeAnimationController(
vsync: VsyncProvider.of(context),
)..play(),
),
],
child: const _AnimatedBody(),
);
}
}
class _AnimatedBody extends StatelessWidget {
const _AnimatedBody();
@override
Widget build(BuildContext context) {
final controller = context.watch<FadeAnimationController>();
return Scaffold(
appBar: AppBar(title: const Text('Ticker Animation')),
body: Center(
child: FadeTransition(
opacity: controller.animationController,
child: const FlutterLogo(size: 100),
),
),
);
}
}
Common Pitfalls & Anti-Patterns
- ❌ Anti-pattern: Attempting to initialize multiple
AnimationControllers usingisSingleTicker: true(which triggers Flutter's runtime single-ticker assertion).- ✔️ Correct: Set
VsyncProvider(isSingleTicker: false)when managing multiple tickers within the same subtree.
- ✔️ Correct: Set
- ❌ Anti-pattern: Creating
AnimationControllerwithout disposing it.- ✔️ Correct: Always pair
AnimationControllerwith a disposal mechanism (e.g.DisposableProvideror an enclosingStatefulWidget).
- ✔️ Correct: Always pair