MFormations
Modern Mobile Engineering

Chapitre 9

09 - Flutter

09 - Flutter

09 - Flutter : Cours complet

Table des matières

  1. Dart : fondations
  2. Futures, streams, isolates
  3. Widgets et arbres
  4. State management : Provider, Riverpod, Bloc, GetX
  5. Layout : Row, Column, Stack, Expanded, CustomPaint
  6. Navigation : Navigator 2.0, go_router
  7. Animations : implicites, explicites, Hero
  8. Material 3
  9. Performance : raster, shader compilation
  10. Tests de widgets
  11. Build : AOT et JIT
  12. Résumé et checklist

1. Dart : fondations

1.1 Pourquoi Dart ?

Dart est le langage de Flutter : compilé (AOT pour la prod, JIT pour le développement), orienté objet, avec un typage sound et un support natif de la programmation asynchrone (futures, streams, isolates).

// Typage fort, inférence
int compteur = 0;
final nom = "Flutter";       // const/immutable
var liste = <String>["a", "b"];

1.2 Types principaux

CatégorieTypes
Primitifsint, double, num, bool, String
CollectionsList, Set, Map, Iterable
Null-safetyint?, String?, late, opérateur !
Fonctionsfonctions, lambdas, typedef
Symboles avancésrecord, pattern, sealed class

1.3 Null safety (sound)

String? nom;
// nom.length   // erreur de compilation
if (nom != null) {
  print(nom.length);  // promotion de type
}
final longueur = nom?.length ?? 0;

Depuis Dart 3, tout est null-safe par défaut : le compilateur garantit qu'un type non-nullable ne peut pas contenir null.

1.4 Les sealed class et le pattern matching (Dart 3)

sealed class Resultat<T> {}
class Succes<T> extends Resultat<T> { final T data; Succes(this.data); }
class Erreur<T> extends Resultat<T> { final String message; Erreur(this.message); }

String afficher(Resultat<int> r) => switch (r) {
      Succes(:final data) => "OK: $data",
      Erreur(:final message) => "KO: $message",
    };

Le switch exhaustif oblige à traiter tous les sous-types (pas de default nécessaire).

1.5 Extensions et mixins

extension NombreClair on String {
  String capitalize() =>
      this.isEmpty ? this : this[0].toUpperCase() + this.substring(1);
}

mixin Loggable {
  void log(String msg) => print("[LOG] $msg");
}

class Service with Loggable {
  void run() => log("run");
}
  • Extensions : ajoutent des méthodes à un type existant sans héritage.
  • Mixins : réutilisent du comportement partagé sans héritage multiple.

1.6 const vs final vs late

Mot-cléSignification
constValeur constante à la compilation (widgets immuables)
finalAssigné une seule fois, à l'exécution
late finalInitialisé paresseusement au premier accès

2. Futures, streams, isolates

2.1 Programmation asynchrone

Future<String> charger() async {
  await Future.delayed(const Duration(milliseconds: 300));
  return "données";
}

void main() async {
  final data = await charger();          // attente
  charger().then((d) => print(d));       // ou then
}

Les Future et Stream évitent de bloquer le thread d'interface (l'UI thread).

2.2 Streams

Stream<int> compteur() async* {
  for (var i = 0; i < 5; i++) {
    await Future.delayed(const Duration(seconds: 1));
    yield i;
  }
}

void main() async {
  await for (final v in compteur()) {
    print(v);  // 0,1,2,3,4
  }
}

Les streams peuvent être single-subscription (une seule écoute) ou broadcast (multi-écoutes).

2.3 Isolates : la mémoire partagée en plus

Dart s'exécute dans des isolates : chaque isolate a sa propre mémoire et ses propres threads (pas de thread partagé). La communication passe par des messages (ports).

Diagramme en cours de génération...
import 'dart:isolate';

Future<int> calculLourd() async {
  return await Isolate.run(() {
    var total = 0;
    for (var i = 0; i < 1_000_000; i++) {
      total += i;
    }
    return total;
  });
}
  • L'UI thread reste fluide car le calcul lourd tourne dans un isolate séparé.
  • compute() (Flutter) est une abstraction plus simple pour les tâches ponctuelles.

2.4 Pourquoi pas des threads classiques ?

