Animations
Meilleures pratiques pour les animations Flutter en utilisant le framework d'animation intégré et les directives de mouvement Material 3. Aucune bibliothèque d'animation tierce (Lottie, Rive, etc.).
Standards Fondamentaux
Appliquez ces standards à TOUS les travaux d'animation :
- Clarifiez l'intention visuelle quand la demande est ambiguë — quand le développeur dit « ajoute une animation » ou « rends-le plus fluide » sans spécifier la propriété, le déclencheur, la durée ou la courbe, demandez confirmation avant d'écrire du code. Si le développeur fournit des spécifications claires (par ex. « fondu de 300 ms ease-in sur la carte à son apparition »), procédez directement
- Utilisez l'approche d'animation la plus simple qui fonctionne — suivez l'arbre de décision ci-dessous ; n'utilisez jamais
AnimationControllerquand une animation implicite suffira - Utilisez les jetons de mouvement Material 3 pour la durée et l'easing — ne codez jamais en dur des valeurs
DurationouCurvearbitraires - Extractez les constantes d'animation — les durées, courbes et décalages vont dans des constantes nommées ou une classe
AppMotioncentralisée, pas en ligne - Disposez les contrôleurs — chaque
AnimationControllerdoit être disposé dans la méthodedispose()duState - Utilisez
SingleTickerProviderStateMixinpour un contrôleur — utilisezTickerProviderStateMixinseulement si le widget possède plusieurs contrôleurs - Gardez les sous-arbres animés petits — enrobez uniquement les widgets qui changent dans le constructeur d'animation, pas des arbres de widgets entiers
- N'animez jamais les propriétés déclenchant la mise en page dans une boucle serrée — animer
width/heightsur les mises en page complexes cause des reconstructions coûteuses ; préférezTransformouOpacityqui opèrent sur la couche de composition
Arbre de Décision pour les Animations
Choisissez l'approche la plus simple qui répond au besoin :
Le widget se reconstruit-il quand la valeur change ?
|
OUI --> Le framework fournit-il un widget AnimatedFoo ?
| |
| OUI --> Utilisez le widget implicite AnimatedFoo
| | (AnimatedContainer, AnimatedOpacity, AnimatedAlign, etc.)
| |
| NON --> Utilisez TweenAnimationBuilder
|
NON --> Avez-vous besoin d'un contrôle fin ?
(répéter, inverser, séquencer, écouter le statut)
|
OUI --> Utilisez AnimationController + AnimatedBuilder
|
NON --> Utilisez TweenAnimationBuilder
Règle générale : si l'animation est « définir une cible et la laisser s'animer jusqu'à là », utilisez l'implicite. Si l'animation doit jouer/mettre en pause/inverser/répéter sur commande, utilisez l'explicite.
Jetons de Mouvement Material 3
Utilisez les classes Durations et Easing intégrées de Flutter — ne codez jamais en dur Duration(milliseconds: ...) ou utilisez Curves.* pour le nouveau code. Les constantes du framework s'alignent sur la spécification de mouvement Material 3 ; consultez la documentation des classes Durations et Easing de Flutter pour la liste complète des jetons.
Constantes de Mouvement Centralisées
Introduisez une classe AppMotion quand le projet utilise des animations sur plusieurs fonctionnalités. Pour une seule animation dans l'app, les jetons M3 en ligne suffisent.
abstract class AppMotion {
// Transitions standard
static const Duration standardDuration = Durations.medium2;
static const Curve standardCurve = Easing.standard;
// Transitions de page
static const Duration pageDuration = Durations.medium4;
static const Curve pageEnterCurve = Easing.emphasizedDecelerate;
static const Curve pageExitCurve = Easing.emphasizedAccelerate;
// Fondus
static const Duration fadeDuration = Durations.short3;
static const Curve fadeCurve = Easing.standard;
}
Animations Implicites
Utilisez les animations implicites quand le widget se reconstruit avec de nouvelles valeurs cibles. Le framework interpole automatiquement. Flutter fournit des widgets AnimatedFoo intégrés (AnimatedContainer, AnimatedOpacity, AnimatedSlide, AnimatedSwitcher, etc.) — utilisez celui qui correspond à la propriété animée. Quand aucun widget intégré n'existe, utilisez TweenAnimationBuilder.
TweenAnimationBuilder
Utilisez TweenAnimationBuilder quand aucun widget AnimatedFoo intégré n'existe pour votre propriété, mais que vous voulez toujours une animation de style implicite « définir et oublier ».
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: isActive ? 1.0 : 0.0),
duration: Durations.medium2,
curve: Easing.standard,
builder: (context, value, child) {
return Transform.scale(
scale: 0.8 + (0.2 * value),
child: Opacity(
opacity: value,
child: child,
),
);
},
child: child, // child n'est pas reconstruit — optimisation
)
Le paramètre child est critique : passez les widgets qui ne dépendent pas de la valeur animée pour éviter les reconstructions inutiles.
Animations Explicites
Utilisez les animations explicites quand vous avez besoin de contrôler la lecture : jouer, mettre en pause, inverser, répéter ou écouter le statut d'animation.
Configuration de AnimationController
class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Durations.medium2,
vsync: this,
);
_fadeAnimation = CurvedAnimation(
parent: _controller,
curve: Easing.standard,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: child,
);
},
child: child, // enfant statique — pas reconstruit chaque frame
);
}
}
Voir references/explicit-animations.md pour les motifs didUpdateWidget, l'injection de contrôleur pour des tests faciles, et les directives des widgets de transition vs AnimatedBuilder.
Animations Chaînées avec Intervalles
Utilisez Interval dans CurvedAnimation pour séquencer des animations sur un seul contrôleur :
late final Animation<double> _fadeAnimation = CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.5, curve: Easing.standard),
);
late final Animation<Offset> _slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.25),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.2, 0.8, curve: Easing.emphasized),
),
);
Voir references/staggered-animations.md pour des exemples complets d'animations d'entrée décalées et d'éléments de liste décalés. Voir references/looping-animations.md pour les motifs d'animations répétées et de pulsation.
Transitions de Page
Les transitions de page personnalisées s'intègrent avec GoRouter via CustomTransitionPage dans GoRouteData.buildPage.
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return CustomTransitionPage(
key: state.pageKey,
child: const DetailsPage(),
transitionDuration: Durations.medium4,
reverseTransitionDuration: Durations.medium4,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: CurvedAnimation(
parent: animation,
curve: Easing.emphasizedDecelerate,
),
child: child,
);
},
);
}
Voir references/page-transitions.md pour une classe d'aide AppPageTransitions réutilisable avec des transitions fondu, fondu-glissement et glissement-montée, et utilisation avec GoRouteData.
Animations Hero
Utilisez Hero pour les transitions d'éléments partagés entre les routes. Le framework gère l'animation automatiquement.
// Écran source
Hero(
tag: 'product-image-${product.id}',
child: Image.network(product.imageUrl),
)
// Écran destination
Hero(
tag: 'product-image-${product.id}',
child: Image.network(product.imageUrl),
)
Règles pour Hero :
- Les étiquettes doivent être uniques dans chaque route — utilisez des identifiants significatifs, pas des indices
- La source et la destination doivent être visibles pendant la transition — Hero ne fonctionne pas avec les listes lazy qui suppriment le widget source
- Enrobez uniquement l'élément visuel — pas la carte ou tuile entière
Performance
À Faire
- Animez
TransformetOpacity— ceux-ci opèrent sur la couche de composition et ignorent la mise en page/peinture - Utilisez le paramètre
childdansAnimatedBuilderetTweenAnimationBuilderpour éviter de reconstruire les widgets statiques chaque frame - Utilisez
RepaintBoundaryautour des widgets animés dans les mises en page complexes pour isoler les repeints
À Éviter
- N'animez pas
width,heightoupaddingsur les mises en page complexes — déclenche des recalculs de mise en page coûteux chaque frame - N'enrobez pas des écrans entiers dans
AnimatedBuilder— enrobez uniquement le sous-arbre qui change - Ne créez pas plusieurs instances de
AnimationControllerpour des animations partageant la même temporisation — utilisezIntervalsur un seul contrôleur
Anti-Motifs
Valeurs magiques codées en dur
// Mauvais — valeurs arbitraires sans sens sémantique
AnimatedContainer(
duration: Duration(milliseconds: 375),
curve: Curves.easeInOutCubic,
// ...
)
// Bon — jetons M3 avec intention claire
AnimatedContainer(
duration: Durations.medium2,
curve: Easing.standard,
// ...
)
Disposition de contrôleur manquante
// Mauvais — fuite mémoire
@override
void dispose() {
super.dispose();
}
// Bon — disposez avant super.dispose()
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Reconstruction des enfants statiques chaque frame
// Mauvais — sous-arbre entier reconstruit 60 fois/seconde
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Opacity(
opacity: _controller.value,
child: const ExpensiveWidget(), // reconstruit chaque frame
);
},
)
// Bon — enfant statique passé en paramètre
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Opacity(
opacity: _controller.value,
child: child,
);
},
child: const ExpensiveWidget(), // construit une fois
)
Utiliser l'explicite quand l'implicite suffit
// Mauvais — complexité inutile pour une animation simple de valeur cible
class _FadeWidgetState extends State<FadeWidget>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
// ... 20+ lignes de boilerplate
// Bon — un widget, zéro boilerplate
AnimatedOpacity(
duration: Durations.short3,
curve: Easing.standard,
opacity: isVisible ? 1.0 : 0.0,
child: child,
)
Référence Rapide
| Approche | Quand l'utiliser |
|---|---|
AnimatedFoo |
Un widget intégré existe pour la propriété |
TweenAnimationBuilder |
Propriété personnalisée, aucun contrôle de lecture nécessaire |
AnimationController |
Besoin de jouer/mettre en pause/inverser/répéter/statut |
Hero |
Transition d'élément partagé entre routes |
CustomTransitionPage |
Transition de page GoRouter personnalisée |
| Mixin | Quand l'utiliser |
|---|---|
SingleTickerProviderStateMixin |
Le widget possède exactement un contrôleur |
TickerProviderStateMixin |
Le widget possède plusieurs contrôleurs |
Ressources Supplémentaires
- references/explicit-animations.md —
didUpdateWidget, contrôleurs testables, widgets de transition vsAnimatedBuilder - references/staggered-animations.md — animations d'entrée décalées et éléments de liste décalés
- references/page-transitions.md — aide
AppPageTransitionsréutilisable et intégration GoRouter - references/looping-animations.md — motifs d'animations répétées, pulsantes et rotations continues