Flutter
Du Dart aux applications multiplateforme — widgets, layouts, navigation, état, API REST et stockage. Pour débutants qui connaissent déjà Python ou JavaScript.
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.
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.
| Flutter | React (équivalent) | Tkinter (équivalent) |
|---|---|---|
Widget | Composant | Widget/Frame |
StatelessWidget | Functional component | Frame statique |
StatefulWidget | Component avec useState | Frame avec variables |
setState() | setState() | variable + update() |
Column/Row | flexbox vertical/horizontal | pack/grid |
Navigator | React Router | — |
Installation & premier projet
# 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 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)
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 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.
// 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
// 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
// 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)
// 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 = 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 = 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),
);
},
)
Widgets essentiels
Gestion de l'état
ChangeNotifier + Consumer.// 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(),
)
// 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
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.
// 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
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
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}');
}
}
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.
# 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
# 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.
- Créer
flutter create meteo_app - Organiser
lib/en dossiers :models/,screens/,services/,widgets/ - Ajouter
http,provider,shared_preferencesdanspubspec.yaml - Créer le modèle
MeteoavecfromJson()
- 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
- Scaffold + AppBar + formulaire de recherche
- FutureBuilder pour afficher la météo
- Icône météo selon la condition
- Température, humidité, vent
- Créer
FavorisModel extends ChangeNotifier - Bouton étoile pour ajouter/retirer
- Écran de liste des favoris
- Persister avec SharedPreferences
- 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)
flutter build apk→ Androidflutter build web→ navigateur- Tester sur émulateur + appareil réel
- Vérifier les permissions réseau
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
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
Cheat Sheet Flutter
⌨️ Commandes essentielles
flutter create app | Créer un projet |
flutter run | Lancer sur émulateur |
flutter run -d chrome | Lancer dans Chrome |
flutter pub get | Installer dépendances |
flutter pub add http | Ajouter une lib |
flutter build apk | Build Android |
flutter build web | Build Web |
flutter doctor | Diagnostiquer l'install |
🎯 Dart rapide
var x = 5 | Type inféré |
String? s | Nullable |
s?.toUpper() | Appel conditionnel |
s ?? 'défaut' | Valeur si null |
final / const | Immutable / compile-time |
async / await | Asynchrone |
=> | Fonction flèche |
'$var / ${expr}' | Interpolation |
📐 Layout
Column | Vertical (flex-col) |
Row | Horizontal (flex-row) |
Stack | Superposé (absolute) |
Expanded | Remplit l'espace dispo |
SizedBox | Espace fixe / taille fixe |
Padding | Marge intérieure |
Center | Centrer un child |
ListView.builder | Liste 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 |
BottomNavigationBar | Onglets en bas |
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