Les threads classiques posent des problèmes de data races (verrous, interblocage). Les isolates éliminent ces risques par conception : pas de mémoire partagée, que de la copie/transmission de messages.


3. Widgets et arbres

3.1 Tout est widget

En Flutter, l'interface est un arbre de widgets immuables : un widget décrit une partie de l'UI ; quand l'état change, on reconstruit le widget avec de nouvelles valeurs.

void main() {
  runApp(const MyApp());
}

3.2 StatelessWidget vs StatefulWidget

TypeCaractéristique
StatelessWidgetImmutable, dépend uniquement de ses paramètres (pas d'état interne)
StatefulWidgetMaintient un état mutable dans un objet State distinct
class Compteur extends StatefulWidget {
  const Compteur({super.key});
  @override
  State<Compteur> createState() => _CompteurState();
}

class _CompteurState extends State<Compteur> {
  int _n = 0;

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () => setState(() => _n++),
      child: Text("$_n"),
    );
  }
}

setState marque le widget comme dirty et déclenche un build à la prochaine frame.

3.3 Les trois arbres

Flutter maintient trois structures synchronisées :

Diagramme en cours de génération...
  • Widget tree : configuration immuable, reconstruite à chaque build.
  • Element tree : instances qui portent l'état et le lifecycle ; réconcilié à chaque frame.
  • Render tree : objets RenderObject qui gèrent layout, painting et hit-testing.

3.4 BuildContext

BuildContext est la position d'un widget dans l'Element tree. Il permet :

  • de remonter dans l'arbre (Theme.of(context), Navigator.of(context)) ;
  • d'écouter un InheritedWidget (c'est le mécanisme du state management) ;
  • de localiser des ancêtres de types connus.
final theme = Theme.of(context);          // remontée dans l'arbre
final size = MediaQuery.sizeOf(context); // taille de l'écran

3.5 const et performance

Déclarer les widgets const permet à Flutter de réutiliser l'instance (même widget = même identité) et de réduire le travail de réconciliation :

const Text("Titre", style: TextStyle(fontWeight: FontWeight.bold));

4. State management

4.1 Problème et principe

L'état vit dans des objets Dart hors UI. Les widgets en ont besoin → on remonte l'état dans un endroit partagé, puis on abonne les widgets. La solution de référence reste le couple ChangeNotifier / InheritedWidget (mais on passe par des bibliothèques).

Diagramme en cours de génération...

4.2 Provider

Basé sur InheritedWidget, le plus simple pour commencer.

ChangeNotifierProvider(
  create: (_) => PanierModel(),
  child: const MyApp(),
);

// lecture
final panier = context.watch<PanierModel>();
final panier = context.read<PanierModel>(); // sans abonnement

4.3 Riverpod

Successeur de Provider, compiler-safe, sans BuildContext obligatoire pour les providers.

final compteurProvider = NotifierProvider<Compteur, int>(Compteur.new);

class Compteur extends Notifier<int> {
  @override
  int build() => 0;
  void incrementer() => state++;
}

// dans un widget
final c = ref.watch(compteurProvider);

4.4 Bloc (flutter_bloc)

Architecture événements → états, très stricte, idéale pour les gros projets.

sealed class CompteurEvent {}
class Incrementer extends CompteurEvent {}

class CompteurBloc extends Bloc<CompteurEvent, int> {
  CompteurBloc() : super(0) {
    on<Incrementer>((event, emit) => emit(state + 1));
  }
}
ForceFaiblesse
Prévisible, testable, traçable (chaque état est explicite)Verbose, boilerplate important

4.5 GetX

« Super-solution » tout-en-un : state, routing, DI, utilitaires. Simple et rapide, mais moins idiomatique et plus opiniâtre.

class Controleur extends GetxController {
  final count = 0.obs;
  void inc() => count++;
}

4.6 Comparaison

SolutionComplexitéType safetyBoilerplateRecommandation
setState0minimeétat local seulement
Providerfaiblecorrectefaiblepetit/moyen projet
Riverpodmoyenneexcellentemoyenprojet moderne, testable
Blocélevéeexcellenteimportantgrosse équipe, besoins stricts
GetXfaiblelimitéefaibleprototypes, équipes familières

5. Layout

5.1 Les widgets de base

