Mobile · Web · Desktop

Flutter

Du Dart aux applications multiplateforme — widgets, layouts, navigation, état, API REST et stockage. Pour débutants qui connaissent déjà Python ou JavaScript.

🤖 Android 🍎 iOS 🌐 Web 🖥️ Desktop

Flutter & Dart — c'est quoi ?

🐦

Flutter est un framework créé par Google (2018) qui permet de développer des applications Android, iOS, Web et Desktop à partir d'un seul codebase. Il utilise le langage Dart et dessine lui-même tous ses composants visuels (pas de composants natifs) — ce qui garantit un rendu identique sur toutes les plateformes.

Un seul code
Un projet → Android + iOS + Web + Linux/Windows/macOS. Pas de React Native (JS bridge), Flutter compile en code natif.
🎨
Tout est widget
L'UI est construite en assemblant des widgets imbriqués — comme des boîtes dans des boîtes. Chaque bouton, texte, icône, couleur de fond = un widget.
🔄

Hot Reload — la fonctionnalité killer de Flutter : modifier le code → l'app se met à jour en moins d'une seconde sans perdre l'état. Le développement devient très rapide.

MyApp ← point d'entrée (runApp) └── MaterialApp ← thème, navigation globale └── Scaffold ← structure de l'écran ├── AppBar ← barre du haut │ └── Text "Mon App" └── Column ← layout vertical ├── Image ← image ├── Text "Bienvenue" └── ElevatedButton └── Text "Cliquer"
FlutterReact (équivalent)Tkinter (équivalent)
WidgetComposantWidget/Frame
StatelessWidgetFunctional componentFrame statique
StatefulWidgetComponent avec useStateFrame avec variables
setState()setState()variable + update()
Column/Rowflexbox vertical/horizontalpack/grid
NavigatorReact Router

Installation & premier projet

Installation Flutter (Windows)
# 1. Télécharger Flutter SDK
# https://docs.flutter.dev/get-started/install/windows
# Extraire dans C:\flutter (pas dans Program Files)

# 2. Ajouter au PATH : C:\flutter\bin

# 3. Vérifier l'installation
flutter doctor

# 4. Installer Android Studio + Android SDK
# https://developer.android.com/studio
# Puis dans Android Studio : SDK Manager → install SDK 34+
# Virtual Device Manager → créer un émulateur Pixel

# 5. Accepter les licences Android
flutter doctor --android-licenses

# Résultat attendu de flutter doctor :
# [✓] Flutter (Channel stable)
# [✓] Android toolchain
# [✓] Android Studio
# [✓] VS Code (extension Flutter installée)
Créer et lancer un projet
# Créer un nouveau projet
flutter create mon_app
cd mon_app

# Structure du projet
# mon_app/
# ├── lib/
# │   └── main.dart       ← TON CODE ici
# ├── android/            ← config Android
# ├── ios/                ← config iOS
# ├── web/                ← config Web
# ├── pubspec.yaml        ← dépendances (= requirements.txt)
# └── test/               ← tests

# Lancer sur émulateur Android
flutter run

# Lancer dans le navigateur
flutter run -d chrome

# Lancer sur tous les appareils connectés
flutter run -d all

# Hot reload : appuyer sur r dans le terminal
# Hot restart : appuyer sur R (perd l'état)
main.dart — application minimale commentée
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());  // point d'entrée — comme if __name__ == "__main__" en Python
}

// Widget racine — configure le thème global
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Mon App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

// Écran principal
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Accueil')),
      body: const Center(
        child: Text('Hello Flutter!', style: TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => print('cliqué'),
        child: const Icon(Icons.add),
      ),
    );
  }
}

Dart — bases pour débutants Python/JS

Dart
// Variables var nom = 'Alice'; int age = 20; double note = 14.5; bool actif = true; String? ville; // nullable // Constante final pi = 3.14; const max = 100;
Python
# Variables nom = 'Alice' age = 20 note = 14.5 actif = True ville = None # Pas de const builtin pi = 3.14 MAX = 100 # convention
JS/TS
// Variables let nom = 'Alice'; let age = 20; let note = 14.5; let actif = true; let ville = null; const pi = 3.14; const MAX = 100;
🎯

