⬅️ Retour au sommaire Flutter
L’application affiche un écran contenant :
Lorsqu’on appuie sur le bouton + en bas à droite, cela incrémente le compteur.
+
Voici le mécanisme général que nous allons aborder pour bien comprendre le fonctionnement d’une application Flutter :
main() ↓ runApp() ↓ MyApp ↓ MaterialApp ↓ MyHomePage ↓ _MyHomePageState ↓ Scaffold ├── AppBar ├── Body │ └── compteur └── FloatingActionButton ↓ _incrementCounter() ↓ setState() ↓ build() ↓ écran actualisé
C’est déjà presque tout le fonctionnement fondamental de Flutter.
import 'package:flutter/material.dart';
Cette ligne importe la bibliothèque Material Design de Flutter.
Elle donne accès à des classes comme :
MaterialApp Scaffold AppBar Text Column Center FloatingActionButton Icon Colors ThemeData
Sans cet import, Dart ne connaîtrait pas ces classes.
Pour tes étudiants Java, tu peux faire le parallèle avec :
import org.springframework.stereotype.Controller;
ou :
import java.util.List;
En Flutter :
signifie en quelque sorte :
« Je veux utiliser les composants graphiques Material fournis par Flutter. »
void main() {
main() est le point d’entrée du programme Dart.
main()
C’est la première fonction exécutée lorsque l’application démarre.
Équivalent Java :
public static void main(String[] args) { }
Ici :
void
signifie que la fonction ne retourne rien.
Donc :
void main()
peut être lu :
Fonction principale de l’application, ne renvoyant aucune valeur.
runApp()
runApp(const MyApp());
runApp() demande à Flutter :
« Affiche ce widget comme racine de mon application. »
On lui donne :
MyApp()
Donc l’arbre Flutter commence par :
runApp ↓ MyApp
runApp() attend un objet de type :
Widget
C’est une notion fondamentale :
En Flutter, presque tout ce qui apparaît à l’écran est un Widget.
const
const MyApp()
const indique que l’objet peut être créé comme une constante immuable.
Flutter peut donc optimiser sa création et éviter certaines reconstructions inutiles.
Tu peux dire aux étudiants :
const = "cet objet ne changera pas"
On retrouvera souvent :
const Text('Bonjour')
plutôt que :
Text('Bonjour')
lorsque le widget ne dépend pas de données changeantes.
MyApp
class MyApp extends StatelessWidget {
On crée une classe :
qui hérite de :
StatelessWidget
Un StatelessWidget est un widget sans état mutable propre.
Autrement dit :
StatelessWidget = widget dont les données ne changent pas pendant son existence
Exemple :
class Logo extends StatelessWidget
si le logo reste toujours identique.
À opposer à :
StatefulWidget
qui peut avoir un état qui évolue.
const MyApp({super.key});
C’est le constructeur de MyApp.
Version développée conceptuellement :
const MyApp({ Key? key, }) : super(key: key);
La syntaxe moderne :
{super.key}
est simplement plus courte.
key
Une Key permet à Flutter d’identifier précisément un widget dans l’arbre.
Key
Imaginons :
Liste ├── Alice ├── Bob └── Charlie
Puis :
Liste ├── Charlie ├── Alice └── Bob
Les Key aident Flutter à comprendre :
« Ce widget est toujours le même objet logique, il a simplement changé de position. »
Pour une première séance, tu peux dire :
key sert à aider Flutter à identifier les widgets, notamment lorsqu’ils bougent ou sont reconstruits.
Pas besoin d’aller beaucoup plus loin immédiatement.
@override
Ton texte contient :
@Override
La vraie syntaxe Dart est :
C’est une annotation indiquant :
« Cette méthode redéfinit une méthode héritée de la classe parente. »
Exactement comme en Java :
@Override public String toString() { }
Ici, MyApp hérite de StatelessWidget et redéfinit :
build()
Widget build(BuildContext context) {
C’est une des lignes les plus importantes du programme.
build() doit retourner un :
qui décrit l’interface graphique.
Tu peux presque traduire :
Widget build(BuildContext context)
par :
« Construis-moi l’interface de ce widget dans ce contexte. »
BuildContext
BuildContext context
context représente la position du widget dans l’arbre des widgets.
context
Imagine :
MaterialApp ↓ Scaffold ↓ Column ↓ Text
Chaque widget se trouve quelque part dans cet arbre.
BuildContext permet notamment de rechercher des informations autour du widget :
Theme.of(context)
Navigator.of(context)
ou encore :
MediaQuery.of(context)
Tu peux dire :
Le BuildContext est l’adresse du widget dans l’arbre Flutter.
C’est simplifié, mais très pédagogique.
return MaterialApp
return MaterialApp(
build() retourne ici un widget :
MaterialApp
MaterialApp représente la configuration générale d’une application utilisant Material Design.
Il peut gérer notamment :
thème navigation langues routes titre mode clair/sombre
C’est souvent l’un des widgets les plus hauts de l’application.
title: 'Flutter Demo',
On passe une propriété nommée :
title
à MaterialApp.
En Dart, les paramètres nommés sont extrêmement fréquents :
MaterialApp( title: 'Flutter Demo', theme: ..., home: ..., )
C’est beaucoup plus lisible que :
MaterialApp('Flutter Demo', ..., ...)
theme: ThemeData(
ThemeData définit le thème graphique général de l’application.
ThemeData
Par exemple :
couleurs polices boutons champs barres cartes
Cela évite de définir les couleurs individuellement partout.
colorScheme
colorScheme: .fromSeed(
Cette syntaxe peut surprendre.
Elle est équivalente à :
colorScheme: ColorScheme.fromSeed(
Les versions récentes de Dart permettent cette syntaxe abrégée avec point lorsque Dart connaît déjà le type attendu.
Pour enseigner au début, je montrerais d’abord la version explicite :
colorScheme: ColorScheme.fromSeed( seedColor: Colors.deepPurple, ),
Elle est plus facile à comprendre.
Puis montrer :
comme raccourci moderne.
seedColor
seedColor: Colors.deepPurple
On donne une couleur de base :
Colors.deepPurple
Flutter va utiliser cette couleur pour générer automatiquement une palette cohérente.
deepPurple ↓ couleur primaire couleur secondaire couleur de surface variantes claires variantes sombres
C’est lié à Material Design 3.
home: const MyHomePage( title: 'Flutter Demo Home Page', ),
home indique quel widget constitue l’écran principal de l’application.
home
MaterialApp ↓ home ↓ MyHomePage
On crée :
MyHomePage
en lui transmettant :
title: 'Flutter Demo Home Page'
class MyHomePage extends StatefulWidget {
Cette fois, on utilise :
Pourquoi ?
Parce que le compteur va changer :
0 1 2 3 ...
L’écran possède donc un état mutable.
C’est une distinction fondamentale :
const MyHomePage({ super.key, required this.title, });
Le constructeur reçoit deux éléments :
super.key
et :
required this.title
required signifie :
required
Ce paramètre est obligatoire.
Ainsi ceci fonctionne :
MyHomePage( title: 'Accueil', )
mais pas :
MyHomePage()
car title est obligatoire.
final String title;
On déclare une propriété :
de type :
String
et marquée :
final
Cela signifie qu’une fois affectée, sa référence ne peut plus changer.
final String title = 'Accueil';
On ne pourra pas ensuite écrire :
title = 'Autre chose';
Les widgets Flutter sont généralement conçus pour être immutables.
createState()
@override State<MyHomePage> createState() => _MyHomePageState();
C’est probablement la partie la plus étrange pour un débutant.
Un StatefulWidget est séparé en deux objets :
MyHomePage │ └── configuration immuable _MyHomePageState │ └── état mutable
createState() demande :
Quel objet doit gérer l’état de MyHomePage ?
La réponse est :
_MyHomePageState()
La notation :
=>
est une fonction courte.
Ceci :
State<MyHomePage> createState() => _MyHomePageState();
équivaut à :
State<MyHomePage> createState() { return _MyHomePageState(); }
Très utile à expliquer aux étudiants.
_MyHomePageState
_
En Dart, un identifiant commençant par _ est privé à la bibliothèque.
_counter _incrementCounter _MyHomePageState
sont considérés privés.
Dart n’utilise donc pas :
private
comme Java.
Java :
private int counter;
Dart :
int _counter;
class _MyHomePageState extends State<MyHomePage> {
Cette classe contient l’état mutable associé à :
C’est ici que nous allons stocker :
_counter
et les méthodes qui modifient cet état.
int _counter = 0;
On crée une variable entière :
initialisée à :
0
Comme elle commence par _, elle est privée.
On peut comparer avec Java :
private int counter = 0;
void _incrementCounter() {
On définit une méthode privée :
_incrementCounter
qui ne retourne rien :
Elle sera exécutée lorsque l’utilisateur appuie sur le bouton +.
setState()
setState(() {
C’est le cœur de cet exemple.
setState() signifie en pratique :
« Je vais modifier l’état. Flutter doit ensuite reconstruire l’interface concernée. »
Le mécanisme est :
clic ↓ _incrementCounter() ↓ setState() ↓ _counter change ↓ Flutter appelle build() ↓ nouveau Text ↓ écran mis à jour
_counter++;
C’est équivalent à :
_counter = _counter + 1;
_counter += 1;
0 → 1 1 → 2 2 → 3
Si on écrivait seulement :
la valeur en mémoire pourrait changer, mais Flutter ne saurait pas nécessairement qu’il doit reconstruire l’écran.
Avec :
setState(() { _counter++; });
on dit explicitement :
L’état a changé, mets à jour l’interface.
C’est une notion fondamentale avant d’introduire plus tard :
Provider Riverpod Bloc ChangeNotifier
@override Widget build(BuildContext context) {
Cette fois, il s’agit du build() de :
Il construit tout l’écran principal.
Scaffold
return Scaffold(
Scaffold signifie littéralement échafaudage.
C’est la structure standard d’un écran Material.
Il prévoit des emplacements pour :
Scaffold ├── appBar ├── body ├── drawer ├── bottomNavigationBar ├── floatingActionButton └── ...
Dans notre application :
Scaffold ├── AppBar ├── Body └── FloatingActionButton
Très utile comme représentation pédagogique.
appBar
appBar: AppBar(
On crée la barre située en haut de l’écran.
┌──────────────────────┐ │ Flutter Demo Home... │ ← AppBar ├──────────────────────┤
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
Cette ligne récupère le thème actuel.
Décomposons :
signifie :
Cherche le Theme disponible autour de ce widget.
Theme
.colorScheme
récupère la palette.
.inversePrimary
récupère une couleur particulière de cette palette.
context ↓ Theme ↓ colorScheme ↓ inversePrimary
title: Text(widget.title),
On affiche un widget Text.
Text
Mais pourquoi :
widget.title
et pas simplement :
Parce que nous sommes dans :
alors que title appartient à :
Flutter met à disposition :
widget
qui permet d’accéder au StatefulWidget associé.
Donne-moi la propriété title du MyHomePage associé à cet état.
body: Center(
body représente le contenu principal de l’écran.
body
Center centre son enfant horizontalement et verticalement.
Center
Structure :
body ↓ Center ↓ Column
child
child: Column(
Beaucoup de widgets Flutter possèdent :
lorsqu’ils n’acceptent qu’un seul enfant.
Center( child: Text('Bonjour'), )
Quand ils acceptent plusieurs enfants, on trouve généralement :
children
Column
Column(
Column dispose ses enfants verticalement.
Text 1 Text 2 Bouton Image
À l’inverse :
Row
les place horizontalement :
Text 1 Text 2 Bouton
mainAxisAlignment
mainAxisAlignment: .center,
Pour une Column, l’axe principal est vertical.
mainAxisAlignment: MainAxisAlignment.center
centre les enfants verticalement.
Encore une fois :
.center
est la syntaxe abrégée moderne.
La version plus explicite est :
mainAxisAlignment: MainAxisAlignment.center,
Je recommande cette forme au début de la formation.
children: [
Column peut contenir plusieurs widgets.
On lui fournit donc une liste Dart :
[ widget1, widget2, widget3, ]
Ici elle contient deux widgets Text.
const Text( 'You have pushed the button this many times:', ),
C’est un texte fixe.
Comme il ne changera jamais :
est pertinent.
Text( '$_counter',
Ici nous faisons de l’interpolation de chaîne.
Si :
_counter == 5
alors :
'$_counter'
devient :
5
Autre exemple :
String nom = 'Alice'; Text('Bonjour $nom')
donne :
Bonjour Alice
style: Theme.of(context).textTheme.headlineMedium,
On récupère à nouveau le thème :
puis :
textTheme
headlineMedium
Le compteur est donc affiché avec une typographie importante.
floatingActionButton: FloatingActionButton(
FloatingActionButton correspond au fameux bouton rond Material :
FloatingActionButton
[+]
souvent placé en bas à droite.
onPressed
onPressed: _incrementCounter,
C’est extrêmement important.
On indique quelle fonction appeler lorsque l’utilisateur appuie sur le bouton.
Utilisateur ↓ clic + ↓ onPressed ↓ _incrementCounter
Remarque importante :
et non :
onPressed: _incrementCounter(),
Parce qu’on transmet la référence vers la fonction, on ne l’exécute pas immédiatement.
C’est une excellente question de QCM.
tooltip
tooltip: 'Increment',
C’est un texte descriptif associé au bouton.
Il peut notamment être utile pour :
accessibilité aide utilisateur appui long technologies d'assistance
Vu ton intérêt pour l’accessibilité, c’est un bon moment pour montrer que Flutter intègre déjà certaines informations sémantiques.
child: const Icon(Icons.add),
Le bouton contient un widget :
Icon
qui utilise :
Icons.add
c’est-à-dire l’icône :
Icons fournit énormément d’icônes Material :
Icons
Icons.add Icons.delete Icons.edit Icons.home Icons.search Icons.settings
Je montrerais absolument ceci aux étudiants :
MyApp │ └── MaterialApp │ └── MyHomePage │ └── Scaffold │ ├── AppBar │ └── Text │ ├── Center │ └── Column │ ├── Text │ └── Text │ └── FloatingActionButton └── Icon
C’est probablement la représentation la plus importante de la première journée Flutter.
Une interface Flutter n’est pas conçue comme une page HTML.
Elle est composée d’un arbre d’objets Widget.
Je terminerais l’explication aux étudiants avec ces trois concepts.
Application ↓ Widget ↓ Widget ↓ Widget
Même :
texte bouton alignement marge page barre icône
sont généralement des widgets.
En Flutter, on ne dit pas :
Créer un texte le placer en X=40 le déplacer changer sa valeur
On décrit plutôt :
Text('$_counter')
Flutter se charge de rendre l’interface correspondant à l’état actuel.
Conceptuellement :
UI = f(état)
C’est une formule très importante :
L’interface est une fonction de l’état.
counter = 0
Flutter construit :
counter = 5
clic ↓ état modifié ↓ setState() ↓ build() ↓ nouvelle interface
C’est le mécanisme central de ce premier exemple.
Je réécrirais le fichier ainsi pour le cours :
// Import des composants Material Design de Flutter. import 'package:flutter/material.dart'; // Point d'entrée de l'application Dart. 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 constant. 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), ), ); } }
Cette version est, à mon avis, meilleure pédagogiquement que le fichier généré automatiquement, notamment parce que j’ai remplacé les raccourcis récents :
.fromSeed .center
par leurs formes explicites :
ColorScheme.fromSeed MainAxisAlignment.center
Pour une première journée Flutter, elles permettent aux étudiants de mieux comprendre le type auquel appartient chaque valeur avant de leur montrer les raccourcis syntaxiques modernes.