← Back to course
▶ Free preview · Lesson offered

Widgets Flutter : Material 3, Cupertino, Slivers et CustomPainter

⏱ 720 min · 🎬 Lesson · 🏆 20 XP
🎬
Video in production
Our educational team is currently filming this lesson with an expert instructor. The text content below is complete and ready to use right now.

Leçon 2 — Widgets Flutter : Material 3, Cupertino, Slivers, Custom Paint

Composer des UI riches avec Stateless/Stateful, layouts, scroll avancé et rendu custom.

🎯 Objectifs pédagogiques

  • Distinguer Stateless vs Stateful et maîtriser le cycle de vie complet
  • Connaître les widgets Material 3 et leurs équivalents Cupertino
  • Composer des layouts avec Row, Column, Stack, Flex, Expanded, Wrap, ConstraintBox
  • Construire des scrolls performants avec Slivers (SliverAppBar, SliverList)
  • Créer un widget custom via RenderObject ou CustomPainter

1. Stateless vs Stateful : la dualité fondatrice

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èreStatelessWidgetStatefulWidget
MutabilitéImmutable (final)State mutable via setState
Méthodes clésbuild(context)initState, build, dispose
CoûtLégerPlus lourd (State object)
Cas d'usageAffichage pur, props inAnimation, formulaire, timer
RebuildQuand props changentsetState ou parent

1.1 Cycle de vie d'un StatefulWidget

Sept callbacks ponctuent la vie d'un State<T> :

  1. createState() — appelée une fois lors de l'insertion dans l'arbre
  2. initState() — initialise abonnements, controllers, animations (appel à super.initState() obligatoire)
  3. didChangeDependencies() — quand un InheritedWidget parent change
  4. build(BuildContext) — appelée à chaque rebuild
  5. didUpdateWidget() — quand le parent reconstruit avec nouvelle config
  6. deactivate() — retirée temporairement de l'arbre
  7. dispose() — destruction définitive : libérer controllers, streams, timers

2. Material 3 — design system Google

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.

3. Cupertino — design system iOS natif

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.

4. Layout — l'ADN du positionnement Flutter

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.

  • Row / Column — alignement linéaire horizontal/vertical avec mainAxisAlignment et crossAxisAlignment
  • Expanded / Flexible — partage l'espace restant selon flex
  • Stack — superposition Z avec Positioned
  • Wrap — passage à la ligne automatique (chips, tags)
  • ConstrainedBox / SizedBox — forçage de contraintes
  • LayoutBuilder — réactif à la taille parent (responsive)

5. Slivers — le scroll avancé

Quand 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.

✏️ Code — Scaffold + ListView.builder performant

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),
      ),
    );
  }
}

6. Custom Paint et RenderObject

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.

💡 Conseils pratiques

  • Toujours const les widgets qui ne dépendent pas du parent — gain rebuild majeur
  • ListView.builder est lazy ; ListView() avec children construit tout en mémoire
  • Pour les listes infinies, ajoute un ScrollController + NotificationListener
  • Utilise Flutter DevTools > Widget Inspector pour debug l'arbre

⚠️ Erreurs courantes

  • Oublier controller.dispose() → fuites mémoire
  • Mettre tout l'écran dans un setState au lieu de découper en sous-widgets
  • Utiliser Container partout au lieu de widgets spécialisés (Padding, Center, SizedBox)

Pour aller plus loin


Approfondissement technique

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.

CustomPaint et Canvas low-level

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().

Slivers et SliverPersistentHeader

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.

Cas pratiques détaillés

Cas 1 — CustomPainter pour jauge circulaire animée

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))

Cas 2 — Hero animation cross-route avec flightShuttleBuilder

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)),
);

Comparaison et benchmark

ApprochePerformanceLisibilitéCas d'usage
ListView.builderExcellent (lazy)ExcellentListe simple 100+ items
CustomScrollView + SliversTrès bonMoyenHeader collapsible + sections mixtes
Stack + PositionedBonBonOverlay, badge, FAB ancré
CustomPaintMaximal contrôleFaibleGraphique, signature, jauge
Flow / CustomMultiChildLayoutTrès bonTrès techniqueLayout dynamique multi-passes
RepaintBoundaryOptimise FPSTransparentIsoler animation locale
HeroBonBonTransition partagée 2 routes

Pièges fréquents en production

  • Hero tag non unique dans une liste : si deux items ont la même tag, Flutter logge une erreur runtime et désactive l'animation.
  • shouldRepaint qui retourne toujours true : votre CustomPainter redessine à chaque build parent, même immobile (visible dans le devtools repaint rainbow).
  • Slivers mélangés à des widgets non-sliver dans un CustomScrollView : erreur silencieuse à la compilation, crash à l'exécution.
  • GestureDetector enveloppant un widget transparent : sans behavior: HitTestBehavior.opaque, le tap traverse vers le widget en dessous.
  • Semantics absentes sur boutons custom (CustomPaint) : VoiceOver/TalkBack lit « image », rejet App Store/Play Store possible pour accessibilité.
  • SliverPersistentHeader sans shouldRebuild correct : reconstruit en boucle, gel UI à 5 fps.
  • InheritedWidget recréé à chaque build du parent : tous les widgets dépendants se rebuild ; toujours stabiliser l'instance.
  • Hero pendant un PageRouteBuilder custom sans transitionDuration aligné : le vol Hero finit avant la transition de route, effet de saut.

Outils et écosystème

Citations sources officielles

« 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

Glossaire (10 termes)

TermeDéfinition
WidgetDescription immuable d'une portion d'UI ; reconstruite à chaque build.
ElementInstance mutable du tree associant Widget et RenderObject, gère le cycle de vie.
RenderObjectObjet bas-niveau effectuant layout, paint et hit testing.
BuildContextHandle sur la position d'un Element dans le tree, sert pour InheritedWidget et navigation.
SliverWidget scrollable composable, brique de CustomScrollView.
CustomPainterClasse permettant de dessiner directement sur le Canvas Skia/Impeller.
RepaintBoundaryWidget qui isole une zone en cache GPU, optimise les repaints partiels.
HeroWidget orchestrant une animation partagée entre deux routes via une tag commune.
SemanticsMétadonnées d'accessibilité lues par VoiceOver, TalkBack et tests a11y.
GestureDetectorWidget interceptant tap, double-tap, drag, scale, long-press via la gesture arena.

Ressources d'approfondissement

Continue the journey 🚀

Sign up to access the 5 other lessons + the final quiz.

Create my account
🍪 Nous utilisons des cookies essentiels et, avec ton accord, des cookies analytiques. En savoir plus

⚙️ Préférences cookies

Choisis quels cookies tu acceptes — modifiable à tout moment.

🔐 Essentiels (obligatoires)Authentification, session, sécurité. Toujours actifs.
📊 Analytics anonymesMesure d'audience anonymisée — aucune donnée personnelle.
📣 MarketingPublicités ITAG pertinentes sur d'autres sites.
💬 Contactez-nous sur WhatsApp