Dart ressemble plus à Java/TypeScript qu'à Python. Les principales différences à retenir : types obligatoires (ou inférés avec var), null safety (le ? indique qu'une variable peut être null), et les accolades partout à la place de l'indentation.

⚠️

final vs const : final = assigné une seule fois (valeur peut être calculée à l'exécution). const = valeur connue à la compilation. Pour les widgets Flutter, préférer const quand possible → meilleures performances.

Dart — fonctions, listes, maps
// Fonction typée
int additionner(int a, int b) {
  return a + b;
}
// Fonction flèche (corps unique)
int doubler(int x) => x * 2;

// Paramètres nommés (très courant en Flutter !)
Widget monWidget({required String titre, int taille = 16}) {
  return Text(titre);
}
// Appel : monWidget(titre: 'Bonjour', taille: 20)

// List (= liste Python)
List<String> fruits = ['pomme', 'banane', 'cerise'];
fruits.add('kiwi');
fruits.where((f) => f.length > 5).toList();  // filter
fruits.map((f) => f.toUpperCase()).toList();    // map

// Map (= dict Python)
Map<String, dynamic> user = {
  'nom': 'Alice',
  'age': 20,
};
print(user['nom']);         // Alice
print(user['email'] ?? '?'); // ?? = valeur si null
Dart — classes & null safety
// Classe Dart
class Etudiant {
  final String nom;
  int age;
  String? email;  // nullable — peut être null

  // Constructeur court
  Etudiant({required this.nom, required this.age, this.email});

  String toString() => 'Etudiant($nom, $age)';
}

var e = Etudiant(nom: 'Alice', age: 20);
print(e.email?.toUpperCase()); // ?. = appel si non-null

// Async / await (équivalent JS)
Future<String> chargerDonnees() async {
  await Future.delayed(Duration(seconds: 1));
  return 'données chargées';
}

// Utilisation
void main() async {
  var result = await chargerDonnees();
  print(result);
}

// String interpolation (comme Python f-strings)
var msg = 'Bonjour ${e.nom}, tu as ${e.age} ans';

StatelessWidget vs StatefulWidget

StatelessWidget — widget sans état interne
// Utiliser quand : l'affichage ne change jamais
// ou dépend uniquement de paramètres reçus de l'extérieur

class CarteEtudiant extends StatelessWidget {
  final String nom;
  final int age;

  const CarteEtudiant({
    super.key,
    required this.nom,
    required this.age,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            Text(nom, style: const TextStyle(
              fontSize: 18, fontWeight: FontWeight.bold,
            )),
            Text('$age ans'),
          ],
        ),
      ),
    );
  }
}

// Utilisation : CarteEtudiant(nom: 'Alice', age: 20)
StatefulWidget — widget avec état interne
// Utiliser quand : l'affichage change en réponse à
// des interactions (clic, saisie, chargement...)

class Compteur extends StatefulWidget {
  const Compteur({super.key});

  @override
  State<Compteur> createState() => _CompteurState();
}

class _CompteurState extends State<Compteur> {
  int _valeur = 0;  // _ = privé par convention

  void _incrementer() {
    setState(() {      // ← OBLIGATOIRE pour déclencher rebuild
      _valeur++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Valeur : $_valeur', fontSize: 24),
        ElevatedButton(
          onPressed: _incrementer,
          child: const Text('+'),
        ),
      ],
    );
  }
}
💡

Règle simple : commencer par StatelessWidget. Si tu réalises que l'affichage doit changer sans recevoir de nouveaux paramètres → passer à StatefulWidget et déplacer les variables dans la classe State.

Layouts — Column, Row, Stack, ListView

