Flutter Animations Implementation
Goal
Implements and manages Flutter animations, selecting the appropriate animation strategy based on UI requirements. Assumes a working Flutter environment, stateful/stateless widget competence, and a standard widget tree structure. Prefer widgets already in the Flutter SDK — do not add flutter_animate, Lottie, Rive, or the animations package unless the user asked for them or the effect cannot be done with the tree below.
Instructions
1. Determine Animation Strategy (Decision Logic)
Evaluate the UI requirement using the following decision tree. Stop at the first YES — later branches also match earlier ones if you ask generic questions first.
- Does a widget fly between two different screens/routes (shared element)? YES → Hero Animations (step 3).
- Are you animating insertion or removal of items in a scrolling list or grid?
YES →
AnimatedList/AnimatedGrid/SliverAnimatedList/SliverAnimatedGrid(step 4) — seereferences/list-and-grid-animations.md. - Are you morphing one Material icon into another (e.g. menu ↔ close)?
YES →
AnimatedIcon(step 5). - Are you cross-fading between exactly two specific, known widgets (a binary state)?
YES →
AnimatedCrossFade(step 6). - Are you swapping one entire child widget for a different, arbitrary widget — not just a property change?
YES →
AnimatedSwitcher(step 7). - Is this a transition between two routes/screens (not a shared-element fly)?
YES → Page Route Transitions (
PageRouteBuilder) (step 8). - Does the animation model real-world movement (springs, gravity, velocity, drag-and-release)?
YES → Physics-based animation (
SpringSimulation) (step 9). - Do multiple different properties or widgets animate in sequence or overlapping? YES → Staggered Animations (step 10).
- Does a single property need to pass through more than two values (multiple keyframes), not just a straight A → B?
YES →
TweenSequence(step 11). - Is it a simple property change on a single widget, and is there a built-in
Animated*widget for it? (color, size, alignment, opacity, padding, scale, slide, rotation, text style, position) YES → Implicit Animations (step 12). - Is it a one-off tweened transition to a custom value with no dedicated
Animated*widget, and does it NOT need programmatic play/pause/reverse/repeat control? YES →TweenAnimationBuilder(step 13) — no controller to manage or dispose. - Do you need programmatic control, and does a built-in
*Transitionwidget already match the transform (fade, slide, scale, rotation, size, alignment, position, decoration)? YES → that*Transitionwidget (step 14) — seereferences/built-in-transitions.md. - Otherwise
YES → Standard Explicit Animations (
AnimationController+Tween+AnimatedBuilder) (step 15) — the general-purpose fallback.
STOP AND ASK THE USER: If the requirement is ambiguous, pause and ask the user to clarify the desired visual effect before writing implementation code.
2. Controller lifecycle (shared primitive)
Every technique that needs a controller (steps 5, 9–11, 14, 15) reuses this setup. Physics (step 9) may pass unbounded: true. Default duration is 300ms unless the user asked for something else.
class _MyAnimatedWidgetState extends State<MyAnimatedWidget> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
}
@override
void dispose() {
_controller.dispose(); // STRICT REQUIREMENT
super.dispose();
}
}
3. Implement Hero Animations (Shared Element)
To fly a widget between routes, wrap the identical widget tree in both routes with a Hero widget using the exact same tag. If the source and destination trees differ, set flightShuttleBuilder rather than hoping the default flight works.
// Source Route
Hero(
tag: 'unique-photo-tag',
child: Image.asset('photo.png', width: 100),
)
// Destination Route
Hero(
tag: 'unique-photo-tag',
child: Image.asset('photo.png', width: 300),
)
4. Implement Animated Insertion/Removal in Lists and Grids
To animate items being added to or removed from a scrolling list or grid, use AnimatedList/AnimatedGrid (or their sliver equivalents inside a CustomScrollView). Full examples and the removal-builder gotcha (capture the removed item before it leaves the backing data list) are in references/list-and-grid-animations.md. Do not use AnimatedSwitcher or manual setState on a ListView for this — the animation needs the AnimatedList*/AnimatedGrid* family to run correctly on insert/remove.
5. Implement AnimatedIcon
Only the predefined AnimatedIcons.* pairs morph (menu ↔ close, play ↔ pause, etc.). Arbitrary IconData will not. Drive progress with the controller from step 2.
AnimatedIcon(
icon: AnimatedIcons.menu_close,
progress: _controller, // 0.0 = menu, 1.0 = close
semanticLabel: 'Menu',
)
// Toggle:
onPressed: () => _controller.isCompleted ? _controller.reverse() : _controller.forward(),
6. Implement AnimatedCrossFade
When the choice is strictly binary — cross-fading between exactly two known widgets — AnimatedCrossFade is simpler than AnimatedSwitcher and needs no Keys. If the two children have different sizes, set alignment and/or layoutBuilder or the layout will jump when the fade finishes.
AnimatedCrossFade(
duration: const Duration(milliseconds: 300),
crossFadeState: _showFirst ? CrossFadeState.showFirst : CrossFadeState.showSecond,
firstChild: const Icon(Icons.check_circle, size: 48),
secondChild: const Icon(Icons.error, size: 48),
)
Use AnimatedSwitcher (step 7) instead once there are more than two possible children, or the children aren't fixed/known in advance.
7. Implement AnimatedSwitcher
To animate the replacement of one child widget with a completely different, arbitrary widget, use AnimatedSwitcher. Every possible child MUST carry a distinct Key — AnimatedSwitcher diffs on the key to detect a switch, and silently skips the transition if two children share a key (or have none).
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) => FadeTransition(opacity: animation, child: child),
child: _isLoading
? const CircularProgressIndicator(key: ValueKey('loading'))
: Text('Loaded', key: ValueKey('loaded')),
)
8. Implement Page Route Transitions
To animate transitions between routes, use PageRouteBuilder and chain a CurveTween with a Tween<Offset>. If the project already uses GoRouter or ThemeData.pageTransitionsTheme, hook the transition there instead of a one-off Navigator.push.
Route<void> _createRoute() {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => const DestinationPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(0.0, 1.0);
const end = Offset.zero;
const curve = Curves.ease;
final tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
);
}
9. Implement Physics-Based Animations
For realistic motion (e.g., snapping back after a drag), replace the step 2 controller with AnimationController.unbounded(vsync: this), feed drag velocity in from a GestureDetector, and apply a SpringSimulation. stiffness: 1 is a demo value — use ~150–400 for UI snaps.
late Animation<Alignment> _animation;
void _runSpringAnimation(Offset pixelsPerSecond, Size size, Alignment dragAlignment) {
_animation = _controller.drive(
AlignmentTween(begin: dragAlignment, end: Alignment.center),
);
final unitsPerSecondX = pixelsPerSecond.dx / size.width;
final unitsPerSecondY = pixelsPerSecond.dy / size.height;
final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
final unitVelocity = unitsPerSecond.distance;
const spring = SpringDescription(mass: 1, stiffness: 170, damping: 12);
final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);
_controller.animateWith(simulation);
}
10. Implement Staggered Animations
For sequential or overlapping animations across multiple properties, use a single AnimationController (step 2) and define multiple Tweens with Interval curves.
class StaggerAnimation extends StatelessWidget {
StaggerAnimation({super.key, required this.controller}) :
opacity = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.0, 0.100, curve: Curves.ease),
),
),
width = Tween<double>(begin: 50.0, end: 150.0).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.125, 0.250, curve: Curves.ease),
),
);
final AnimationController controller;
final Animation<double> opacity;
final Animation<double> width;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context, child) {
return Opacity(
opacity: opacity.value,
child: Container(width: width.value, height: 50, color: Colors.blue),
);
},
);
}
}
11. Implement TweenSequence
When a single property must pass through more than two values — not just begin → end — use TweenSequence instead of chaining separate animations by hand.
final Animation<double> _animation = TweenSequence<double>([
TweenSequenceItem(tween: Tween(begin: 0.0, end: 100.0), weight: 1),
TweenSequenceItem(tween: Tween(begin: 100.0, end: 50.0), weight: 1),
TweenSequenceItem(tween: Tween(begin: 50.0, end: 200.0), weight: 1),
TweenSequenceItem(tween: Tween(begin: 200.0, end: 0.0), weight: 1),
]).animate(_controller);
weight sets each stage's share of the controller's total duration — equal weights divide the timeline evenly; this is distinct from Staggered Animations (step 10), which staggers multiple different properties/widgets rather than moving one property through multiple stages.
12. Implement Implicit Animations
For simple transitions between values, use implicit animation widgets. Do not manually manage state or controllers. Built-in options include AnimatedContainer, AnimatedOpacity, AnimatedAlign, AnimatedPadding, AnimatedPositioned (inside a Stack), AnimatedScale, AnimatedSlide, AnimatedRotation, AnimatedSize, AnimatedDefaultTextStyle, and AnimatedPhysicalModel. Default to ~300ms and Curves.easeInOut unless the user asked for a bounce or overshoot.
When MediaQuery.disableAnimationsOf(context) is true, use Duration.zero (or skip the animation) so reduced-motion users are not forced through the motion.
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _isExpanded ? 200.0 : 100.0,
height: _isExpanded ? 200.0 : 100.0,
decoration: BoxDecoration(
color: _isExpanded ? Colors.green : Colors.blue,
borderRadius: BorderRadius.circular(_isExpanded ? 50.0 : 8.0),
),
child: const FlutterLogo(),
)
13. Implement TweenAnimationBuilder
For a one-off animation to a custom value with no dedicated implicit widget, and no need to programmatically play/pause/reverse it, use TweenAnimationBuilder. It manages its own internal controller — nothing to dispose. It automatically re-animates from the current value whenever end changes on rebuild.
TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0.0, end: _targetRotation),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
builder: (context, value, child) {
return Transform.rotate(angle: value, child: child);
},
child: const FlutterLogo(), // static subtree passed as child for performance
)
14. Implement Built-in Transition Widgets
Before reaching for AnimatedBuilder, check whether a built-in *Transition widget already renders the effect — they drive the same Animation object but avoid boilerplate. Reuse the controller from step 2. Two of the most common:
FadeTransition(opacity: _controller, child: const FlutterLogo())
SlideTransition(
position: Tween<Offset>(begin: const Offset(0, 1), end: Offset.zero).animate(_controller),
child: const FlutterLogo(),
)
AnimationController is an Animation<double>, so passing _controller is correct for FadeTransition.opacity, ScaleTransition.scale, RotationTransition.turns, and SizeTransition.sizeFactor. For Offset / Alignment / Decoration / RelativeRect, wrap with the matching Tween.
The full catalog — ScaleTransition, RotationTransition, SizeTransition, AlignTransition, PositionedTransition, RelativePositionedTransition, and DecoratedBoxTransition — is in references/built-in-transitions.md. Read it before falling back to a manual AnimatedBuilder (step 15).
15. Implement Standard Explicit Animations (fallback)
When no *Transition widget matches the property, drive a custom Tween through AnimatedBuilder. Same controller lifecycle as step 2.
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_animation = Tween<double>(begin: 0, end: 300).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
_controller.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return SizedBox(
height: _animation.value,
width: _animation.value,
child: child,
);
},
child: const FlutterLogo(), // Passed as child for performance
);
}
16. Validate-and-Fix Loop
After generating animation code, verify the following:
- Does the
Stateclass useSingleTickerProviderStateMixin(orTickerProviderStateMixinfor multiple controllers)? - Is
_controller.dispose()explicitly called in thedispose()method? - If using
AnimatedBuilderorTweenAnimationBuilder, is the static widget passed to thechildparameter rather than rebuilt inside thebuilderfunction? - If using
AnimatedSwitcher, does every branch of the child expression have a distinct, stableKey? - If using a
*Transitionwidget: raw_controlleris fine whereAnimation<double>is required;Offset/Alignment/Decoration/RelativeRectparameters MUST go through the matchingTween. - If using
AnimatedList/AnimatedGrid, is the backing data list mutated in the correct order relative toinsertItem/removeItem, and does the removal builder capture the removed item's data directly rather than re-reading it from the now-mutated list by index? - Before hand-rolling
AnimatedBuilderfor a fade/slide/scale/rotation/size/align/position/decoration effect, was a built-in*Transitionwidget considered? - If
MediaQuery.disableAnimationsOf(context)is true, is durationDuration.zero(or the animation skipped)? - If using
AnimatedIcon, is the icon one ofAnimatedIcons.*— not an arbitraryIconData? - If using
AnimatedCrossFade, are the two children similar in size, or isalignment/layoutBuilderset so the layout does not jump? - Was a new animation package (
flutter_animate, Lottie, Rive,animations) added without the user asking?
If any of these are missing, fix the code immediately before presenting it to the user.
Constraints
- SDK first: Prefer Flutter framework widgets. Do not add
flutter_animate, Lottie, Rive, or theanimationspackage unless the user asked for them or the tree above cannot express the effect. - Reduced motion: When
MediaQuery.disableAnimationsOf(context)is true, useDuration.zeroor skip decorative motion. - Strict Disposal: You MUST include
dispose()methods for allAnimationControllerinstances to prevent memory leaks.TweenAnimationBuildermanages its own controller internally and does not need manual disposal. - No URLs: Do not include external links or URLs in the output or comments.
- Immutability: Treat
TweenandCurveclasses as stateless and immutable. Do not attempt to mutate them after instantiation. - Performance: Always use
AnimatedBuilder/AnimatedWidget/*Transitionwidgets instead of callingsetState()inside a controller'saddListenerwhen building complex widget trees. Pass unchanging subtrees via thechildparameter for the same reason. - Hero Tags: Hero tags MUST be identical and unique per route transition. Do not use generic tags like
'image'if multiple heroes exist. - AnimatedSwitcher Keys: Every widget passed as
AnimatedSwitcher's child MUST have a uniqueKey, or Flutter cannot detect the switch and the transition will silently not play. - AnimatedIcon pairs:
AnimatedIcononly morphsAnimatedIcons.*pairs. UseAnimatedSwitcherfor arbitrary icons. - AnimatedCrossFade size: Differently sized children jump at the end of the fade unless
alignment/layoutBuilderis set. - RotationTransition Units:
RotationTransition.turnsis in fractions of a full turn (1.0= 360°), not radians — don't reuse a radian value meant forTransform.rotate. - Typed animations: Raw
_controlleris valid forAnimation<double>slots. Wrap it with the matchingTweenforOffset,Alignment,Decoration, andRelativeRect. - AnimatedList/Grid Ordering: Capture the item being removed before mutating the backing data list, since the removal animation must render a widget for an item that's already gone from the list by the time it plays.