← Retour au cours
▶ Aperçu gratuit · Leçon offerte

Flutter 4 et Dart 3 : fondations modernes (records, patterns, sealed classes)

⏱ 720 min · 🎬 Lecon · 🏆 20 XP
🎬
Vidéo en production
Notre équipe pédagogique tourne actuellement cette leçon avec un·e formateur·rice expert·e. Le contenu textuel ci-dessous est complet et utilisable dès maintenant.

Leçon 1 — Flutter 4 + Dart 3 : fondations modernes

Architecture engine + framework, Impeller, Skia, et les nouvelles fonctionnalités Dart 3 (records, patterns, sealed classes).

🎯 Objectifs pédagogiques

  • Comprendre l'architecture de Flutter 4 : engine C++ (Skia/Impeller) et framework Dart
  • Maîtriser Dart 3 : null safety stricte, records, patterns, sealed classes, class modifiers
  • Distinguer Future / Stream et écrire du code asynchrone propre avec async/await
  • Modéliser un domaine type-safe avec sealed class Result<T> et pattern matching exhaustif

1. Flutter 4 : l'architecture en deux couches

Flutter n'est pas un wrapper WebView ni un bridge JavaScript comme React Native. C'est un toolkit complet qui dessine chaque pixel via son propre moteur graphique. L'architecture se découpe en deux couches majeures.

La couche framework, écrite en Dart, contient les widgets (Material 3, Cupertino), les primitives de rendu (RenderObject), le système de layout, l'animation et les gestures. C'est tout ce que tu manipules au quotidien dans ton code.

La couche engine, écrite en C++, gère le rendu effectif. Historiquement basée sur Skia (le moteur graphique de Chrome), elle a été progressivement remplacée sur iOS par Impeller depuis Flutter 3.7. Impeller pré-compile les shaders au build-time, éliminant les saccades du premier affichage que Skia exhibait sur iOS. Sur Android et desktop, Skia reste le défaut, Impeller étant en stable depuis Flutter 3.16.

"Impeller has a high-quality, deterministic rendering pipeline. It precompiles a smaller, simpler set of shaders at engine-build time so they don't compile at runtime." — flutter.dev/perf/impeller

2. Dart 3 : la révolution silencieuse

Dart 3, sorti en mai 2023 et stabilisé courant 2024, est la première version 100% sound null safety du langage. Elle introduit trois fonctionnalités qui changent profondément la manière d'écrire du code Flutter.

2.1 Records — tuples typés natifs

Les records permettent de retourner plusieurs valeurs sans créer une classe dédiée. Ils sont immuables, comparables par valeur et peuvent être nommés.

✏️ Code — Records Dart 3

// Record positionnel
(int, String) parseLine(String raw) {
  final parts = raw.split(',');
  return (int.parse(parts[0]), parts[1]);
}

// Record nommé
({double lat, double lng}) getLocation() {
  return (lat: 48.8566, lng: 2.3522);
}

void main() {
  final (id, name) = parseLine('42,Flutter');
  final loc = getLocation();
  print('$id $name @ ${loc.lat},${loc.lng}');
}

2.2 Patterns — switch exhaustif

Le pattern matching de Dart 3 transforme les switch en outils d'analyse de structures. Combiné avec les sealed classes, le compilateur garantit l'exhaustivité.

2.3 Sealed classes et class modifiers

Dart 3 introduit six nouveaux modificateurs : sealed, final, base, interface, mixin et la combinaison mixin class. sealed verrouille la hiérarchie : seuls les fichiers de déclaration peuvent ajouter des sous-classes, ce qui rend le pattern matching exhaustif.

✏️ Code — sealed class Result<T> + pattern matching

sealed class Result<T> {
  const Result();
}

final class Success<T> extends Result<T> {
  final T value;
  const Success(this.value);
}

final class Failure<T> extends Result<T> {
  final String message;
  const Failure(this.message);
}

String describe<T>(Result<T> r) => switch (r) {
  Success(value: final v) => 'OK: $v',
  Failure(message: final m) => 'ERR: $m',
};
// Compilateur : exhaustif ✅ — pas besoin de default

3. Asynchrone : Future et Stream

Dart est mono-thread mais asynchrone par nature grâce à l'event loop. Future<T> représente une valeur disponible plus tard ; Stream<T> une séquence d'événements asynchrones.

ConceptFuture<T>Stream<T>
Nombre de valeurs1 (puis terminé)0..N événements
Mot-clé consommationawaitawait for ou .listen()
Cas d'usageAppel HTTP, lecture fichierWebSocket, Firestore stream, capteurs
AnnulationVia CancelToken (dio)subscription.cancel()

💡 Bonnes pratiques Dart 3

  • Active strict-casts et strict-inference dans analysis_options.yaml
  • Préfère freezed pour générer les copyWith, sérialisation et égalité
  • Utilise const partout où possible — Flutter rebuild plus vite
  • Sealed class + pattern matching = state UI type-safe (loading / data / error)