Column & Row — alignement et espacement
// Column = arrangement vertical (flexbox column)
Column(
  mainAxisAlignment: MainAxisAlignment.center,   // axe principal = vertical
  crossAxisAlignment: CrossAxisAlignment.start,  // axe transversal = horizontal
  children: [
    const Text('Titre'),
    const SizedBox(height: 8),   // espacement fixe
    const Text('Sous-titre'),
    const Spacer(),               // espace flexible (pousse vers le bas)
    ElevatedButton(onPressed: (){}, child: const Text('OK')),
  ],
)

// Row = arrangement horizontal
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    const Icon(Icons.person),
    Expanded(child: const Text('Alice')), // prend l'espace restant
    const Text('20 ans'),
  ],
)

// Padding et Container
Container(
  padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
  margin: const EdgeInsets.all(8),
  decoration: BoxDecoration(
    color: Colors.blue.shade100,
    borderRadius: BorderRadius.circular(12),
  ),
  child: const Text('Contenu'),
)
Stack & ListView
// Stack = superposition (comme position: absolute en CSS)
Stack(
  children: [
    Image.network('https://example.com/fond.jpg'),
    Positioned(          // positionné dans le Stack
      bottom: 16,
      left: 16,
      child: const Text('Sur l\'image',
        style: TextStyle(color: Colors.white, fontSize: 20)),
    ),
  ],
)

// ListView — liste scrollable (remplace Column pour les longues listes)
ListView(
  children: [
    ListTile(leading: Icon(Icons.person), title: Text('Alice')),
    ListTile(leading: Icon(Icons.person), title: Text('Bob')),
  ],
)

// ListView.builder — efficace pour les listes dynamiques longues
ListView.builder(
  itemCount: etudiants.length,
  itemBuilder: (context, index) {
    final e = etudiants[index];
    return ListTile(
      title: Text(e.nom),
      subtitle: Text('${e.age} ans'),
      trailing: const Icon(Icons.arrow_forward),
      onTap: () => voirDetails(e),
    );
  },
)

Gestion de l'état

setState()
État local — 1 widget
Simple, aucune dépendance. Parfait pour les formulaires, compteurs, animations locales.
Ne partage pas l'état entre écrans. Devient lourd si l'état est complexe.
Provider
État partagé — recommandé débutants
Officiel Flutter. Simple à comprendre. Bien documenté. ChangeNotifier + Consumer.
Verbeux pour les gros projets. Rebuild parfois trop large.
Riverpod
État partagé — recommandé avancé
Plus sûr que Provider. Pas de BuildContext requis. Très populaire en 2024-2025.
Courbe d'apprentissage plus raide. Nouveau paradigme à apprendre.
Provider — partager l'état entre écrans
// pubspec.yaml
# dependencies:
#   provider: ^6.1.0

// 1. Le modèle — étend ChangeNotifier
class PanierModel extends ChangeNotifier {
  final List<String> _articles = [];

  List<String> get articles => _articles;
  int get total => _articles.length;

  void ajouter(String article) {
    _articles.add(article);
    notifyListeners();  // ← déclenche la mise à jour UI
  }

  void supprimer(String article) {
    _articles.remove(article);
    notifyListeners();
  }
}

// 2. Exposer le modèle à l'arbre de widgets
MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (_) => PanierModel()),
  ],
  child: const MyApp(),
)
Provider — lire et modifier depuis n'importe où
// 3. Lire l'état (rebuild quand ça change)
class BadgePanier extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final total = context.watch<PanierModel>().total;
    return Badge(
      label: Text('$total'),
      child: const Icon(Icons.shopping_cart),
    );
  }
}

// 4. Modifier l'état (pas de rebuild)
class BoutonAjout extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {
        // read() = pas de rebuild sur ce widget
        context.read<PanierModel>().ajouter('Produit');
      },
      child: const Text('Ajouter au panier'),
    );
  }
}

// Résumé :
// context.watch<T>() = lire + se réabonner aux mises à jour
// context.read<T>()  = lire sans s'abonner (dans onPressed...)
// context.select<T,R>((m) => m.prop) = watch partiel

Formulaires & validation

