Motion makes apps feel polished and helps users follow changes, and Flutter makes it cheap to add.
Implicit animations are the easy path: widgets such as AnimatedContainer, AnimatedOpacity, AnimatedAlign, AnimatedPadding, AnimatedSwitcher and AnimatedCrossFade animate automatically whenever their target properties change. Give them a duration and optional curve, then just change values with setState. TweenAnimationBuilder does the same for any value.
Explicit animations give full control — looping, reversing, staggering, driving by a gesture:
- An `AnimationController` produces values from 0.0 to 1.0 over a
duration, ticking once per frame. It needs avsync: mixSingleTickerProviderStateMixin(orTickerProviderStateMixinfor several controllers) into your State and passthis. Create it ininitState, dispose it indispose. - Control it with
forward(),reverse(),repeat(),animateTo(),stop(). - A `Tween` maps 0–1 to a range:
Tween(begin: 0.0, end: 200.0).animate(CurvedAnimation(parent: controller, curve: Curves.easeOut)). - Rebuild with `AnimatedBuilder` (or transition widgets like
FadeTransition,SlideTransition,ScaleTransition) so only the animated subtree rebuilds each frame.
Hero animates a shared element between routes: wrap both widgets in Hero(tag: sameTag, child: ...).
Prefer transitions that animate opacity and transforms; they're cheap to render. Respect MediaQuery.disableAnimationsOf(context) for users who reduce motion.
Example
import 'package:flutter/material.dart';
class Pulse extends StatefulWidget {
const Pulse({super.key});
@override
State<Pulse> createState() => _PulseState();
}
class _PulseState extends State<Pulse> with SingleTickerProviderStateMixin {
late final _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 800))
..repeat(reverse: true);
late final _scale = Tween(begin: 0.8, end: 1.2)
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
bool _expanded = false;
@override
void dispose() { _controller.dispose(); super.dispose(); }
@override
Widget build(BuildContext context) => Column(children: [
ScaleTransition(scale: _scale, child: const Icon(Icons.favorite, size: 48)), // explicit
GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: AnimatedContainer( // implicit
duration: const Duration(milliseconds: 300), curve: Curves.easeOut,
width: _expanded ? 200 : 100, height: 50, color: _expanded ? Colors.teal : Colors.grey),
),
]);
}Remember
- Implicit
Animated*widgets animate when their properties change - AnimationController + Tween + a transition/AnimatedBuilder for full control
- Controllers need a TickerProvider mixin and must be disposed
Watch out for
- Calling setState in an animation listener to rebuild the whole screen every frame instead of using AnimatedBuilder or a transition.