Fiche de revision

Developpement
Mobile — Dart & Flutter

Synthese des slides du cours et des chapitres Dart et Flutter du syllabus, en 13 chapitres, de la comparaison des solutions mobiles jusqu'a Firebase et l'architecture BLoC.

Slides du cours Syllabus Dart Syllabus Flutter Fonctionnalites mobiles React Native

1. Panorama des solutions mobiles

1.1 Les 5 familles

Approche Techno Avantages Inconvénients
Natif Android : Kotlin/Java + Android Studio · iOS : Swift/Objective-C + Xcode Accès direct à toutes les API natives, perfs maximales, UX optimisée 2 bases de code séparées, coût élevé (2 équipes)
Cross-platform Flutter (Dart, Google), React Native (JS/TS, Meta), .NET MAUI (C#, Microsoft), Kotlin Multiplatform Mobile (KMM) Code partagé, perfs proches du natif, écosystèmes riches Dépendance au framework, certaines API natives via plugins
Hybride (Web + conteneur) Ionic + Capacitor (Angular/React/Vue), Cordova (déclinant) Rapide si on maîtrise le web, une base de code Perfs limitées, mal adapté aux apps lourdes/jeux, UI peu native
PWA Application web installable Pas besoin de store (mais possible via store Android), offline via cache, notifications push Pas 100 % d'accès au natif (capteurs, Bluetooth, NFC selon l'OS), push partiellement limitées sur iOS
No-code / low-code Glide, Adalo, Bubble, Appgyver, Thunkable Création très rapide sans coder, idéal MVP Limité pour apps complexes ou très personnalisées

1.2 Détails cross-platform (à connaître)

  • Flutter — une seule base de code, perfs proches du natif grâce à son moteur graphique, très populaire et mature en 2025, large écosystème de widgets. Idéal pour une belle UI et des animations fluides.
  • React Native — basé sur React, grosse communauté et beaucoup de librairies, utilisé par Instagram, Discord. Plus mature qu'avant mais parfois moins performant que Flutter. Créé par Meta, 1re version 2015. Traduit les composants JS en composants natifs.
  • .NET MAUI — successeur de Xamarin, partage de code avec les autres projets .NET, intéressant si on est déjà dans l'écosystème Microsoft.
  • KMM — partage la logique métier (accès API, traitements) mais UI séparée par plateforme. Plus flexible que Flutter/RN, mais plus complexe.

