Chapitre 9
09 - Flutter
09 - Flutter
09 - Flutter : Cours complet
Table des matières
- Dart : fondations
- Futures, streams, isolates
- Widgets et arbres
- State management : Provider, Riverpod, Bloc, GetX
- Layout : Row, Column, Stack, Expanded, CustomPaint
- Navigation : Navigator 2.0, go_router
- Animations : implicites, explicites, Hero
- Material 3
- Performance : raster, shader compilation
- Tests de widgets
- Build : AOT et JIT
- 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égorie | Types |
|---|---|
| Primitifs | int, double, num, bool, String |
| Collections | List, Set, Map, Iterable |
| Null-safety | int?, String?, late, opérateur ! |
| Fonctions | fonctions, lambdas, typedef |
| Symboles avancés | record, 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 |
|---|---|
const | Valeur constante à la compilation (widgets immuables) |
final | Assigné une seule fois, à l'exécution |
late final | Initialisé 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
| Type | Caractéristique |
|---|---|
StatelessWidget | Immutable, dépend uniquement de ses paramètres (pas d'état interne) |
StatefulWidget | Maintient 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
RenderObjectqui 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));
}
}
| Force | Faiblesse |
|---|---|
| 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
| Solution | Complexité | Type safety | Boilerplate | Recommandation |
|---|---|---|---|---|
| setState | 0 | — | minime | état local seulement |
| Provider | faible | correcte | faible | petit/moyen projet |
| Riverpod | moyenne | excellente | moyen | projet moderne, testable |
| Bloc | élevée | excellente | important | grosse équipe, besoins stricts |
| GetX | faible | limitée | faible | prototypes, équipes familières |
5. Layout
5.1 Les widgets de base
| Widget | Rôle |
|---|---|
Row | enfants en ligne horizontale |
Column | enfants en ligne verticale |
Stack | enfants empilés (z-index) |
Expanded / Flexible | répartir l'espace libre (flex) |
Container | box décorée (padding, margin, color, box) |
Align / Center | positionnement |
SizedBox | taille fixe ou espace vide |
Wrap | retour à la ligne automatique |
CustomPaint | dessin 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
| Version | Modèle | Usage |
|---|---|---|
| Navigator 1.0 | Pile impérative : Navigator.push/pop | Simple, route fixe |
| Navigator 2.0 | URL-like, RouterDelegate + RouteInformationParser | Deep 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
schemeet/ou un domaine ; go_router matche les URLs. - State restoration :
MaterialAppavecrestorationScopeIdconserve 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()
}
AnimationControllerpilote le temps (vsync pour la 60/120 fps) ;Tween/CurveTweentransforment0..1en valeur interpolée ;with SingleTickerProviderStateMixinfournit 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
| Outil | Usage |
|---|---|
| Flutter DevTools → Performance | Flame chart, frames janky |
flutter run --profile | profile 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(); utiliserRepaintBoundarypour isoler le repaint. constsur 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
| Type | Vitesse | Portée |
|---|---|---|
| Unit test | rapide | logique pure (bloc, repos) |
| Widget test | moyenne | un widget + son interaction |
| Integration test | lente | parcours 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.byIconpour localiser ;mockito/mocktailpour les dépendances ;- tester aussi les erreurs et les états vides.
11. Build : AOT et JIT
11.1 Deux modes de compilation Dart
| Mode | Usage | Caractéristique |
|---|---|---|
| JIT (Just-In-Time) | développement | hot reload, hot restart, le code est compilé à l'exécution |
| AOT (Ahead-Of-Time) | production | code 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
- Dart → kernel (AST/sérialisé) via le front-end ;
- AOT : kernel → code machine natif (fini) pour la release ;
- Les assets, fonts et
pubspec.yamlsont embarqués ; - 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 :
useMaterial3par 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 analyzesans erreur -
flutter testau vert (unit + widget tests) -
constmax 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