Composer des UI riches avec Stateless/Stateful, layouts, scroll avancé et rendu custom.
Tout est widget en Flutter. La question centrale lors de la création d'un nouveau composant est : a-t-il besoin de garder un état mutable interne ? Si non, StatelessWidget ; si oui, StatefulWidget.
"A widget that does not require mutable state. Stateless widgets are useful when the part of the user interface you are describing does not depend on anything other than the configuration information in the object itself." — flutter.dev — StatelessWidget
| Critère | StatelessWidget | StatefulWidget |
|---|---|---|
| Mutabilité | Immutable (final) | State mutable via setState |
| Méthodes clés | build(context) | initState, build, dispose |
| Coût | Léger | Plus lourd (State object) |
| Cas d'usage | Affichage pur, props in | Animation, formulaire, timer |
| Rebuild | Quand props changent | setState ou parent |
Sept callbacks ponctuent la vie d'un State<T> :
super.initState() obligatoire)Activé par défaut depuis Flutter 3.16 (useMaterial3: true est désormais le défaut), Material 3 apporte le Dynamic Color (couleurs extraites du wallpaper Android 12+), des composants repensés (NavigationBar remplace BottomNavigationBar, Card avec elevation tonale) et un système typographique enrichi.
Widgets indispensables : MaterialApp (root), Scaffold (chassis), AppBar + SliverAppBar, FloatingActionButton, NavigationBar + NavigationDrawer, FilledButton, ElevatedButton, OutlinedButton, SegmentedButton, Card, Chip, DatePicker, SnackBar.
Le package cupertino reproduit l'UI iOS pixel-perfect. CupertinoApp, CupertinoNavigationBar, CupertinoTabScaffold, CupertinoButton, CupertinoSlider, CupertinoActionSheet. Utilisable conjointement avec Material via la détection Platform.isIOS ou un package comme flutter_platform_widgets.
Le système de layout repose sur une règle simple : constraints go down, sizes go up, parent sets position. Le parent passe des contraintes (min/max width/height), l'enfant choisit sa taille à l'intérieur, le parent le positionne.
mainAxisAlignment et crossAxisAlignmentflexPositionedQuand ListView ne suffit plus (header collapsible, mix de listes/grilles, parallax), on descend dans CustomScrollView avec des Slivers : SliverAppBar (collapsing avec FlexibleSpaceBar), SliverList, SliverGrid, SliverFillRemaining, SliverPersistentHeader.
class ProductListScreen extends StatelessWidget {
final List<Product> products;
const ProductListScreen({super.key, required this.products});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Catalogue')),
body: ListView.builder(
itemCount: products.length,
itemBuilder: (ctx, i) {
final p = products[i];
return ListTile(
leading: Image.network(p.imageUrl, width: 56),
title: Text(p.name),
subtitle: Text('\${p.price.toStringAsFixed(2)} EUR'),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.pushNamed(ctx, '/p/\${p.id}'),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.pushNamed(context, '/cart'),
child: const Icon(Icons.shopping_cart),
),
);
}
}
Pour des UI vraiment custom (graphiques, jeux, signature pad), CustomPainter donne accès à un Canvas Skia/Impeller : drawPath, drawCircle, drawImage, gradients, transformations. Pour aller plus loin, hériter de RenderObject permet de contrôler layout, paint et hit-testing.
const les widgets qui ne dépendent pas du parent — gain rebuild majeurListView.builder est lazy ; ListView() avec children construit tout en mémoireScrollController + NotificationListenercontroller.dispose() → fuites mémoiresetState au lieu de découper en sous-widgetsContainer partout au lieu de widgets spécialisés (Padding, Center, SizedBox)Le triptyque Flutter Widget / Element / RenderObject est au cœur de toute optimisation sérieuse. Un Widget est immuable et décrit une configuration. Un Element est le maillon mutable du tree qui maintient l'état et opère le diffing. Le RenderObject est l'objet bas niveau qui réalise le layout, le paint et le hit testing. Quand vous appelez setState, vous reconstruisez les Widgets, mais Flutter ne reconstruit pas forcément les Elements ni les RenderObjects : le framework compare les runtimeType et la key pour décider s'il faut détruire l'Element ou simplement le mettre à jour (update(newWidget)). Cette mécanique explique pourquoi un ListView.builder avec une ValueKey instable peut détruire et recréer mille widgets à chaque scroll, alors qu'une key stable permet le recyclage.
Le cycle de vie d'un RenderObject est précis : performResize calcule la taille selon les contraintes parentes, performLayout positionne les enfants, paint dessine sur le Canvas, hitTest détermine si un événement pointeur le concerne. Chaque RenderObject peut être repaint boundary (rasterisation isolée en cache GPU) ou non. Un RepaintBoundary mal placé alourdit la mémoire vidéo ; bien placé, il évite des repaints inutiles sur des animations limitées à une portion d'écran. La devtools timeline (« Rasterizer Thread ») vous montre où placer ces frontières.
Quand aucun widget existant ne produit le visuel voulu (jauge circulaire personnalisée, graphique radar, signature manuscrite), CustomPaint couplé à un CustomPainter donne accès direct au Canvas Skia/Impeller. Vous y dessinez via canvas.drawPath, canvas.drawCircle, canvas.drawShadow, et vous pouvez appliquer des BlendMode exotiques (multiply, colorBurn) impossibles via les widgets standards. Le contrat clé est shouldRepaint(oldDelegate) : retournez false tant que vos paramètres n'ont pas changé, sinon vous redessinez à chaque frame, même statique. Pour les chemins complexes, précalculez le Path dans initState et stockez-le, plutôt que de le reconstruire à chaque paint().
Un Sliver est un widget qui produit un morceau de zone scrollable. Les Slivers permettent des effets impossibles avec une simple ListView : header qui se contracte (SliverAppBar), section qui colle en haut (SliverPersistentHeader avec pinned: true), grille intercalée avec liste (SliverGrid puis SliverList). Un SliverPersistentHeaderDelegate personnalisé doit retourner ses minExtent, maxExtent et implémenter shouldRebuild. C'est par cette API que les écrans « profile » iOS-style (photo qui se réduit en scroll) sont construits proprement, sans hack de scroll listener.
On veut afficher un score 0-100 sous forme d'arc coloré (vert si > 70, orange entre 40 et 70, rouge sinon), avec animation de remplissage.
class GaugePainter extends CustomPainter {
final double value; // 0..1
GaugePainter(this.value);
@override
void paint(Canvas canvas, Size size) {
final center = size.center(Offset.zero);
final radius = size.shortestSide / 2 - 12;
final bg = Paint()
..color = const Color(0xFFE2E8F0)
..strokeWidth = 14
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
final fg = Paint()
..color = value > 0.7
? const Color(0xFF10B981)
: value > 0.4
? const Color(0xFFF59E0B)
: const Color(0xFFEF4444)
..strokeWidth = 14
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
canvas.drawCircle(center, radius, bg);
final rect = Rect.fromCircle(center: center, radius: radius);
canvas.drawArc(rect, -3.14 / 2, 6.28 * value, false, fg);
}
@override
bool shouldRepaint(GaugePainter old) => old.value != value;
}
// Usage : AnimatedBuilder + CustomPaint(painter: GaugePainter(animation.value))
Vous voulez qu'une miniature carrée devienne un cercle plein écran avec changement de couleur et bord radial pendant le vol Hero.
Hero(
tag: 'avatar-${user.id}',
flightShuttleBuilder: (ctx, anim, dir, fromCtx, toCtx) {
return AnimatedBuilder(
animation: anim,
builder: (_, __) {
final t = Curves.easeInOutCubic.transform(anim.value);
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Color.lerp(
const Color(0xFF6366F1),
const Color(0xFFEC4899),
t,
),
border: Border.all(
color: Colors.white,
width: 2 + 4 * t,
),
),
child: ClipOval(
child: Image.network(user.photo, fit: BoxFit.cover),
),
);
},
);
},
child: CircleAvatar(backgroundImage: NetworkImage(user.photo)),
);
| Approche | Performance | Lisibilité | Cas d'usage |
|---|---|---|---|
| ListView.builder | Excellent (lazy) | Excellent | Liste simple 100+ items |
| CustomScrollView + Slivers | Très bon | Moyen | Header collapsible + sections mixtes |
| Stack + Positioned | Bon | Bon | Overlay, badge, FAB ancré |
| CustomPaint | Maximal contrôle | Faible | Graphique, signature, jauge |
| Flow / CustomMultiChildLayout | Très bon | Très technique | Layout dynamique multi-passes |
| RepaintBoundary | Optimise FPS | Transparent | Isoler animation locale |
| Hero | Bon | Bon | Transition partagée 2 routes |
CustomScrollView : erreur silencieuse à la compilation, crash à l'exécution.behavior: HitTestBehavior.opaque, le tap traverse vers le widget en dessous.transitionDuration aligné : le vol Hero finit avant la transition de route, effet de saut.« Widgets describe what their view should look like given their current configuration and state. Whenever a widget's state changes, the widget rebuilds its description, which the framework diffs against the previous description in order to determine the minimal changes needed in the underlying render tree. » — docs.flutter.dev/resources/architectural-overview
« Slivers are portions of a scrollable area that you can define to behave in a special way. A CustomScrollView lets you mix slivers together to achieve custom scrolling effects. » — docs.flutter.dev/ui/advanced/slivers
| Terme | Définition |
|---|---|
| Widget | Description immuable d'une portion d'UI ; reconstruite à chaque build. |
| Element | Instance mutable du tree associant Widget et RenderObject, gère le cycle de vie. |
| RenderObject | Objet bas-niveau effectuant layout, paint et hit testing. |
| BuildContext | Handle sur la position d'un Element dans le tree, sert pour InheritedWidget et navigation. |
| Sliver | Widget scrollable composable, brique de CustomScrollView. |
| CustomPainter | Classe permettant de dessiner directement sur le Canvas Skia/Impeller. |
| RepaintBoundary | Widget qui isole une zone en cache GPU, optimise les repaints partiels. |
| Hero | Widget orchestrant une animation partagée entre deux routes via une tag commune. |
| Semantics | Métadonnées d'accessibilité lues par VoiceOver, TalkBack et tests a11y. |
| GestureDetector | Widget interceptant tap, double-tap, drag, scale, long-press via la gesture arena. |
Choisis quels cookies tu acceptes — modifiable à tout moment.