1.3 Tableau « laquelle choisir ? » (tombe facilement à l'examen)

Situation du projet Solution conseillée Pourquoi
Projet ambitieux, besoin de perfs, budget élevé Natif (Swift + Kotlin) Accès direct à toutes les API, perf maximale, UX optimisée
Projet polyvalent, efficace, moderne Flutter (ou React Native) Une seule base de code, bon écosystème, apps complètes et visuellement riches
Projet simple, orienté contenu PWA ou Ionic Développement rapide, déploiement facile, installation via navigateur
Prototype rapide, petit projet sans développeurs No-code / low-code Création rapide, idéal pour tester une idée ou un MVP

1.4 Ligne du temps (repères des slides)

2007 iPhone (Objective-C) · 2008 Android (Java), App Store & Google Play · 2011 PhoneGap/Cordova (hybride) · 2014 Swift, React Native interne · 2015 React Native open-source, Ionic · 2017 Flutter (alpha), PWA · 2018 Kotlin officiel Android, Flutter bêta · 2019 Flutter 1.0, annonce .NET MAUI · 2020 boom PWA, no-code plus mature · 2022 Flutter 3.0 stable, .NET MAUI stable · 2023 KMM progresse, PWA sur iOS · 2024 Flutter 3.20+, React Native modernisé · 2025 Flutter & React Native leaders, PWA adoptées, no-code courant.


2. Flutter : l'idée de départ

Le problème que Google voulait résoudre : écrire un code vraiment multiplateforme → mobile (Android, iOS), web, desktop (Windows, macOS, Unix).

La réponse en deux temps : 1. un nouveau langage : Dart ; 2. une librairie écrite en Dart : Flutter.

Pourquoi « vraiment natif » ? La plupart des solutions multiplateformes insèrent une machine virtuelle entre le programme et l'OS hôte (Java, Cordova…). Google voulait des applications réellement natives : vitesse d'exécution plus rapide et accès à tous les appels système de l'OS.

Compilation vs transpilation (tableau des slides) — Flutter transpile une partie du code vers les langages cibles :

OS Langages cibles
Android C++ ou Java ou Kotlin
iOS Swift ou Objective-C
Windows C#
macOS Swift ou Objective-C
Unix C++

Petite nuance : le syllabus décrit plutôt Flutter comme compilé en code machine avec son propre moteur de rendu qui « dessine chaque pixel » (d'où la cohérence visuelle parfaite entre plateformes). Pour l'examen, restitue le tableau des slides si la question porte dessus, mais garde en tête les deux formulations : pas de VM entre l'app et l'OS.

Repères Flutter : création mai 2017 · première release publique décembre 2018 · librairie écrite en Dart · grosse communauté · extensions sur https://pub.dev/ · hot reload.


3. Dart — fondamentaux

Langage typé statiquement, familier si on connaît C#/Java.

3.1 Projet et point d'entrée

dart create nomDuProjet     # projet Dart pur (pas Flutter)

Arborescence : .dart_tool/, bin/, lib/, test/, .gitignore, analysis_options.yaml, CHANGELOG.md, pubspec.lock, pubspec.yaml, README.md.

void main(List<String> arguments) {
  print('Hello world !');
}

3.2 Types et variables

Type Exemple
int 42, -7
double 3.14
String 'Hello', "Dart"
bool true, false
List<T> [1, 2, 3]
Map<K, V> {'key': 'value'}
Set<T> {1, 2, 3}
int i = 5;          // type explicite
var j = 5;          // inférence : j est int, définitivement
// j = 'dix';       // ❌ erreur de compilation
late String config; // initialisée plus tard, avant utilisation
dynamic data = 'x'; // n'importe quel type, plus AUCUNE vérification
  • var → le type est fixé à la première assignation (inférence, pas de dynamisme).
  • dynamic → désactive complètement le typage : perte d'autocomplétion, erreurs à l'exécution. Cas légitimes : JSON d'API externe, interop JS, prototypage, libs très génériques.
  • late → non-nullable mais initialisé plus tard.

Interpolation : 'Bonjour $name', '${age + 1} ans', '${age >= 18 ? "Majeur" : "Mineur"}'.

3.3 Null safety (chapitre chouchou des examens)

Le problème : en Java/C#, une variable objet peut valoir null ; on peut quand même appeler s.lengthNullPointerException / NullReferenceException à l'exécution.

La solution Dart : depuis Dart 2 les types peuvent être null-safe, depuis Dart 3 (mai 2023) ils le sont tous. Par défaut tout est non-nullable ; on rend un type nullable avec le suffixe ?.

String s = null;    // ❌ interdit
String? s2 = null;  // ✅ nullable

Sound null safety : la hiérarchie de types change. Avant, Null était sous-type de tout (List, double, int…). Avec la sound null safety, Null sort de l'arbre Object et String? devient l'union de String et Null.

Les 3 opérateurs :

Opérateur Nom Effet
?. safe navigation appelle le membre seulement si non-null, sinon l'expression vaut null
?? null coalescing valeur de droite si la gauche est null
! null assertion force le compilateur à traiter la valeur comme non-null — crash à l'exécution si elle est null
String checkList(List<Object>? list) {
  if (list?.isEmpty ?? true) return 'Got nothing';   // ?. + ??
  return 'Got something';
}

String checkList2(List<Object>? list) {
  if (list!.isEmpty) return 'Got nothing';           // ! : à n'utiliser qu'après un test != null
  return 'Got something';
}

late : variable non-nullable initialisée plus tard → late String name;

3.4 Fonctions

Trois formes de paramètres :

// Positionnels (obligatoires) + positionnels optionnels entre [ ]
String getName2(User? user, [bool fullname = false, String prefix = ""]) { ... }
getName2(user, true, 'Mr.');

// Nommés entre { }, avec required pour les obligatoires (très "Flutter")
String getName(User? user, {bool fullname = false, required String prefix}) { ... }
getName(user, fullname: true, prefix: 'Mr.');

// Mélange possible
void setupAccount(String username, {required String email, String role = 'user'}) { ... }

// Arrow function
String greetShort(String name) => 'Bonjour $name !';

Fonctions first-class / lambdas : une fonction est une valeur, assignable à une variable, passable en paramètre, retournable.

var multiply = (int a, int b) => a * b;

String Function(User? user, {bool fullname, required String prefix}) getName =
    (User? user, {bool fullname = false, required String prefix}) { ... };

void processNumbers(List<int> numbers, Function(int) processor) {
  for (var n in numbers) processor(n);
}
processNumbers([1, 2, 3], (n) => print('Nombre: $n'));

3.5 Collections

// List
var fruits = ['pomme', 'banane'];
fruits.add('kiwi'); fruits.addAll([...]); fruits.insert(1, 'fraise');
fruits.remove('banane'); fruits.removeAt(0);
fruits.length; fruits.isEmpty; fruits.contains('pomme');
fruits.map((f) => f.toUpperCase()).toList();
fruits.where((f) => f.startsWith('a')).toList();

// Map
var ages = <String, int>{'Alice': 25};
ages['Diana'] = 26; ages.remove('Bob');
int? a = ages['Alice'];          // accès nullable
int safe = ages['Zoe'] ?? 0;
ages.forEach((nom, age) => print('$nom a $age ans'));
ages.keys.toList(); ages.values.toList();

// Set (pas de doublons)
var s1 = {1, 2, 3}, s2 = {2, 3, 4};
s1.union(s2);        // {1,2,3,4}
s1.intersection(s2); // {2,3}
s1.difference(s2);   // {1}

Syntaxe déclarative moderne (if, for, spread ... dans une collection) :

var menuItems = [
  'Accueil',
  if (includeExtra) 'Extra',
  if (user.isAdmin) ...adminItems,
  for (var i in [1, 2, 3]) 'Item$i',
];

4. Dart — POO

4.1 Classe type

class User {
  final String name;      // immutable (recommandé en Flutter)
  final int age;
  String? email;

  User(this.name, this.age, {this.email});   // constructeur principal
  User.guest() : name = 'Invité', age = 0;   // constructeur nommé

  String greet() => 'Bonjour, je suis $name';
  bool get isAdult => age >= 18;             // getter
  set userEmail(String? e) { email = e?.toLowerCase(); }  // setter

  @override
  String toString() => 'User($name, $age ans)';
}

Version « slides » avec champs privés :

class User {
  String _firstName;
  String _lastName;
  User(this._firstName, this._lastName);
  String get firstName => _firstName;
  set firstName(String value) => _firstName = value;
  String getFullName() => "$_firstName $_lastName";
}
Aspect C#/Java Dart
Constructeur public User(String n){this.name=n;} User(this.name);
Getter public bool IsAdult(){...} bool get isAdult => age >= 18;
Readonly readonly string name; final String name;

4.2 Visibilité : la règle de l'underscore

Pas de private/public/protected. Tout ce qui commence par _ est privé : champs, méthodes, classes, getters/setters, constantes.

⚠️ Point piège : en Dart, privé = privé au fichier (bibliothèque), pas privé à la classe. Deux classes dans le même .dart accèdent à leurs membres _. Pour une vraie encapsulation au niveau classe → fichiers séparés.

Conventions : _balance, _validateData(), _DatabaseHelper, _MAX_RETRIES.

4.3 Constructeurs

class Point {
  final double x, y;

  Point(this.x, this.y);                       // principal
  Point.origin() : x = 0, y = 0;               // nommé
  Point.fromJson(Map<String, dynamic> json)    // nommé, liste d'initialisation
      : x = json['x']?.toDouble() ?? 0.0,
        y = json['y']?.toDouble() ?? 0.0;

  factory Point.cached(double x, double y) {   // factory
    return Point(x, y);
  }
}
  • Constructeurs nommés : Dart n'a pas de surcharge → on nomme les variantes (User.empty(), User.fromJSON(...)). Plus expressif et sans ambiguïté.
  • Liste d'initialisation (après :) : obligatoire pour initialiser les final et late final ; tous les champs non-nullables doivent être assignés avant la fin du constructeur.
  • Corps du constructeur : validation et logique, exécuté après l'initialisation.
  • factory : n'est pas obligé de créer une nouvelle instance — peut renvoyer une instance en cache, un singleton, ou choisir dynamiquement la sous-classe. Doit renvoyer une instance de la classe (ou sous-classe).
class Shape {
  factory Shape(String type) {
    switch (type) {
      case 'circle': return Circle();
      case 'square': return Square();
      default: throw ArgumentError('Unknown shape type');
    }
  }
}

4.4 extends / implements / mixin (les 3 mots-clés à ne pas confondre)

class Car {
  void accelerate() => print("simple acceleration");
  void brakes()     => print("simple brakes");
}

extends — héritage simple, on hérite du code, on redéfinit ce qu'on veut avec @override :

class Toyota extends Car {
  @override
  void brakes() => print("toyota disk brakes");
}
// toyota.accelerate() → "simple acceleration"  (hérité)
// toyota.brakes()     → "toyota disk brakes"

implementstoute classe peut servir d'interface. On récupère uniquement le contrat : il faut réimplémenter toutes les méthodes, rien n'est hérité. Interfaces multiples possibles.

class Toyota implements Car {
  void brakes()     => print("toyota disk brakes");
  void accelerate() => print("toyota power acceleration");  // OBLIGATOIRE
}
// toyota.accelerate() → "toyota power acceleration"

Interface « pure » : abstract interface class PaymentProcessor { ... } avec uniquement des signatures.

mixinwith — réutilisation de code sans héritage multiple. Le code du mixin est copié dans la classe. Un mixin ne peut pas avoir de constructeur, mais peut avoir propriétés et méthodes, et être générique.

mixin EuroVICar {
  void emission() => print("euro vi standard emission");
}

class Toyota with EuroVICar implements Car {
  void brakes()     => print("toyota disk brakes");
  void accelerate() => print("toyota power acceleration");
}
// output : toyota power acceleration / toyota disk brakes / euro vi standard emission

class Duck extends Animal with Flyable, Swimmable, Debuggable { ... }
mixin CacheMixin<T> { final Map<String, T> _cache = {}; ... }

Classes abstraites & polymorphisme : abstract class Animal { void makeSound(); void sleep() => ...; }, sous-classes avec super(name, age), @override, test de type if (animal is Dog) animal.fetch();.

4.5 Extensions (n'existe pas en C#/Java sous cette forme)

Ajouter méthodes / getters / opérateurs à une classe existante qu'on ne contrôle pas.

extension StringValidation on String {
  bool get isValidEmail => contains('@') && contains('.') && length > 5;
  String truncate(int maxLength) =>
      length <= maxLength ? this : '${substring(0, maxLength)}...';
}

extension ListUtils<T> on List<T> {
  T? get firstOrNull => isEmpty ? null : first;
}

'user@example.com'.isValidEmail;   // true

4.6 @immutable

En Flutter, on privilégie les classes immuables : toutes les propriétés final, constructeur const, annotation @immutable (package meta). Améliore prévisibilité, testabilité, gestion d'état.


5. Dart — programmation asynchrone

5.1 Le modèle

  • Single-threaded : une seule opération à la fois.
  • Event loop : file d'attente des tâches, exécution des callbacks quand le thread principal est libre.
  • Non-blocking : réseau, fichiers… n'empêchent pas le reste de tourner.

5.2 Future

Future<T> = valeur disponible plus tard (≈ Task<T> en C#, Promise<T> en JS).

Future<String> getUserName(int id) async {
  await Future.delayed(Duration(seconds: 2));
  return 'Utilisateur $id';
}

void waitNSeconds(int n) async {
  await Future.delayed(Duration(seconds: n));
  print('$n seconds have passed');
}

Points à retenir : - Une fonction async retourne toujours un Future, même si elle renvoie une valeur simple. - await n'est utilisable que dans une fonction async. - En Dart, async se place après la signature, avant le corps (≠ C#/JS où le mot-clé est en tête). - Un Future ne démarre pas tant qu'il n'est pas appelé/attendu (« promesse paresseuse »).

Séquentiel vs parallèle :

String name = await getUserName(123);      // séquentiel
int age = await getUserAge(123);

var results = await Future.wait([          // parallèle (≈ Task.WhenAll)
  getUserName(123),
  getUserAge(123),
]);

Erreurs : try / catch, on NotFoundException, on NetworkException catch (e), rethrow. Alternative typée : modèle Result (sealed class Result<T,E> avec Success / Failure).

Style callback : .then(...), .catchError(...), .whenComplete(...), .timeout(Duration(...)).

5.3 Streams

Flux continu de valeurs (≈ IObservable en C#). Pipeline : source → Stream → transformations (map, where, distinct, take) → écoute (listen, await for, StreamBuilder).

Création — générateur async* + yield (contrairement à return, yield n'interrompt pas la fonction) :

Stream<int> count(int countTo) async* {
  for (int i = 1; i <= countTo; i++) {
    yield i;
    await Future.delayed(const Duration(seconds: 1));
  }
}
count(10).listen((int value) => print(value));

Création — StreamController (≈ Subject) :

class MyDataService {
  final _onNewData = StreamController<String>();
  Stream<String> get onNewData => _onNewData.stream;   // sortie
  void addData(data) => _onNewData.add(data);          // émission
  void dispose() => _onNewData.close();                // TOUJOURS fermer
}

final subscription = service.onNewData.listen(
  (String data) => print(data),
  onError: (error) => print(error),
  onDone: () => print("Stream closed!"),
);
subscription.cancel();

controller.addError(...) pour émettre une erreur.

Consommation — await for (boucle asynchrone, break possible, s'arrête à la fermeture du flux) :

await for (int number in numberStream()) {
  print('Reçu: $number');
  if (number == 3) break;
}

Transformation :

input.where((m) => m.isNotEmpty).map((m) => m.toUpperCase()).distinct().take(10);

5.4 Patterns avancés

  • Isolates — vraie concurrence, thread séparé pour les calculs lourds : await Isolate.run(() { ... }) (≈ Task.Run).
  • Timeout / retry.timeout(Duration(seconds: 10)), boucle de retry avec rethrow à la dernière tentative.
  • Debounce / throttle — debounce : attendre la fin de la saisie (Timer annulé et relancé) ; throttle : limiter la fréquence.

6. Dart — manipulation de données

6.1 JSON avec dart:convert

import 'dart:convert';

Map<String, dynamic> userJson = json.decode(jsonString);  // parsing
int id = userJson['id'] as int;
List<String> skills = (userJson['skills'] as List<dynamic>).map((s) => s as String).toList();
String? email = userJson['email'] as String?;

String out = json.encode(userData);                        // sérialisation
String pretty = JsonEncoder.withIndent('  ').convert(userData);

Contrairement à C# (Newtonsoft), Dart ne désérialise pas directement en objet : on obtient une Map<String, dynamic> qu'on convertit soi-même.

6.2 Le modèle de données canonique

Le trio factory X.fromJson / toJson() / copyWith() :

class User {
  final int id;
  final String name;
  final String? email;
  final List<String> skills;

  const User({required this.id, required this.name, this.email, this.skills = const []});

  factory User.fromJson(Map<String, dynamic> json) => User(
        id: json['id'] as int,
        name: json['name'] as String,
        email: json['email'] as String?,
        skills: (json['skills'] as List<dynamic>?)?.map((s) => s as String).toList() ?? [],
      );

  Map<String, dynamic> toJson() => {'id': id, 'name': name, 'email': email, 'skills': skills};

  User copyWith({int? id, String? name, String? email}) => User(
        id: id ?? this.id,
        name: name ?? this.name,
        email: email ?? this.email,
        skills: skills,
      );

  @override
  bool operator ==(Object other) =>
      identical(this, other) || other is User && id == other.id;
  @override
  int get hashCode => id.hashCode;
}

copyWith = créer une copie modifiée d'un objet immuable (essentiel avec BLoC).

Parsing robuste : extension getAs<T> / getAsOrDefault<T> sur Map<String, dynamic>, validation dans la factory, DateTime.tryParse, throw FormatException(...).

6.3 HTTP

dart pub add http        # ou flutter pub add http
import 'dart:convert';
import 'package:http/http.dart' as http;

workWithFuture() async {
  print(await http.read(Uri.https('randomuser.me', 'api')));
  var url = Uri.https('randomuser.me', 'api', {'results': "500"});
  var response = await http.get(url);
  print('Response status: ${response.statusCode}');
  var data = jsonDecode(response.body);
}

Service typique : get / post / put / delete, headers Content-Type: application/json, .timeout(...), tests sur statusCode (200 / 201 / 204 / 404), exceptions custom (ApiException, UserNotFoundException), pagination (UsersResponse avec total, page, totalPages).

6.4 Repository pattern

Interface abstraite (abstract class UserRepository) + implémentation avec cache (Map<int, User> _cache + _cacheTimestamps + _cacheExpiry). Sépare la logique métier de l'accès aux données → testabilité et maintenance.

6.5 Persistance locale

  • shared_preferences — petites données (préférences, user courant) sérialisées en JSON : prefs.setString(key, jsonEncode(...)), prefs.getString(key), prefs.remove(key).
  • Fichiers JSONpath_provider (getApplicationDocumentsDirectory()) + dart:io (File(...).writeAsString / readAsString) pour des volumes plus gros.
  • SQLite (sqflite + path)openDatabase(path, version, onCreate, onUpgrade), db.insert/query/update/delete/rawQuery, db.transaction(...), index, migrations. Modèle avec fromMap / toMap (⚠️ SQLite stocke les booléens en INTEGER 0/1, les dates en toIso8601String()).
  • ORM — Flutter n'a pas d'ORM officiel (≠ Entity Framework / Hibernate) :
SQLite pur Drift Floor ObjectBox
Facilité faible bonne moyenne excellente
Type safety faible excellente bonne excellente
Relations faible excellente moyenne excellente
Migrations moyenne excellente bonne moyenne

Drift = l'ORM le plus mature (tables générées, code type-safe) · Floor = inspiré de Room (annotations @entity, @dao, @Query) · ObjectBox = NoSQL très rapide.


7. Flutter — bases

7.1 Structure du projet

mon_app/
├── android/          # config Android
├── ios/              # config iOS
├── assets/           # images, fonts, data
├── lib/              # code Dart
│   ├── main.dart     # point d'entrée
│   ├── models/
│   ├── screens/ (ou pages/)
│   ├── widgets/
│   └── services/
├── test/
└── pubspec.yaml      # configuration + dépendances

pubspec.yaml : name, description, version, environment (sdk, flutter), dependencies, dev_dependencies, section flutter: (uses-material-design, assets:, fonts:). Commandes : flutter create ma_premiere_app, flutter run, flutter pub add <pkg>, flutter pub deps, flutter pub upgrade, flutter doctor.

7.2 Code minimal et point d'entrée

// lib/main.dart
import 'package:flutter/widgets.dart';

main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(context) =>
      Center(child: Text('Hello Flutter!', textDirection: TextDirection.ltr));
}

Version Material :

import 'package:flutter/material.dart';

void main() => runApp(MonApplication());

class MonApplication extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'Mon App',
        theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
        home: EcranAccueil(),
        debugShowCheckedModeBanner: false,
      );
}

7.3 Material ou Cupertino

MaterialApp (style Android/Google) vs CupertinoApp (style iOS). Adaptatif possible via Platform.isIOS (dart:io).

7.4 Widgets essentiels (tableau des slides)

Nom Utilité
Text Label
ElevatedButton Bouton coloré
TextButton Bouton non coloré
Column Composition verticale
Row Composition horizontale
Expanded Composition étendue
Flex Composition flexible
SizedBox Limitation de taille / espace
SingleChildScrollView Scroll automatique du contenu
Icon Icône
Image Image
Avatar Image stylisée d'un utilisateur
StreamBuilder Reconstruction à chaque nouvelle entrée du stream
LayoutBuilder Reconstruction à chaque nouvelle dimension du widget

Structure d'écran : Scaffold (appBar, body, bottomNavigationBar, floatingActionButton). Affichage : Text, RichText/TextSpan, Image.asset, Image.network (loadingBuilder, errorBuilder), Icon. Interaction : TextField (+ TextEditingController), ElevatedButton / OutlinedButton / TextButton, SwitchListTile, Slider, RadioListTile. Décoration : Container (padding, margin, decoration: BoxDecoration(color, borderRadius, border)), Card, Padding, Wrap, Chip.

7.5 Mise en page

  • Row = horizontal, Column = vertical.
  • mainAxisAlignment : axe principal (horizontal pour Row, vertical pour Column).
  • crossAxisAlignment : axe secondaire.
  • Expanded : force l'occupation de tout l'espace restant ; flex: pour les proportions (flex:1 + flex:2 → 1/3 et 2/3).
  • Flexible : autorise le widget à prendre jusqu'à l'espace disponible, sans le forcer.
  • Stack : superposition en calques + Positioned (top, bottom, left, right).
  • ListView : liste défilante. ListView.builder(itemCount:, itemBuilder:)ne construit que les éléments visibles : perf, mémoire, fluidité. À privilégier systématiquement pour les listes dynamiques.
  • GridView.count(crossAxisCount:) et GridView.builder(gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(...)).
  • ListTile (leading, title, subtitle, trailing, onTap).

7.6 Thèmes

ThemeData dans MaterialApp : primarySwatch, useMaterial3, colorScheme: ColorScheme.fromSeed(seedColor:, brightness:), textTheme, elevatedButtonTheme, appBarTheme. Thème sombre via darkTheme: + themeMode: ThemeMode.system. Accès : Theme.of(context).textTheme.headlineMedium, Theme.of(context).colorScheme.primaryContainer.

7.7 Hot reload

Hot Reload (Ctrl+S) : voir les changements instantanément. Hot Restart (Ctrl+Shift+R) : recharge complète.


8. Flutter — gestion de l'état

8.1 Types d'état

  • État local (widget state) : propre à un widget/écran (compteur, champ de saisie).
  • État global (app state) : partagé entre écrans (utilisateur, préférences, panier).
  • Des données statiques (liste de pays) ne constituent pas un état.

8.2 StatelessWidget vs StatefulWidget

StatelessWidget — immuable, l'état est fixé à la création et ne change pas. Exemples : avatars, textes fixes, images. Utiliser const autant que possible.

class UserName extends StatelessWidget {
  final String name;
  const UserName(this.name);

  @override
  Widget build(BuildContext context) => DecoratedBox(
        decoration: BoxDecoration(color: Colors.lightBlueAccent),
        child: Padding(padding: const EdgeInsets.all(8.0), child: Text(name)),
      );
}

StatefulWidget — se redessine sur changement d'état. Divisé en 2 classes : le widget (configuration) + l'objet State (état mutable et logique).

class Compteur extends StatefulWidget {
  const Compteur({Key? key}) : super(key: key);
  @override
  State<Compteur> createState() => _CompteurState();
}

class _CompteurState extends State<Compteur> {
  int i = 0;
  @override
  Widget build(BuildContext context) => ElevatedButton(
        child: Text('Cliqué ${i} fois'),
        onPressed: () => setState(() => i++),
      );
}

À chaque setState, build est rappelée pour rafraîchir le widget.

8.3 Cycle de vie d'un StatefulWidget

Méthode Quand Rôle
initState() une seule fois, après création initialiser données, créer controllers ; super.initState() obligatoire
build() à chaque redessin retourner l'arbre de widgets
dispose() avant destruction libérer les ressources (controllers, timers, streams) ; super.dispose() obligatoire

Règles d'or : - Toujours dispose() les controllers et StreamControllers. - Vérifier mounted avant un setState() dans un callback asynchrone. - Ne jamais appeler setState() dans build(), ni après dispose(). - Garder setState() léger (pas de calculs lourds).

8.4 FutureBuilder et StreamBuilder

FutureBuilder StreamBuilder
Source une seule valeur future flux continu
Cas d'usage appel API REST, lecture de fichier, requête DB ponctuelle chat, notifications, capteurs, Firestore
FutureBuilder<List<String>>(
  future: _chargerDonnees(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) return CircularProgressIndicator();
    if (snapshot.hasError) return Text('Erreur: ${snapshot.error}');
    final donnees = snapshot.data!;
    return ListView.builder(...);
  },
)
StreamBuilder<int>(
  stream: _controller.stream,
  initialData: 0,                 // évite l'état de chargement initial
  builder: (context, snapshot) => Text('${snapshot.data}'),
)

Le trio de tests à connaître : snapshot.hasErrorsnapshot.connectionState == ConnectionState.waitingsnapshot.hasData / snapshot.data!.

8.5 Optimisation des rebuilds

Chaque setState() redessine le widget et ses enfants. Trois techniques :

  1. const — un widget const n'est jamais reconstruit.
  2. Séparer les widgets — isoler ce qui change dans de petits widgets dédiés (AffichageCompteur(valeur: _compteur)).
  3. Keyskey: ValueKey(items[index].id) aide Flutter à identifier ce qui a vraiment changé dans une liste.

À éviter : calculs lourds dans build(), setState() trop fréquents, widgets qui font trop de choses. Astuce debug : mettre un print() dans les build() pour voir ce qui se reconstruit.


9. Flutter — navigation

Flutter gère les écrans avec une pile (stack) via le Navigator.

Navigation de base

Navigator.push(context, MaterialPageRoute(builder: (context) => const SecondRoute()));
Navigator.pop(context);                       // retour
Navigator.pushReplacement(context, MaterialPageRoute(builder: (c) => HomeScreen()));

Routes nommées

MaterialApp(
  initialRoute: '/',
  routes: {
    '/': (context) => const FirstScreen(),
    '/second': (context) => const SecondScreen(),
  },
)
Navigator.pushNamed(context, '/second');

Passage d'arguments

Navigator.pushNamed(context, ExtractArgumentsScreen.routeName, arguments: "Ceci est un argument");
// dans le build de l'écran cible :
final String args = ModalRoute.of(context)!.settings.arguments as String;

Autre méthode (syllabus) : passer les données par le constructeur du widget cible (DetailScreen(title: ..., description: ...)).

Retour avec résultat

var result = await Navigator.pushNamed(context, SecondScreen.routeName, arguments: "...");
setState(() => texteAAfficher = result);
// dans SecondScreen :
Navigator.pop(context, "Ceci est le résultat");   // n'importe quel objet

Dialogues

showDialog(
  context: context,
  builder: (BuildContext context) => AlertDialog(
    title: Text("Erreur !"),
    content: Text("Mon dieu, c'est une erreur !"),
    actions: <Widget>[
      TextButton(child: Text("Fermer"), onPressed: () => Navigator.of(context).pop()),
    ],
  ),
);

10. Architecture BLoC

10.1 Le problème et l'idée

Problème de Flutter avec setState() seul : - rebuilds répétitifs de tous les enfants d'un widget dès qu'il change un peu d'état ; - difficile de retrouver les données entre widgets parents/enfants/frères ; - état dispersé, couplage fort UI ↔ logique, testabilité limitée, code peu réutilisable.

Idée : séparer événements, états et mise à jour des widgets en 3 parties distinctes. (Comparaison faite en cours avec les contextes React.)

BLoC = Business Logic Component, pattern de Google, basé sur les Streams de Dart. Principe : « tout est un Stream ». Schéma : UI → Event → BLoC → State → UI, et le BLoC fait request/response avec la couche data.

Architecture Complexité Testabilité Cas d'usage
setState faible limitée widgets simples
Provider moyenne bonne apps moyennes
BLoC élevée excellente apps complexes

10.2 Les 3 composants

  1. Events : actions de l'utilisateur (Incrementer, AddTodo, LoadTodos).
  2. States : états de l'app (TodoLoading, TodoLoaded, TodoError).
  3. BLoC : convertit les Events en States (logique métier).

Flux unidirectionnel : l'utilisateur agit → l'UI envoie un Event → le BLoC produit un State → l'UI se reconstruit.

10.3 BLoC « à la main » (sans package)

abstract class CompteurEvent {}
class Incrementer extends CompteurEvent {}
class Decrementer extends CompteurEvent {}

abstract class CompteurState {}
class CompteurInitial extends CompteurState {}
class CompteurValeur extends CompteurState {
  final int valeur;
  CompteurValeur(this.valeur);
}

class CompteurBloc {
  int _compteur = 0;
  final _stateController = StreamController<CompteurState>();
  Stream<CompteurState> get stateStream => _stateController.stream;   // sortie
  final _eventController = StreamController<CompteurEvent>();
  Sink<CompteurEvent> get eventSink => _eventController.sink;         // entrée

  CompteurBloc() {
    _eventController.stream.listen(_handleEvent);
    _stateController.add(CompteurInitial());
  }

  void _handleEvent(CompteurEvent event) {
    if (event is Incrementer) _compteur++;
    else if (event is Decrementer) _compteur--;
    _stateController.add(CompteurValeur(_compteur));
  }

  void dispose() { _eventController.close(); _stateController.close(); }
}

Côté UI : StreamBuilder<CompteurState>(stream: _bloc.stateStream, ...) et _bloc.eventSink.add(Incrementer()). dispose() du bloc dans le dispose() du State.

10.4 Package flutter_bloc

flutter pub add flutter_bloc equatable

Installation : https://pub.dev/packages/flutter_bloc

Apports : gestion automatique du cycle de vie des streams, widgets spécialisés, rebuild intelligent, API simplifiée, intégration Equatable (comparaison d'états via props).

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterInitial()) {
    on<CounterIncremented>(_onIncremented);
  }
  int _counter = 0;
  void _onIncremented(CounterIncremented event, Emitter<CounterState> emit) {
    _counter++;
    emit(CounterValue(_counter));
  }
}

Events/States avec Equatable :

abstract class CounterEvent extends Equatable {
  @override
  List<Object> get props => [];
}
class CounterValue extends CounterState {
  final int value;
  CounterValue(this.value);
  @override
  List<Object> get props => [value];
}

Les 4 widgets à connaître :

Widget Rôle
BlocProvider injecte le BLoC dans l'arbre : BlocProvider(create: (c) => CounterBloc(), child: ...)
BlocBuilder reconstruit l'UI à chaque changement d'état
BlocListener exécute une action (snackbar, navigation) sans reconstruire
BlocConsumer combine Builder + Listener

Envoyer un event : context.read<CounterBloc>().add(CounterIncremented()); ou BlocProvider.of<CounterBloc>(context).add(...).

Gestion d'erreurs dans un handler :

void _onLoadTodos(LoadTodos event, Emitter<TodoState> emit) async {
  emit(TodoLoading());
  try {
    final todos = await _repository.getTodos();
    emit(TodoLoaded(todos));
  } catch (e) {
    emit(TodoError(e.toString()));
  }
}

Organisation des fichiers : lib/blocs/todo/{todo_bloc.dart, todo_event.dart, todo_state.dart}, lib/models/, lib/screens/. Conventions : Events = verbes à l'impératif (LoadTodos, AddTodo) · States = noms/adjectifs (TodoLoading, TodoLoaded) · BLoC = suffixe Bloc. Tests : blocTest<TodoBloc, TodoState>(build:, act:, expect:), setUp / tearDown (bloc.close()). Debug : class AppBlocObserver extends BlocObserver (onChange, onError) + Bloc.observer = AppBlocObserver(); dans main.

Avantages : séparation claire, testabilité, réutilisabilité, prédictibilité, débogage facile. Inconvénients : courbe d'apprentissage, boilerplate, attention aux rebuilds inutiles.


11. Firebase & Cloud Firestore

11.1 Idée

Firebase = BaaS (Backend-as-a-Service) de Google : pas de serveur à créer/maintenir. Beaucoup d'outils out-of-the-box.

Service Rôle
Authentication comptes utilisateurs (email/mdp, Google, Facebook…)
Cloud Firestore base NoSQL orientée documents
Cloud Storage fichiers (photos de profil…)
Cloud Functions code serveur (notifications push…)
+ Analytics, Crashlytics, Performance, Test Lab, Hosting, ML Kit

Base de données temps réel, NoSQL orientée documents, très proche du JSON, les clients peuvent écouter les changements d'état. Docs : https://firebase.google.com/docs/guides

11.2 Les 2 types de DB (question classique)

Realtime Database Cloud Firestore
Stocke tout dans une grande arborescence JSON Stocke des collections de documents
Données simples : très faciles à stocker Données simples : faciles à stocker (documents ≈ JSON)
Données complexes/hiérarchiques : difficiles à organiser à grande échelle Données complexes : plus faciles grâce aux sous-collections
Nécessite moins de dénormalisation et d'aplatissement

Référence : https://firebase.google.com/docs/database/rtdb-vs-firestore

11.3 Structure Firestore

📁 users/                    ← Collection
  📄 alice123/               ← Document
     • name: "Alice"         ← Champ
     📁 tasks/               ← Sous-collection
       📄 task1/  • title, • completed

Collection = dossier · Document = fichier JSON · Champ = ligne de ce JSON.

11.4 Mise en place

flutter pub add cloud_firestore firebase_core firebase_auth
# ou, recommandé : FlutterFire CLI
npm install -g firebase-tools
dart pub global activate flutterfire_cli
firebase login
flutterfire configure
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();          // OBLIGATOIRE avant Firebase
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,  // firebase_options.dart généré
  );
  runApp(const MyApp());
}