Formulaire complet avec validation
class FormulaireScreen extends StatefulWidget {
  const FormulaireScreen({super.key});
  @override
  State<FormulaireScreen> createState() => _FormulaireState();
}

class _FormulaireState extends State<FormulaireScreen> {
  final _formKey = GlobalKey<FormState>();  // ← clé du formulaire
  final _nomCtrl  = TextEditingController();
  final _emailCtrl = TextEditingController();
  bool _conditions = false;

  @override
  void dispose() {
    _nomCtrl.dispose();    // IMPORTANT : libérer la mémoire
    _emailCtrl.dispose();
    super.dispose();
  }

  void _soumettre() {
    if (_formKey.currentState!.validate() && _conditions) {
      print('Nom: ${_nomCtrl.text}, Email: ${_emailCtrl.text}');
      Navigator.pop(context);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Inscription')),
      body: Form(
        key: _formKey,
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: [

            TextFormField(
              controller: _nomCtrl,
              decoration: const InputDecoration(
                labelText: 'Nom complet',
                prefixIcon: Icon(Icons.person),
                border: OutlineInputBorder(),
              ),
              validator: (val) {
                if (val == null || val.isEmpty) return 'Nom requis';
                if (val.length < 2) return 'Trop court';
                return null;  // null = valide
              },
            ),

            const SizedBox(height: 16),

            TextFormField(
              controller: _emailCtrl,
              keyboardType: TextInputType.emailAddress,
              decoration: const InputDecoration(
                labelText: 'Email',
                prefixIcon: Icon(Icons.email),
                border: OutlineInputBorder(),
              ),
              validator: (val) {
                if (val == null || !val.contains('@'))
                  return 'Email invalide';
                return null;
              },
            ),

            CheckboxListTile(
              title: const Text("J'accepte les conditions"),
              value: _conditions,
              onChanged: (v) => setState(() => _conditions = v!),
            ),

            ElevatedButton(
              onPressed: _soumettre,
              child: const Text("S'inscrire"),
            ),
          ],
        ),
      ),
    );
  }
}
🎹

Les TextEditingController doivent toujours être libérés dans dispose(). Oublier cela crée une fuite mémoire — l'app sera de plus en plus lente.

Widgets de saisie utiles
// DropdownButtonFormField
String? _ville;
DropdownButtonFormField<String>(
  value: _ville,
  decoration: const InputDecoration(labelText: 'Ville'),
  items: ['Mons', 'Bruxelles', 'Liège']
      .map((v) => DropdownMenuItem(value: v, child: Text(v)))
      .toList(),
  onChanged: (v) => setState(() => _ville = v),
  validator: (v) => v == null ? 'Choisir une ville' : null,
)

// Mot de passe avec bouton œil
bool _cacheMotDePasse = true;
TextFormField(
  obscureText: _cacheMotDePasse,
  decoration: InputDecoration(
    labelText: 'Mot de passe',
    suffixIcon: IconButton(
      icon: Icon(_cacheMotDePasse ? Icons.visibility : Icons.visibility_off),
      onPressed: () => setState(() => _cacheMotDePasse = !_cacheMotDePasse),
    ),
  ),
)

Appels API REST

pubspec.yaml — ajouter http
# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.0        # requêtes HTTP
  dio: ^5.4.0         # alternatif plus puissant

# Installer les dépendances
flutter pub get
GET — charger des données
import 'package:http/http.dart' as http;
import 'dart:convert';

// Modèle de données
class Meteo {
  final String ville;
  final double temperature;
  final String description;

  Meteo({required this.ville, required this.temperature, required this.description});

  // Désérialisation depuis JSON
  factory Meteo.fromJson(Map<String, dynamic> json) {
    return Meteo(
      ville:       json['name'],
      temperature: json['main']['temp'].toDouble(),
      description: json['weather'][0]['description'],
    );
  }
}