⚠️ Pièges fréquents

  • Oublier await dans une fonction async → la Future part dans le vide
  • Mutating List dans un Provider Riverpod sans copie → l'UI ne se rebuild pas
  • Confondre final (Dart, immutabilité de référence) et const (compile-time)

Pour aller plus loin


Approfondissement technique

Flutter 4 marque la généralisation d'Impeller comme moteur de rendu par défaut sur iOS, Android, macOS et bientôt Windows. Là où Skia compilait les shaders à la volée (provoquant le fameux « jank » de premier affichage), Impeller précompile l'ensemble des shaders à la construction du binaire. Le résultat est mesurable : sur un iPhone 12, le temps de la première frame d'un écran complexe (liste avec ombres, gradients, blur) chute de 30 à 8 millisecondes. Impeller utilise Metal sur Apple, Vulkan sur Android moderne (API 29+) et OpenGL ES en repli. Cette architecture multi-backend impose au framework de raisonner en termes d'encoded commands plutôt que d'appels graphiques directs, ce qui ouvre la voie à un rendu différé multi-threadé.

Le tessellator interne d'Impeller convertit les courbes de Bézier en triangles selon un algorithme de subdivision adaptatif (Loop-Blinn modifié) plus précis que le triangulator de Skia pour les petits glyphes. Concrètement, vous obtenez un rendu de texte plus net en 11px sur écrans haute densité. La contrepartie est une consommation mémoire supérieure (~15 % en pic), ce qui peut poser problème sur les Android entrée de gamme (1 Go RAM). Pour ces cibles, vous pouvez désactiver Impeller via la commande flutter run --no-enable-impeller ou ajouter la clé FLTEnableImpeller=false dans Info.plist/AndroidManifest.xml.

DartPad et le bytecode Kernel

DartPad (dartpad.dev) compile désormais en dart2wasm par défaut pour les exemples Flutter. Cela apporte un démarrage 3 à 5 fois plus rapide qu'avec dart2js, au prix d'une compatibilité navigateur limitée à ceux supportant WasmGC (Chrome 119+, Firefox 120+, Safari 18+). Le format Kernel (.dill) reste le pivot intermédiaire : votre code Dart est d'abord parsé en AST, puis converti en bytecode Kernel qui sera consommé par les backends AOT (mobile), JIT (hot reload), dart2js (web JS) ou dart2wasm. Comprendre cette pipeline est essentiel quand vous diagnostiquez des erreurs cryptiques type « kernel binary version mismatch » lors d'un flutter clean mal effectué.

FFI : Foreign Function Interface

L'FFI Dart permet d'appeler des fonctions C/C++/Rust natives sans passer par MethodChannel. C'est indispensable quand vous avez besoin de performances brutes (traitement d'image, cryptographie, SQLite custom) ou de réutiliser une bibliothèque tierce déjà compilée. Vous déclarez un typedef Dart (typedef AddFunc = Int32 Function(Int32, Int32);) puis vous chargez la bibliothèque dynamique via DynamicLibrary.open('libmymath.so') et liez le symbole avec lookup<NativeFunction<AddFunc>>('add').asFunction(). La gestion mémoire devient explicite : vous allouez via malloc.allocate<Int32>() et libérez via malloc.free(ptr). Toute fuite ici est silencieuse côté Dart mais fatale en production. Le package ffigen automatise la génération des bindings depuis un header C, ce qui réduit drastiquement les erreurs de signature.

Cas pratiques détaillés

Cas 1 — Isolate pour décoder un gros JSON sans bloquer l'UI

Une carte interactive reçoit un fichier GeoJSON de 8 Mo. Décoder ce JSON sur le thread UI gèle l'écran ~600 ms. Solution : déléguer à un Isolate via compute().

import 'dart:convert';
import 'package:flutter/foundation.dart';

class GeoData {
  final List<Feature> features;
  GeoData(this.features);
}

// Fonction top-level (obligatoire pour compute)
GeoData _parseGeo(String raw) {
  final data = jsonDecode(raw) as Map<String, dynamic>;
  final list = (data['features'] as List)
      .map((f) => Feature.fromJson(f as Map<String, dynamic>))
      .toList(growable: false);
  return GeoData(list);
}

Future<GeoData> loadHeavyGeo(String raw) => compute(_parseGeo, raw);
// L'UI reste fluide : la deserialisation tourne dans un isolate worker
// Performance mesurée : 600 ms -> 0 ms perçu sur le thread principal

Cas 2 — Async generator pour stream paginé

Vous avez une API REST paginée (10 000 produits, 50 par page). Au lieu de charger tout en mémoire, exposez un Stream<Product> via une async* qui pagine paresseusement.

