🖨️ Version PDF
Avant d’aborder le langage DART, nous allons découvrir la structure d’un projet Flutter et vous allez constater que vous travaillez souvent dans le répertoire lib, avec le fichier pubspec.yaml et test.
Voici une vue d’ensemble de la stucture du projet de démonstration hello_flutter :
Le mieux est de séparer les dossiers en 4 groupes pour bien saisir leur utilité et nous pouvons ignorer les dossiers cachés .github, .vscode, .idea et .widget_preview :
Code Flutter ├── lib/ └── test/ Configuration ├── pubspec.yaml ├── analysis_options.yaml └── README.md Plateformes ├── android/ ├── ios/ ├── web/ ├── windows/ ├── linux/ └── macos/ Fichiers générés/outils ├── .dart_tool/ ├── .metadata ├── hello_flutter.iml ├── .gitignore └── pubspec.lock
C’est ici que l’on écrit quasiment tout le code Flutter.
Dans le projet actuel :
lib/ └── main.dart
Le fichier main.dart est le point d’entrée de l’application. Il contient une méthode main() comme en Java. Nous verrons ultérieurement le détail du code :
void main() { runApp(const MyApp()); }
Dans les applications mobiles que nous allons développer, nous allons avoir ce type de structure qui ressemble à nos projets habituels :
lib/ ├── main.dart ├── models/ ├── screens/ ├── widgets/ ├── services/ ├── repositories/ ├── viewmodels/ └── core/
Par exemple, lorsque vous allez développerer une application du type TaskFlow, vous aurez cette structure :
lib/ ├── main.dart │ ├── models/ │ └── task.dart │ ├── screens/ │ ├── login_screen.dart │ ├── task_list_screen.dart │ └── task_form_screen.dart │ ├── services/ │ └── api_service.dart │ ├── repositories/ │ └── task_repository.dart │ └── viewmodels/ └── task_view_model.dart
Un modèle proche du MVC que vous connaissez !
Ce dossier contient aussi les tests Flutter. Par défaut, on a souvent :
test/ └── widget_test.dart
Le test généré vérifie généralement que l’application de compteur fonctionne. Dans notre cas, cela donnerait le code ci-dessous :
import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hello_flutter/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(const MyApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); expect(find.text('1'), findsNothing); // Tap the '+' icon and trigger a frame. await tester.tap(find.byIcon(Icons.add)); await tester.pump(); // Verify that our counter has incremented. expect(find.text('0'), findsNothing); expect(find.text('1'), findsOneWidget); }); }
Cela vérifie que l’Application est lancée, que le compteur est égal à 0, et que lorsque l’on clique sur le bouton +, le compteur s’incrémente bien. Pour exécuter les tests, il suffit de saisir la commande flutter test.
flutter test
Dans une application professionnelle, on aura donc des dossiers :
pubspec.yaml
C’est probablement le deuxième fichier le plus important après lib/main.dart. Il permet de gérer :
On peut dire qu’il joue un rôle comparable au pom.xml dans Maven.
Pour un développeur.euse Spring Boot, voici une comparaison :
pom.xml
dependencies
src/main/resources
assets
<version>
version:
Exemple de contenu du fichier pubspec.yaml:
name: hello_flutter description: "Un nouveau projet Flutter" version: 1.0.0+1 dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.8 // si on veut utiliser HTTP http: ^1.6.0 // pour Provider : provider: ^6.1.5 // pour déclarer des images flutter: assets: - assets/images/
Puis lancez flutter pub get
flutter pub get
Pour récuperer votre image logo.png stockée dans le répertoire images, il vous suffira d’écrire le code ci-dessous :
Image.asset('assets/images/logo.png')
pubspec.lock
Ce fichier indique les versions exactes des bibliothèques installées. Par exemple pubspec.yaml peut demander une version supérieure ou égale à http: ^1.6.0, mais pubspec.lock va mémoriser la version exacte : http 1.6.0. Cela garantit que plusieurs développeur.euse.s utilisent les mêmes versions !
Flutter met à jour automatiquement ce fichier que vous n’avez pas besoin de modifier !.. ouf
Ce dossier contient tout ce qui est spécifique à Android. Flutter compile le code Dart, mais il doit ensuite produire une vraie application Android.
Ce dossier contient généralement :
android/ ├── app/ ├── gradle/ ├── build.gradle ├── settings.gradle └── ...
Selon les versions récentes, certains fichiers utilisent Kotlin DSL : build.gradle.kts
On y intervient pour des besoins Android spécifiques comme celles-ci :
Le fichier important est android/app/src/main/AndroidManifest.xml dans lequel on pourra déclarer ceci :
android/app/src/main/AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
Nous verrons cela ultérieurement…
Même principe que pour le dossier android, mais pour Apple (ios). Il contient le projet natif iOS utilisé par Xcode.
Par exemple :
ios/ ├── Runner/ ├── Runner.xcodeproj/ └── ...
Un fichier très important : ios/Runner/Info.plist qui sert notamment à déclarer pourquoi l’application demande certaines permissions.
ios/Runner/Info.plist
// caméra : <key>NSCameraUsageDescription</key> <string>L'application utilise la caméra pour photographier une tâche.</string> // géolocalisation : <key>NSLocationWhenInUseUsageDescription</key> <string>L'application utilise votre position.</string>
Attention : sous Windows, on peut avoir ce dossier, mais on ne peux pas compiler localement l’application iOS sans macOS et Xcode !
Ce dossier sert à produire une version Web de l’application Flutter. Il contient notamment :
Quand on exécute flutter run -d chrome, Flutter utilise ce dossier. Vous pouvez aussi construire la version Web : flutter build web pour obtenir ensuite un build/web/ qui peut être déployé sur un serveur Web.
flutter run -d chrome
flutter build web
Cela signifie qu’un même projet Flutter peut être généré pour différentes plateformes :
flutter analyze
lib/
src/main/java/
main.dart
MaWebApp.java
test/
src/test/java/
assets/
src/main/resources/
build/
target/
analysis_options.yaml
android/
ios/
A retenir : le code métier et l’interface Flutter restent principalement dans lib. les dossiers Android, iOS, Web, Windows, Linux et macOS servent de pont entre Flutter et chaque plateforme cible.
lib
Voilà pour un premier tour d’horizon sur la structure d’une application Flutter. Voyons maintenant quelques notions du langage DART très proche de Java sans toutefois rentrer dans les détails. Je vous donnerai un lien ou des documents PDF sur le langage lui-même.
Dart est un langage orienté objet, à typage statique, très proche syntaxiquement de Java/Kotlin. Vous allez vous sentir rapidement à l’aise, même si vous avez fait du C# !
void main() { // typage explicite int age = 57; double taille = 1.74; String prenom = "Agnès"; bool estActif = true; // inférence de type (équivalent de `var` en Java version 10+) ou JS si vous êtes familier à ce langage. var ville = "Paris"; // Dart en déduit un String // Constantes avec des différences final DateTime maintenant = DateTime.now(); // valeur fixée à l'exécution car on ne connait pas encore le date ! const double pi = 3.14159; // valeur fixée à la compilation avant que le programme ne démarre ! print("$prenom a $age ans et habite à $ville"); // interpolation de chaînes }
En Flutter, il faut bien comprendre l’intérêt et la différence des constantes de type final et const :
final
const
La valeur est connue et figée avant même que votre programme soit lancé. Le compilateur la “grave” littéralement dans le code compilé.
Conséquences :
const x = 3.14 * 2
const x = DateTime.now()
const Text('Bonjour') // jamais reconstruit, réutilisé tel quel
Avec finaldans notre exemple plus haut, la valeur est calculée au moment où la ligne s’exécute, au lancement de votre programme. Une fois assignée, elle ne peut plus changer, mais elle peut différer à chaque exécution (chaque lancement donne une date différente).
final DateTime maintenant = DateTime.now();
exemples :
final maintenant = DateTime.now(); // ok — évalué quand la ligne tourne maintenant = autreChose; // erreur : déjà assigné const test = DateTime.now(); // erreur de compilation : pas une constante
Petit tableau récapitulatif
pi
DateTime.now()
widget
Les bénéfices concrets :
Règle pratique : Utilisez const dès que possible, et final lorsque la valeur dépend de l’exécution (heure, entrée utilisateur, réseau, random…). En Flutter, préfixer vos widgets statiques par const (quand c’est possible) est un réflexe recommandé pour garder une app fluide.
String.format(...)
"$variable"
"${expression}"
Comme Optional<T> en Java, mais intégré nativement au système de types, ici on utilise le point d’interrogation ? comme en Php :
Optional<T>
?
String nom = "Bob"; // ne peut jamais être null String? surnom; // peut être null (le "?" est obligatoire) print(surnom?.length); // accès sécurisé : renvoie null si surnom est null print(surnom ?? "Aucun"); // opérateur "??" : valeur par défaut si null dans le cas où l'on doit afficher une valeur
Pas de nouveauté, exceptés les paramètres nommés et les préfixes.
// une fonction classique int addition(int a, int b) { return a + b; } // une fonction fléchée (equivalent lambda concise) devenue courante dans de nombreux langages int carre(int x) => x * x; // paramètres nommés (très utilisés dans les widgets Flutter !) // il faut penser à ne pas oublier les accolades void afficherMessage({required String texte, String prefixe = "Message"}) { print("[$prefixe] $texte"); } afficherMessage(texte: "Bonjour"); // -> [Message] Bonjour
Les paramètres nommés ({...}) sont partout dans Flutter : chaque widget se construit avec ce type de syntaxe, un peu comme un Builder Pattern Java, mais natif au langage.
{...}
Dart est un langage orienté Objet et il supporte aussi les interfaces implicites, les mixins (with), les classes abstraites, exactement comme vous en avez l’habitude côté back-end en Java ou autres.
with
Petit rappel sur les mixins (with) : c’est un moyen de réutiliser du code et des fonctionnalités dans plusieurs classes et hiérarchies, sans utiliser l’héritage classique. Ils permettent d’intégrer des méthodes ou des propriétés dans une classe à l’aide du mot-clé with, un peu comme des interfaces.
Exemple sans mixins :
class Personne { final String nom; final int age; // Constructeur avec syntaxe raccourcie (équivalent d'un constructeur Lombok @AllArgsConstructor) Personne(this.nom, this.age); // fonction flèchée qui retourne une chaîne de caractères String seDecrire() => "$nom a $age ans"; } // classe étudiant qui hérite de Personne class Etudiant extends Personne { final String ecole; // constructeur qui reçoit 3 arguments et appelle celui de la classe mère en passant 2 arguments. Etudiant(String nom, int age, this.ecole) : super(nom, age); @override String seDecrire() => "${super.seDecrire()}, étudie à $ecole"; } void main() { final etudiant = Etudiant("Pierre", 21, "ESIEA"); print(e.seDecrire()); }
Avec Mixins :
// déclaration du mixin mixin Volant { void voler() { print("Je vole dans les airs !"); } } // Classe qui utilise le mixin class Canard with Volant { void emettreSon() { print("Coin coin"); } } void main() { var donald = Canard(); donald.voler(); // Méthode venant du mixin non redéfinie donald.emettreSon(); // méthode dans Canard }
Future
async/await
En Dart (et Flutter), Future et async/await servent à gérer les opérations asynchrones, c’est-à-dire les tâches qui prennent du temps (comme charger une image, lire un fichier ou appeler une API) sans bloquer l’application pendant l’attente. Ce mécanisme est fondamental !
Chaque appel à une API REST Spring Boot ou Php (Jour 4) se fera avec Future + async/await, exactement comme un @Async côté Spring. Si vous avez déjà manipulé CompletableFuture en Java, Future<T> en Dart est son équivalent direct.
@Async
CompletableFuture
Future<T>
Future<String> recupererDonnees() async { await Future.delayed(const Duration(seconds: 2)); // simule un appel réseau return "Données reçues"; } void main() async { print("Début"); String resultat = await recupererDonnees(); print(resultat); print("Fin"); }
Quelques explications sur le code :
Future :
Future<String>
async :
await :
Imaginions que vous écriviez var resultat = recupererDonnees(); mais sans le await, le programme n’attendrait pas ! La variable resultat ne contiendraient pas le texte, mais une instance de Future<String> (ticket de commande) et le programme afficherait Fin avant même que les données ne soient arrivées.
var resultat = recupererDonnees();
await
Nous pourrions compléter notre code avec un try/catch pour gérer les erreurs !
try/catch
Ici, pas de difficulté particulière.
// collection de chaînes List<String> fruits = ["Pomme", "Banane", "Kiwi"]; fruits.add("Mangue"); // collection clef/valeur Map<String, int> stock = {"Pomme": 10, "Banane": 5}; stock["Kiwi"] = 3; // et l'équivalent des Streams en Java List<String> fruitsMajuscules = fruits .where((f) => f.length > 4) .map((f) => f.toUpperCase()) .toList(); print(fruitsMajuscules); // [POMME, BANANE, MANGUE] sauf le kiwi qui fait 4 caractères !
Retour sur le point d’entrée en Flutter qui est toujours la fonction main() dans lib/main.dart :
main()
lib/main.dart
runApp()
SpringApplication.run(...)
MaterialApp
@Configuration
BuildContext context
import 'package:flutter/material.dart'; // méthode main() void main() { runApp(const MonApplication()); } // classe principale de l'application class MonApplication extends StatelessWidget { const MonApplication({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Mon Application de Démonstration Flutter', theme: ThemeData( primarySwatch: Colors.blue, ), home: const PageAccueil(), ); } } // page d'accueil class PageAccueil extends StatelessWidget { const PageAccueil({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Mon Application"), backgroundColor: Colors.blue, foregroundColor: Colors.white, ), drawer: Drawer( child: ListView( padding: EdgeInsets.zero, children: const [ DrawerHeader( decoration: BoxDecoration( color: Colors.blue, ), child: Text( "Menu", style: TextStyle( color: Colors.white, fontSize: 24, ), ), ), ListTile( leading: Icon(Icons.home), title: Text("Accueil"), ), ], ), ), body: const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ FlutterLogo( size: 140, ), SizedBox(height: 32), Text( "Bienvenue sur la page\nd'accueil", textAlign: TextAlign.center, style: TextStyle( fontSize: 28, fontWeight: FontWeight.bold, ), ), SizedBox(height: 16), Text( "Application Flutter de démonstration", style: TextStyle( fontSize: 18, color: Colors.grey, ), ), ], ), ), ); } }
Ce que le code ci-dessus devrait afficher :
super.key
Dans le code ci-dessus, vous voyez que notre application hérite d’un StatelessWidget et que l’on fait fait passer une key dans le constructeur de la super classe. C’est une syntaxe qui existe depuis Dart 2.17+. Tout widget Flutter hérite de Widget, dont le constructeur exige un paramètre nommé optionnel.
key
Ce key est un identifiant que le framework utilise pour reconnaître un widget lors des reconstructions. C’est le mécanisme qui permet à Flutter de faire la différence entre deux widgets de même type dans une liste, ou de préserver l’état d’un StatefulWidget quand il se déplace dans l’arbre. Vous n’avez presque jamais besoin d’en fournir un vous-même. En fait, ce paramètre est optionnel.
super.keypeut se traduire par “je passe la main sur le paramètre key à ma classe parente Widget, sans code intermédiaire”. Vous n’avez rien à en faire dans 99 % des cas. c’est de la plomberie standard que Flutter exige pour ses widgets !
En Flutter, absolument tout est un widget : un texte, une image, une marge, un espacement, une animation, l’écran entier. C’est le concept central du framework. En règle générale, on redéfinit la méthode build() qui retourne un objet de type Widget. build décrit à quoi le Widget ressemble en fonction de son état actuel.
build()
Widget
build
Important : Flutter reconstruit uniquement les parties de l’arbre qui ont changé et c’est ce qui rend le framework rapide.
Dans le code, vous voyez que Scaffold signifie littéralement “échafaudage” en anglais. En Flutter, c’est le widget qui fournit la structure de base d’une page (un écran) selon les guidelines Material Design de Google.
Scaffold
Concrètement, un écran d’application Material type a toujours les mêmes ingrédients :
Scaffold est donc la cuisine équipée : il fournit tous les emplacements prêts à l’emploi. Un widget “nu” comme un Column serait la cuisine vide et vous devriez tout construire vous-même !
Column
// notre classe qui hérite de StatelessWidget class PageAccueil extends StatelessWidget { const PageAccueil({super.key}); @override Widget build(BuildContext context) { return Scaffold( // structure de page Material appBar: AppBar(title: const Text("Accueil")), // ajout d'une barre en haut avec titre de l'écran // zone centrale : contenu métier body: Center( // centre les enfants dans l'écran child: Column( // les empile verticalement mainAxisAlignment: MainAxisAlignment.center, // et les centre verticalement children: const [ Icon(Icons.flutter_dash, size: 80, color: Colors.blue), SizedBox(height: 16), // on ajoute un espace de 16 pixels Text("Bienvenue sur Flutter !", style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold), ), ], // le children ), ), ); // fin du Scaffold } // fin de la méthode build() }
Analogie Spring Boot : pensez à build() comme à une méthode toString() déclarative qui renvoie la représentation visuelle de l’objet, réévaluée à chaque changement d’état, un peu comme un moteur de template (Thymeleaf) qui se re-render automatiquement.
toString()
Attention : Remarque sur le code Text("Bienvenue sur Flutter !", style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold) , ), -> On met une virgule finale car Dart a un outil officiel de mise en forme automatique (dart format intégré à VS Code à la sauvegarde). Il utilise la présence ou l’absence d’une virgule finale pour décider comment découper les appels sur plusieurs lignes.
Text("Bienvenue sur Flutter !", style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold) , ),
dart format
C’est LA distinction fondamentale de cette journée de découverte ! Souvenez-vous que stateless signifie sans état et stateful avec état.
stateless
stateful
Tableau de comparaison pour illustrer cette notion :
StatelessWidget
StatefulWidget
@Service
setState()
Pour voir la différence, modifier l’héritage de PageCompteur en mettant un StatelessWidget ! En fait, vous ne pourrez pas appeler une méthode setState() qui n’existe que grâce à la classe mère StatefulWidget.
class PageCompteur extends StatefulWidget { const PageCompteur({super.key}); @override State<PageCompteur> createState() => _PageCompteurState(); // ici on fabrique l'état grâce à createState() } class _PageCompteurState extends State<PageCompteur> { int _compteur = 0; // état : une variable mutable, vit ici et survit aux rebuilds ! // fonction qui sera appelée lorsque l'internaute va "tapper" sur le bouton ou "appuyer" (onPressed) void _incrementer() { setState(() { // demande explicite : il faut Toujours passer par setState() pour notifier la valeur de l'objet _compteur++; }); } @override Widget build(BuildContext context) { // le build est relancé à chaque appel à la méhtode setState() return Scaffold( appBar: AppBar(title: const Text("Compteur")), body: Center( child: Text("Valeur : $_compteur", style: const TextStyle(fontSize: 30)), // affiche la donnée courante du compteur ), floatingActionButton: FloatingActionButton( onPressed: _incrementer, child: const Icon(Icons.add), ), ); } }
Voici ce que ce code ci-dessus permettrait d’afficher et de réaliser :
setState() indique à Flutter : « mon état a changé, merci de rappeler build() pour rafraîchir l’écran ». C’est le mécanisme le plus simple de gestion d’état (nous verrons des alternatives plus robustes au Jour 3 avec Provider).
Provider
On pourrait aussi écrire la dernière partie du code de cette façon comme dans l’exemple de hello_flutter, mais c’est moins joli !
@override Widget build(BuildContext context) { // relancé à chaque setState() return Column( children: [ Text('Valeur : $_compteur'), // affiche la donnée courante ElevatedButton( onPressed: _incrementer, child: const Text('+1'), ), ], ); }
Créez un nouveau projet flutter create jour1_exercices et faites les exercices dans des fichiers séparés sous lib/, en les important tour à tour dans main.dart pour les tester.
flutter create jour1_exercices
Écrivez, dans un fichier dart_exercices.dart exécutable avec dart run, les éléments suivants :
dart_exercices.dart
dart run
estPair(int n)
bool
Livre
titre
auteur
anneePublication
resume()
for
Voici une liste de livres pour vous aider :
// 2. liste de livres final livres = [ Livre("1984", "George Orwell", 1949), Livre("Dune", "Frank Herbert", 1965), Livre("Sapiens", "Yuval Noah Harari", 2011), Livre("Fondation", "Isaac Asimov", 1951), Livre("Clean Code", "Robert C. Martin", 2008), ];
En partant du code du Compteur fournit dans ce cours :
Pour vous aider, il vous reste à écrire le code :
Créez un StatelessWidget nommé CarteInfo qui affiche, dans un Container avec une bordure arrondie et une ombre légère, une icône, un titre et une description passés en paramètres du constructeur. Aidez-vous de la documentation sur Dart et Flutter en ligne.
CarteInfo
Container
Et pour les plus courageux ou courageuses, le site officiel des packages Dart et Flutter
Toujours pour vous aider :
Attendez avant de consulter les corrigés complets et commentés de ces 3 exercices : Corrigés Jour 1
Vous l’avez certainement déjà fait avec des pages web traditionnelles…
Cahier des charges :
Créez une application carte_visite à écran unique représentant votre carte de visite professionnelle :
carte_visite
CircleAvatar
Row
Icons.email
Icons.phone
Icons.location_on
SnackBar
GestureDetector.onLongPress
Testez votre application sur votre téléphone physique avant de passer à la journée 2 !
Pour vous aider :
Voir le corrigé complet du TP
// Importation des composants Material Design de Flutter. import 'package:flutter/material.dart'; // Point d'entrée de l'application Dart comme le void main() dans Java void main() { // Lance Flutter en utilisant MyApp comme widget racine. runApp(const MyApp()); } // Widget principal de l'application. // StatelessWidget car MyApp ne possède pas d'état mutable class MyApp extends StatelessWidget { // Constructeur const MyApp({super.key}); // build() décrit l'interface produite par ce widget @override Widget build(BuildContext context) { // Configuration générale de l'application Material. return MaterialApp( // Nom logique de l'application title: 'Flutter Demo', // Thème graphique global theme: ThemeData( // Création automatique d'une palette depuis une couleur. colorScheme: ColorScheme.fromSeed( seedColor: Colors.deepPurple, ), ), // Premier écran affiché. home: const MyHomePage( title: 'Flutter Demo Home Page', ), ); } } // Écran possédant un état modifiable class MyHomePage extends StatefulWidget { const MyHomePage({ super.key, required this.title, }); // titre immuable reçu par le constructeur. final String title; // Création de l'objet qui contiendra l'état @override State<MyHomePage> createState() { return _MyHomePageState(); } } // État associé à MyHomePage. class _MyHomePageState extends State<MyHomePage> { // Variable d'état. int _counter = 0; // Méthode appelée lorsque l'utilisateur appuie sur + void _incrementCounter() { // Indique à Flutter que l'état va changer setState(() { // Incrémente le compteur _counter++; }); } // Construction de l'écran. @override Widget build(BuildContext context) { // Structure générale d'un écran Material return Scaffold( // Barre supérieure appBar: AppBar( // Couleur provenant du thème. backgroundColor: Theme.of(context).colorScheme.inversePrimary, // widget permet d'accéder au StatefulWidget associé. title: Text(widget.title), ), // Contenu principal de l'écran. body: Center( // Organisation verticale. child: Column( // Centre verticalement les enfants. mainAxisAlignment: MainAxisAlignment.center, // Widgets contenus dans la colonne. children: [ // Texte fixe. const Text( 'You have pushed the button this many times:', ), // Valeur actuelle du compteur. Text( '$_counter', // Style provenant du thème. style: Theme.of(context).textTheme.headlineMedium, ), ], ), ), // Bouton flottant en bas à droite. floatingActionButton: FloatingActionButton( // Fonction exécutée au clic. onPressed: _incrementCounter, // Description du bouton. tooltip: 'Increment', // Icône +. child: const Icon(Icons.add), ), ); } }
➡️ Chapitre 2 — Widgets, mise en page et navigation
⬅️ Retour au sommaire Flutter