⚠️ L'initialisation doit précéder runApp(), sinon crash au premier appel Firebase.

11.5 Connexion et lecture

late FirebaseFirestore db;
late Stream<QuerySnapshot> messagesCollection;

@override
void initState() {
  super.initState();
  this.db = FirebaseFirestore.instance;
  this.messagesCollection = db.collection("messages").snapshots();
}
StreamBuilder<QuerySnapshot>(
  stream: messagesCollection,
  builder: (context, snapshot) {
    if (snapshot.hasError) return Text("Error");
    if (snapshot.connectionState == ConnectionState.waiting) return Text("Loading");
    return ListView(
      children: snapshot.data!.docs.map((DocumentSnapshot document) {
        var data = document.data() as Map<String, dynamic>;
        return ListTile(title: Text(data['texte']), subtitle: Text(data['username']));
      }).toList(),
    );
  },
)

CRUD Firestore :

CollectionReference get _userTasks =>
    _db.collection('users').doc(userId).collection('tasks');

await _userTasks.add({'title': t, 'completed': false,
                      'createdAt': FieldValue.serverTimestamp()});   // CREATE
Stream<QuerySnapshot> s = _userTasks.orderBy('createdAt', descending: true).snapshots(); // READ temps réel
await _userTasks.doc(taskId).update({'completed': true});            // UPDATE
await _userTasks.doc(taskId).delete();                               // DELETE