WidgetRôle
Rowenfants en ligne horizontale
Columnenfants en ligne verticale
Stackenfants empilés (z-index)
Expanded / Flexiblerépartir l'espace libre (flex)
Containerbox décorée (padding, margin, color, box)
Align / Centerpositionnement
SizedBoxtaille fixe ou espace vide
Wrapretour à la ligne automatique
CustomPaintdessin vectoriel sur mesure
Row(
  children: [
    const Icon(Icons.star, color: Colors.amber),
    Expanded(
      child: Text("Un long titre qui s'adapte à la largeur disponible"),
    ),
    TextButton(onPressed: () {}, child: const Text("Voir")),
  ],
)

5.2 Contraintes et flux de layout

Flutter fait le layout en deux passes :

Diagramme en cours de génération...
  • Un enfant reçoit des contraintes (BoxConstraints min/max) ;
  • il choisit une taille dans ces bornes ;
  • le parent le positionne.

5.3 CustomPaint

Pour du dessin vectoriel personnalisé, on fournit un CustomPainter :

class CourbePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2;
    final path = Path()..moveTo(0, size.height / 2);
    path.cubicTo(size.width * .25, 0, size.width * .75, size.height, size.width, size.height / 2);
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

CustomPaint(size: const Size(200, 100), painter: CourbePainter())

6. Navigation

6.1 Navigator 1.0 (historique) vs Navigator 2.0

VersionModèleUsage
Navigator 1.0Pile impérative : Navigator.push/popSimple, route fixe
Navigator 2.0URL-like, RouterDelegate + RouteInformationParserDeep links, Web, état de route déclaratif

Navigator 2.0 est déclaratif : la route est un état que vous contrôlez, ce qui permet de partager les règles de navigation avec le navigateur Web.

6.2 go_router (recommandé)

go_router simplifie Navigator 2.0 : routes typées, redirections, shell, deep links.

final router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
      routes: [
        GoRoute(
          path: 'produit/:id',
          builder: (context, state) => ProductScreen(
            id: int.parse(state.pathParameters['id']!),
          ),
        ),
      ],
    ),
  ],
);

MaterialApp.router(routerConfig: router);
context.go('/produit/42');        // navigation impérative
context.push('/produit/42');      // push (retour possible)

6.3 Deep links et state restoration

  • Deep links : on déclare un scheme et/ou un domaine ; go_router matche les URLs.
  • State restoration : MaterialApp avec restorationScopeId conserve la pile de navigation après un kill du process.

7. Animations

7.1 Animations implicites

Le framework anime lui-même une valeur quand elle change (durée/curves fournies) :

AnimatedContainer(
  duration: const Duration(milliseconds: 300),
  width: _etendu ? 300 : 100,
  height: _etendu ? 300 : 100,
  color: _etendu ? Colors.blue : Colors.red,
)

Autres widgets : AnimatedOpacity, AnimatedPadding, AnimatedAlign, AnimatedSwitcher, AnimatedPositioned.

7.2 Animations explicites (AnimationController)

class _DemoState extends State<Demo> with SingleTickerProviderStateMixin {
  late final AnimationController _c = AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 500),
  );
  late final Animation<double> _scale =
      Tween<double>(begin: 0.5, end: 1.5).animate(_c);

  @override
  void dispose() {
    _c.dispose();
    super.dispose();
  }
  // _c.forward() / _c.reverse() / _c.repeat()
}
  • AnimationController pilote le temps (vsync pour la 60/120 fps) ;
  • Tween/CurveTween transforment 0..1 en valeur interpolée ;
  • with SingleTickerProviderStateMixin fournit le vsync.

7.3 Hero

Animation de partage d'élément entre deux routes (l'image « vole ») :

Hero(tag: 'produit-${p.id}', child: Image.network(p.image));
// même tag sur la route de destination

7.4 Transitions de route

PageRouteBuilder(
  pageBuilder: (_, __, ___) => const DetailScreen(),
  transitionsBuilder: (_, animation, __, child) =>
      FadeTransition(opacity: animation, child: child),
)

Avec go_router : pageBuilder custom ou CustomTransitionPage.


8. Material 3

8.1 Principes