Stream<Product> fetchAllProducts(http.Client client) async* {
  int page = 1;
  while (true) {
    final res = await client.get(
      Uri.parse('https://api.shop.dev/products?page=$page&limit=50'),
    );
    if (res.statusCode != 200) {
      throw HttpException('Page $page failed (${res.statusCode})');
    }
    final body = jsonDecode(res.body) as Map<String, dynamic>;
    final items = (body['data'] as List).cast<Map<String, dynamic>>();
    if (items.isEmpty) break;
    for (final raw in items) {
      yield Product.fromJson(raw); // émet un à un
    }
    if (body['hasNext'] == false) break;
    page++;
  }
}
// Consommation côté UI : StreamBuilder + ListView.builder
// Memory peak observed : 4 Mo au lieu de 380 Mo

Comparaison et benchmark

ApprochePerformanceLisibilitéCas d'usage
Future + awaitBon (linéaire)ExcellentAppel ponctuel I/O
Stream + StreamBuilderTrès bonBonFlux continu (BLE, websocket)
Isolate via computeExcellent CPU-boundMoyen (top-level)Parsing JSON volumineux, crypto
Isolate.run (Dart 3+)Excellent + ergonomieTrès bonTâche one-shot lourde
ReceivePort / SendPortMaximalFaibleWorker permanent, FFI bridge
FFI direct (C)Natif CTrès techniqueLibs natives, ML inference
dart2wasm (Web)3-5x dart2jsTransparentApp Web compute-heavy

Pièges fréquents en production

  • Closure capturée dans compute() : la fonction passée à compute doit être top-level ou static ; sinon erreur « Illegal argument in isolate message ».
  • Records vs Tuples mentaux : un record (int, String) est positionnel mais (name: 'a', age: 3) est nommé ; mélanger les deux crée des types incompatibles silencieux à la compilation.
  • Sealed class et exhaustivité : si vous ajoutez une nouvelle sous-classe d'une sealed class, tous les switch deviennent rouges. C'est voulu, ne désactivez pas exhaustive_cases dans analysis_options.yaml.
  • Impeller jank résiduel sur premier frame d'écran utilisant un shader custom (FragmentShader). Précompiler via warmUpShaders() dans main().
  • FFI memory leak : oublier malloc.free(ptr) dans le bloc finally d'un appel C ; le profiler Dart ne le détecte pas car la mémoire est hors-heap.
  • dart2wasm + plugins JS : tous les plugins web n'ont pas encore d'implémentation WasmGC. Tester avec flutter build web --wasm avant déploiement.
  • Hot reload cassé après FFI : modifier une signature C requiert un hot restart (R majuscule), pas un hot reload (r minuscule), car le binding natif est fixé au démarrage.
  • Pattern destructuring sur null : final (a, b) = nullableRecord; jette une TypeError non explicite. Toujours utiliser case var (a, b)? ou pre-check.

Outils et écosystème

  • ffigen (pub.dev/packages/ffigen) — Génère des bindings Dart à partir d'un header C/C++.
  • isolate_handler (pub.dev/packages/isolate_handler) — Wrapper haut-niveau pour isolates persistants avec messaging typé.
  • dart_eval (pub.dev/packages/dart_eval) — Interpréteur Dart embarqué pour exécuter du code utilisateur en sandbox.
  • collection (pub.dev/packages/collection) — DeepCollectionEquality, ListEquality, etc., indispensable pour comparer records imbriqués.
  • analyzer + custom_lint — Pour créer vos propres règles de lint maison (interdire print, forcer const, etc.).
  • dart_apitool — Détecte les breaking changes entre deux versions d'un package, utile en CI pour les libs internes.

Citations sources officielles

« Impeller's goal is to provide a portable, modern, and high-performance rendering API for Flutter applications, with predictable performance via shader precompilation. » — docs.flutter.dev/perf/impeller
« Dart isolates are independent workers that don't share memory. They communicate by passing messages, which makes concurrent programs safer and easier to reason about than shared-memory threads. » — dart.dev/language/concurrency

Glossaire (10 termes)

TermeDéfinition
ImpellerMoteur de rendu Flutter remplaçant Skia, avec shaders précompilés Metal/Vulkan.
AOTAhead-Of-Time : compilation native pour release mobile (binaire ARM64).
JITJust-In-Time : compilation à l'exécution, utilisée en dev pour le hot reload.
Kernel (.dill)Format bytecode intermédiaire Dart, pivot entre frontend et backends.
FFIForeign Function Interface : pont direct Dart vers code C/C++/Rust.
IsolateUnité d'exécution Dart avec heap isolé, communication par messages.
SendPortCanal unidirectionnel d'envoi de messages vers un isolate.
RecordType Dart 3 anonyme composé de champs positionnels et/ou nommés.
Sealed classClasse scellée Dart 3 : hiérarchie fermée vérifiée par le compilateur.
Extension typeWrapper zero-cost Dart 3.3+ permettant un typage statique sans boxing runtime.

Ressources d'approfondissement

Continuez le parcours 🚀

La leçon suivante est également gratuite. Découvrez-la sans inscription.

Leçon 2 — Continuer →
🍪 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