Modèle : factory Tache.fromFirestore(DocumentSnapshot doc) (avec doc.id et (data['createdAt'] as Timestamp?)?.toDate()) + toFirestore().

11.6 Authentication

Stream<User?> authChanges = FirebaseAuth.instance.authStateChanges();
User? currentUser = FirebaseAuth.instance.currentUser;

await _auth.createUserWithEmailAndPassword(email: e, password: p);  // inscription
await _auth.signInWithEmailAndPassword(email: e, password: p);      // connexion
await _auth.signOut();                                              // déconnexion

AuthWrapper — le « chef d'orchestre » qui choisit la page selon l'état de connexion :

StreamBuilder<User?>(
  stream: FirebaseAuth.instance.authStateChanges(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) return EcranChargement();
    return snapshot.hasData ? AppPrincipale() : PageAuth();
  },
)

Pourquoi un Stream et pas un Future ? Future = « donne-moi le résultat une fois » ; Stream = « tiens-moi au courant de tous les changements » — l'état de connexion peut changer à tout moment.

11.7 Règles de sécurité

Définies côté serveur (console Firebase), donc inviolables.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
      match /tasks/{taskId} {
        allow read, write: if request.auth != null && request.auth.uid == userId;
      }
    }
    match /categories/{categoryId} {
      allow read: if true;
      allow write: if request.auth != null;
    }
  }
}
Règle Signification
request.auth != null l'utilisateur est connecté
request.auth.uid == userId il n'accède qu'à ses propres données
allow read: if true lecture publique
allow write: if false écriture interdite