// Appel API
Future<Meteo> fetchMeteo(String ville) async {
  final url = Uri.parse(
    'https://api.openweathermap.org/data/2.5/weather'
    '?q=$ville&appid=CLE&units=metric&lang=fr'
  );

  final response = await http.get(url);

  if (response.statusCode == 200) {
    final data = jsonDecode(response.body);
    return Meteo.fromJson(data);
  } else {
    throw Exception('Erreur ${response.statusCode}');
  }
}
FutureBuilder — afficher les données async
class MeteoWidget extends StatefulWidget {
  @override
  State<MeteoWidget> createState() => _MeteoState();
}

class _MeteoState extends State<MeteoWidget> {
  late Future<Meteo> _meteoFuture;

  @override
  void initState() {
    super.initState();
    _meteoFuture = fetchMeteo('Mons');  // lancer une seule fois
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Meteo>(
      future: _meteoFuture,
      builder: (context, snapshot) {
        // En cours de chargement
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const CircularProgressIndicator();
        }
        // Erreur
        if (snapshot.hasError) {
          return Text('Erreur : ${snapshot.error}');
        }
        // Données disponibles
        final meteo = snapshot.data!;
        return Column(children: [
          Text(meteo.ville, style: const TextStyle(fontSize: 24)),
          Text('${meteo.temperature}°C'),
          Text(meteo.description),
        ]);
      },
    );
  }
}

// POST — envoyer des données
final response = await http.post(
  Uri.parse('https://api.example.com/users'),
  headers: {'Content-Type': 'application/json'},
  body: jsonEncode({'nom': 'Alice', 'email': 'alice@heh.be'}),
);

Stockage local

💾

Deux outils principaux selon le besoin : SharedPreferences pour les petites données simples (préférences, token, dernière valeur), sqflite pour les données structurées en grande quantité qui ont besoin de requêtes.

SharedPreferences — données simples
# pubspec.yaml
# shared_preferences: ^2.2.2

import 'package:shared_preferences/shared_preferences.dart';

// Sauvegarder
Future<void> sauvegarderPrenom(String prenom) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString('prenom', prenom);
}

// Lire
Future<String?> lirePrenom() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getString('prenom');  // null si absent
}

// Supprimer
Future<void> effacerPrenom() async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.remove('prenom');
}

// Types supportés : String, int, double, bool, List<String>
// Cas d'usage : thème clair/sombre, token d'auth, onboarding vu
sqflite — base SQLite locale
# pubspec.yaml
# sqflite: ^2.3.2
# path: ^1.9.0

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class DatabaseHelper {
  static Database? _db;

  static Future<Database> getDatabase() async {
    if (_db != null) return _db!;

    final path = join(await getDatabasesPath(), 'app.db');
    _db = await openDatabase(
      path,
      version: 1,
      onCreate: (db, version) async {
        await db.execute('''
          CREATE TABLE notes(
            id      INTEGER PRIMARY KEY AUTOINCREMENT,
            titre   TEXT NOT NULL,
            contenu TEXT,
            date    TEXT
          )
        ''');
      },
    );
    return _db!;
  }

  // INSERT
  static Future<int> insererNote(Map<String, dynamic> note) async {
    final db = await getDatabase();
    return await db.insert('notes', note);
  }

  // SELECT
  static Future<List<Map>> lireToutesNotes() async {
    final db = await getDatabase();
    return await db.query('notes', orderBy: 'date DESC');
  }

  // DELETE
  static Future<void> supprimerNote(int id) async {
    final db = await getDatabase();
    await db.delete('notes', where: 'id = ?', whereArgs: [id]);
  }
}

Projet fil rouge — Météo App

🌦️

Une application météo complète qui combine tout ce qu'on a vu : Dart, widgets, navigation, état avec Provider, formulaire de recherche, appel API OpenWeatherMap, et sauvegarde des villes favorites avec SharedPreferences.

ÉTAPE 1
Structure du projet
  • Créer flutter create meteo_app
  • Organiser lib/ en dossiers : models/, screens/, services/, widgets/
  • Ajouter http, provider, shared_preferences dans pubspec.yaml
  • Créer le modèle Meteo avec fromJson()
