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.length → NullPointerException / 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 lesfinaletlate 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"
implements — toute 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.
mixin … with — 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 avecrethrowà la dernière tentative. - Debounce / throttle — debounce : attendre la fin de la saisie (
Timerannulé 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 JSON —
path_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 avecfromMap/toMap(⚠️ SQLite stocke les booléens en INTEGER 0/1, les dates entoIso8601String()). - 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:)etGridView.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.hasError → snapshot.connectionState == ConnectionState.waiting → snapshot.hasData / snapshot.data!.
8.5 Optimisation des rebuilds
Chaque setState() redessine le widget et ses enfants. Trois techniques :
const— un widgetconstn'est jamais reconstruit.- Séparer les widgets — isoler ce qui change dans de petits widgets dédiés (
AffichageCompteur(valeur: _compteur)). - Keys —
key: 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
- Events : actions de l'utilisateur (
Incrementer,AddTodo,LoadTodos). - States : états de l'app (
TodoLoading,TodoLoaded,TodoError). - 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
- Les 5 approches de dev mobile + un avantage et un inconvénient de chacune.
- Le tableau « quelle solution pour quel projet ».
- Flutter = Dart + moteur graphique, pas de VM entre l'app et l'OS ; tableau de transpilation par OS.
- Dates : Dart 3 = mai 2023 (null safety partout) · Flutter créé mai 2017, release publique décembre 2018.
varvsdynamicvslatevsfinal.- Null safety :
?,?.,??,!,late— et le danger de!. - Les 3 types de paramètres : positionnels, positionnels optionnels
[], nommés{}+required. extendsvsimplementsvswith(mixin) — quelle méthode s'exécute dans l'exempleToyota?- Le
_= privé au fichier, pas à la classe. - Constructeur nommé vs
factoryvs liste d'initialisation. - Extensions : ajouter des méthodes à
String,int,List. FuturevsStream: une valeur unique vs un flux ;async/awaitvsasync*/yield.StreamController:.stream,.sink,.add(),.addError(),.close(),listen(onError:, onDone:),subscription.cancel().fromJson/toJson/copyWith— savoir écrire le modèle complet.- StatelessWidget vs StatefulWidget : les 2 classes,
createState,setState→build. - Cycle de vie :
initState→build→dispose(+mounted). FutureBuildervsStreamBuilderet la gestion desnapshot(waiting / hasError / hasData).- Layout :
Row/Column+mainAxisAlignment/crossAxisAlignment,ExpandedvsFlexible,Stack+Positioned,ListView.builderet pourquoi. - Navigation :
push/pop, routes nommées,arguments+ModalRoute.of(context)!.settings.arguments, retour de valeur viaNavigator.pop(context, result). - 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
StreamControllerou ne pasdispose()unTextEditingController. - Utiliser
!sans avoir vérifié!= nullavant. - Appeler
setState()dansbuild(), ou aprèsdispose()(d'où le testmounted). - Confondre
implements(tout réimplémenter) etextends(on hérite). - Mettre
asyncau mauvais endroit : en Dart c'estFuture<T> f() async { ... }, pasasync Future<T> f(). - Utiliser
ListView(children: [...])pour une grande liste au lieu deListView.builder. - Modifier une liste d'état sans en créer une nouvelle en BLoC (préférer
_todos = [..._todos, newTodo]).