11.8 Firebase + BLoC

Architecture en 3 couches :

📱 UI Layer     ← Widgets (affichage uniquement)
🧠 BLoC Layer   ← Logique métier et états
📦 Repo Layer   ← Accès aux données (Firebase)

Le Repository traduit Firebase ↔ BLoC :

Stream<List<Task>> watchTasks() => _tasksCollection
    .orderBy('createdAt', descending: true)
    .snapshots()
    .map((snap) => snap.docs.map((doc) => Task.fromFirestore(doc)).toList());

Le BLoC s'abonne au stream du repository dans le handler LoadTasks (StreamSubscription stockée, annulée dans close()), et les ajouts/modifs passent par le repo — le stream se met à jour tout seul. Config : MultiRepositoryProvider + RepositoryProvider<TaskRepository> autour de MaterialApp. Référence donnée en cours : https://petercoding.com/firebase/2022/03/28/using-firebase-with-bloc-pattern-in-flutter/


12. Fiche mémo — les 20 points qui tombent le plus

  1. Les 5 approches de dev mobile + un avantage et un inconvénient de chacune.
  2. Le tableau « quelle solution pour quel projet ».
  3. Flutter = Dart + moteur graphique, pas de VM entre l'app et l'OS ; tableau de transpilation par OS.
  4. Dates : Dart 3 = mai 2023 (null safety partout) · Flutter créé mai 2017, release publique décembre 2018.
  5. var vs dynamic vs late vs final.
  6. Null safety : ?, ?., ??, !, late — et le danger de !.
  7. Les 3 types de paramètres : positionnels, positionnels optionnels [], nommés {} + required.
  8. extends vs implements vs with (mixin) — quelle méthode s'exécute dans l'exemple Toyota ?
  9. Le _ = privé au fichier, pas à la classe.
  10. Constructeur nommé vs factory vs liste d'initialisation.
  11. Extensions : ajouter des méthodes à String, int, List.
  12. Future vs Stream : une valeur unique vs un flux ; async/await vs async*/yield.
  13. StreamController : .stream, .sink, .add(), .addError(), .close(), listen(onError:, onDone:), subscription.cancel().
  14. fromJson / toJson / copyWith — savoir écrire le modèle complet.
  15. StatelessWidget vs StatefulWidget : les 2 classes, createState, setStatebuild.
  16. Cycle de vie : initStatebuilddispose (+ mounted).
  17. FutureBuilder vs StreamBuilder et la gestion de snapshot (waiting / hasError / hasData).
  18. Layout : Row/Column + mainAxisAlignment/crossAxisAlignment, Expanded vs Flexible, Stack+Positioned, ListView.builder et pourquoi.
  19. Navigation : push/pop, routes nommées, arguments + ModalRoute.of(context)!.settings.arguments, retour de valeur via Navigator.pop(context, result).
  20. BLoC : Events / States / BLoC, flux unidirectionnel, BlocProvider / BlocBuilder / BlocListener / BlocConsumer, context.read<X>().add(...), emit(...), Equatable.

Bonus Firebase : RTDB vs Firestore, collection/document/champ, WidgetsFlutterBinding.ensureInitialized() avant Firebase.initializeApp(), authStateChanges() en Stream, règles request.auth.uid == userId.


13. Erreurs à ne pas commettre en question de code

  • Oublier super.initState() / super.dispose().
  • Ne pas fermer un StreamController ou ne pas dispose() un TextEditingController.
  • Utiliser ! sans avoir vérifié != null avant.
  • Appeler setState() dans build(), ou après dispose() (d'où le test mounted).
  • Confondre implements (tout réimplémenter) et extends (on hérite).
  • Mettre async au mauvais endroit : en Dart c'est Future<T> f() async { ... }, pas async Future<T> f().
  • Utiliser ListView(children: [...]) pour une grande liste au lieu de ListView.builder.
  • Modifier une liste d'état sans en créer une nouvelle en BLoC (préférer _todos = [..._todos, newTodo]).
Haut de page