ÉTAPE 2
Service API
  • Créer services/meteo_service.dart
  • Implémenter fetchMeteo(ville)
  • Gérer les erreurs (ville introuvable, pas de réseau)
  • Tester avec main() avant l'UI
ÉTAPE 3
Écran principal
  • Scaffold + AppBar + formulaire de recherche
  • FutureBuilder pour afficher la météo
  • Icône météo selon la condition
  • Température, humidité, vent
ÉTAPE 4
Favoris avec Provider
  • Créer FavorisModel extends ChangeNotifier
  • Bouton étoile pour ajouter/retirer
  • Écran de liste des favoris
  • Persister avec SharedPreferences
ÉTAPE 5
Navigation & UX
  • BottomNavigationBar : Accueil / Favoris / Paramètres
  • Écran Détails avec prévisions 5 jours
  • Thème clair/sombre (SharedPreferences)
  • Gestion offline (message si pas de réseau)
ÉTAPE 6
Build & déploiement
  • flutter build apk → Android
  • flutter build web → navigateur
  • Tester sur émulateur + appareil réel
  • Vérifier les permissions réseau
Structure finale du projet
meteo_app/
├── lib/
│   ├── main.dart               ← MyApp + routes
│   ├── models/
│   │   └── meteo.dart          ← classe Meteo + fromJson
│   ├── services/
│   │   └── meteo_service.dart  ← fetchMeteo()
│   ├── providers/
│   │   └── favoris_provider.dart ← ChangeNotifier
│   ├── screens/
│   │   ├── accueil_screen.dart ← recherche + météo
│   │   ├── favoris_screen.dart ← liste des favoris
│   │   └── detail_screen.dart  ← prévisions 5j
│   └── widgets/
│       ├── meteo_card.dart     ← carte réutilisable
│       └── loading_widget.dart ← spinner centralisé
├── pubspec.yaml
└── test/
    └── meteo_service_test.dart
pubspec.yaml complet
name: meteo_app
description: Application météo Flutter

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  # HTTP
  http: ^1.2.0
  # État global
  provider: ^6.1.0
  # Stockage local
  shared_preferences: ^2.2.2
  # Icônes météo
  flutter_svg: ^2.0.9
  # Formatage des dates
  intl: ^0.19.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0
http requêtes REST
provider état global
shared_prefs stockage
dio HTTP avancé
sqflite SQLite
go_router navigation
riverpod état avancé
intl dates/format

Cheat Sheet Flutter

⌨️ Commandes essentielles

flutter create appCréer un projet
flutter runLancer sur émulateur
flutter run -d chromeLancer dans Chrome
flutter pub getInstaller dépendances
flutter pub add httpAjouter une lib
flutter build apkBuild Android
flutter build webBuild Web
flutter doctorDiagnostiquer l'install

🎯 Dart rapide

var x = 5Type inféré
String? sNullable
s?.toUpper()Appel conditionnel
s ?? 'défaut'Valeur si null
final / constImmutable / compile-time
async / awaitAsynchrone
=>Fonction flèche
'$var / ${expr}'Interpolation

📐 Layout

ColumnVertical (flex-col)
RowHorizontal (flex-row)
StackSuperposé (absolute)
ExpandedRemplit l'espace dispo
SizedBoxEspace fixe / taille fixe
PaddingMarge intérieure
CenterCentrer un child
ListView.builderListe scrollable efficace

🧭 Navigation

Navigator.push()Aller vers écran
Navigator.pop()Retour
Navigator.pushNamed()Route nommée
pushReplacement()Remplacer (sans retour)
await Navigator.push()Attendre retour
BottomNavigationBarOnglets en bas
Hotkeys VS Code — développement Flutter
r          → Hot Reload (dans terminal flutter run)
R          → Hot Restart (perd l'état)
Ctrl+.     → Quick Fix / Wrap with widget
stless     → snippet StatelessWidget complet
stful      → snippet StatefulWidget complet
Ctrl+Shift+P → Flutter: New Project, Flutter: Select Device