Material 3 (M3, lancé 2021-2022) : thème dynamique, formes arrondies, surfaces tonales, dynamic color (adaptation aux fonds d'écran Android 12+).

8.2 Activer M3

MaterialApp(
  theme: ThemeData(
    colorSchemeSeed: Colors.deepPurple,
    useMaterial3: true,  // par défaut depuis Flutter 3.16
  ),
)

8.3 ColorScheme

M3 génère une palette tonale de 13 couleurs depuis une couleur de départ (seed). Le système adapte automatiquement composants et surfaces.

8.4 Composants M3

FilledButton, NavigationBar, Card, SnackBar, BottomSheet, Chip, SearchBar, Badge… tous suivent les specs M3.


9. Performance

9.1 Raster vs rasteriser — l'essentiel

Flutter rend lui-même son UI : la couche d'impression (Skia ou Impeller) convertit les widgets en triangles, pixels, puis envoie au GPU.

Diagramme en cours de génération...

9.2 Impeller

Impeller (iOS 2024+, Android par défaut en 2025) remplace Skia : précompilation des shaders, élimine le jank de compilation des shaders (petits freeze au premier rendu d'un effet).

9.3 Profiling

OutilUsage
Flutter DevTools → PerformanceFlame chart, frames janky
flutter run --profileprofile mode (AOT, sans debug)
Timelineévénements de frame, raster/paint
Shader jank (en mode profile)détecte les shaders non précompilés

9.4 Bonnes pratiques

  • Éviter le travail lourd dans build() ; utiliser RepaintBoundary pour isoler le repaint.
  • const sur les widgets ; éviter les rebuilds inutiles.
  • ListView.builder (paresseux) pour les longues listes.
  • Déplacer les calculs lourds dans un isolate (compute).
  • Vérifier les leaks : controllers et listeners à disposer dans dispose().

10. Tests de widgets

10.1 Types de tests

TypeVitessePortée
Unit testrapidelogique pure (bloc, repos)
Widget testmoyenneun widget + son interaction
Integration testlenteparcours complet sur device

10.2 Exemple de widget test

testWidgets('Incrémente le compteur', (tester) async {
  await tester.pumpWidget(const CompteurApp());

  expect(find.text('0'), findsOneWidget);
  await tester.tap(find.byType(ElevatedButton));
  await tester.pump();

  expect(find.text('1'), findsOneWidget);
});
  • tester.pump() avance d'une frame ;
  • find.text, find.byType, find.byIcon pour localiser ;
  • mockito/mocktail pour les dépendances ;
  • tester aussi les erreurs et les états vides.

11. Build : AOT et JIT

11.1 Deux modes de compilation Dart

ModeUsageCaractéristique
JIT (Just-In-Time)développementhot reload, hot restart, le code est compilé à l'exécution
AOT (Ahead-Of-Time)productioncode machine précompilé, démarrage rapide, pas d'outils dev
Diagramme en cours de génération...

11.2 Builds cibles

flutter build apk        # Android
flutter build appbundle  # Google Play (recommandé)
flutter build ios        # iOS (sur macOS)
flutter build web        # Web
flutter build windows    # Desktop Windows

11.3 La mécanique du build

  1. Dart → kernel (AST/sérialisé) via le front-end ;
  2. AOT : kernel → code machine natif (fini) pour la release ;
  3. Les assets, fonts et pubspec.yaml sont embarqués ;
  4. Le runtime Flutter (engine) est fourni par le framework.

12. Résumé et checklist

À retenir

  • Dart : null-safety, sealed class/pattern matching, futures/streams, isolates.
  • Les 3 arbres : widget (config) → element (état) → render (peinture).
  • State management : setState (local), Provider/Riverpod (moyen), Bloc (gros), GetX (rapide).
  • Layout en 2 passes : contraintes → taille → position.
  • go_router pour une navigation déclarative et des deep links.
  • Animations implicites (Animated*) vs explicites (AnimationController).
  • M3 : useMaterial3 par défaut, colorSchemeSeed, dynamic color.
  • Impeller supprime le jank de compilation des shaders.
  • JIT pour le dev (hot reload), AOT pour la prod.

Checklist du développeur Flutter

  • flutter analyze sans erreur
  • flutter test au vert (unit + widget tests)
  • const max sur les widgets
  • Listes paresseuses (ListView.builder)
  • Controllers disposés (dispose)
  • Build profile testé sur device réel
  • Taille du bundle vérifiée
  • Accessibilité : labels, contrastes, tailles de tap