Flutter
Kit de développement multiplateforme créé par Google utilisant le language Dart (créé par Google aussi).
Permet de développer en même temps, avec une seule base de code, pour MacOs, Windows, Linux, iPhone, Android, web.... car Dart sera compilé spécifiquement en code natif (via les dossier windows, ios, android...) pour notre cible (dans le dossier "build").
Flutter est à la fois un framework pour créer des UI en Dart et une collection d'outils pour faire tourner notre code sur les autres plateformes.
Attention, on ne peut tester/lancer nos applis MacOs/iOs qu'à partir de MacOs, nos applis Windows que depuis Windows et nos applis Linux que depuis Linux. On peut tester/lancer nos applis Android (via émulateur) et Web de partout.
Installation
https://docs.flutter.dev/install/quick
Globalement :
-installer Git (Flutter utilise lui-même Git, un "flutter upgrade" est en fait un Git pull)
-dézipper le zip de Flutter
-ajouter flutter\sdk\flutter\bin au PATH.
-"flutter doctor" sur une invite de commande doit montrer "flutter" avec une coche verte (mais il n'y a pas encore de plateforme visée, ça sera en rouge).
Premiers pas
Dans n'importe quel dossier :
flutter create ma_belle_app_flutter va créer une quinzaine de fichiers/dossiers.
Pour lancer l'app en visant une cible (
flutter devices pour les voir) :
flutter run -d chrome ou
flutter run -d edge
Concepts
En Dart tous les types sont des classes. Donc toutes les valeurs sont des objets :
var maVar = 1;
print(maVar.isEven);
print(2.isEven);
Tous les types héritent de Object (int hérite de num qui hérite d'object). Un Widget est un Object. Et un Object est une structure de données. Même les "tableaux" [a,b,c] sont en fait des listes.
Widget
En Flutter beaucoup de classes sont des "widgets" (l'appli elle même, les layouts, les boutons, widget "padding", widget pour l'opacité etc), c'est à dire des classes qui extends "widget" (ou ses descendants). On peut néanmoins créer/avoir des classes qui ne sont pas des widgets : les états, les listeners, le BuildContext.... donc "en Flutter TOUT est widget" est juste marketing.
Il est très rapide de détruire et recréer un widget (60 fois par seconde sans problème).
-Les Widgets sont
immuables, ce sont des classes (donc avec méthodes, attributs...).
-Certains sont
visibles, d'autres
invisibles (servent à stocker des données, d'autres widgets, faire du layout...).
-Certains héritent de
StatelessWidget, des widgets pour l'affichage, configurables, avec des enfants... Ne pas y mettre mettre un attribut variable car son instance est recréée sans pitié (genre
var maBelleImg="1.png" puis
Image.asset(maBelleImg) ne sert à rien car maBelleImg sera réinitialisé).
-Un
StatelessWidget doit absolument implémenter une méthode
"build", qui retourne un Widget, sera appelée lors de l'insertion dans l'arbre des widgets / quand le widget parent se reconstruit / un InheritedWidget (thème, langue...) change.
-D'autres qui demandent des
état internes doivent être des
StatefulWidget (car doivent mémoriser des choses, ex un champ texte retiendra la position du curseur, la position de la scrollbar... on mémorise le nom d'une image qui va changer...).
-Un StatefulWidget doit
absolument contenir une méthode
createState. C'est dans le State qu'on trouvera la méthode "build". Le
state est conservé. Si on fait
var maBelleImg="1.png" puis
Image.asset(maBelleImg) il faudra rappeler build. Le plus simple est d'appeler la fonction "setState" des statefulwidgets mais on peut utiliser d'autres trucs genre des ChangeNotifier.
-
Certains widgets sont dans les faits stateless mais n'héritent pas de StatelessWidget car il
font du rendu (héritent de RenderObjectWidget). Ils sont le fond du fond, c'est à dire quasiment les pixels qu'on va écrire sur l'écran, on arrive au bout de l'arbre là. Comme ils n'héritent pas de StatelessWidget ils n'ont pas besoin d'implémenter de méthode Build.
Appli minimale
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: Text("coucou")));
}
Exemple avec setState
Ci-dessous un widget qui change son image selon ce qu'elle a en attribut (donc widget avec un state, stateful) :
class DiceRoller extends StatefulWidget {
const DiceRoller({super.key});
@override
State<DiceRoller> createState() {
return _DiceRollerState();
}
}
class _DiceRollerState extends State<DiceRoller> {
final randomizer = Random();
var diceNbr = 1;
void rollDice() {
setState(() { // Cette fonction force le rafraîchissement du widget en rappelant "Build"
diceNbr = randomizer.nextInt(6) + 1; //Ce code pourrait être à l'extérieur de setState mais le laisser dedans aide Flutter pour le debug.
});
}
@override
Widget build(ctx) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset( //penser a declarer son repertoire assets/img/ dans pubspec.yaml
'assets/img/dice-$diceNbr.png',
cacheWidth: 600, //donner un cacheWidth plus grand que l'image peut aider à la lisser
filterQuality: FilterQuality.high //pareil, pour lisser
),
OutlinedButton(
onPressed: rollDice,
child: const Text("Lancer le dé !"),
Application exemple en détail
Ci-dessous une analyse perso de l'appli exemple donnée par Google en 2026.
On a un StateLess widget "MyApp" qui créé un stateful widget spécial ChangeNotifierProvider contenant l'état de l'appli. Il lance aussi la création de MyHomePage().
L'archi est bien pour des projets avec des attributs partagés mais pas super clair pour comprendre le fonctionnement de Flutter.
void main() { //Première fonction appelée
runApp(MyApp()); // fonction du framework Flutter, prend un widget en entrée
}
Au moment de la création d'un widget, la méthode build est appelée :
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) { // Appelée par Flutter quand il détecte le widget, retourne un widget
print("build appelé"); // S'appellera une seule fois (ou si l'user passe en mode sombre, ou change la langue de son tel)
return ChangeNotifierProvider( // C'est (le constructeur d') un widget Stateful (a "Widget" pour ancêtre), est invisible, informe ses enfants de changements d'états.
create: (context) => MyAppState(), //L'état de l'application. Sera transmis aux enfants.
child: MaterialApp( //ici on créé un autre widget, invisible, qui transmettra des infos de config visuelles (thème, bavigation, police d'écriture...).
title: 'Namer App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(), //La route par défaut de MaterialApp
),
);
}
}
En dessous on peut avoir le ChangeNotifier qui est utilisé pour les données de l'applipartagées entre plusieurs widgets :
class MyAppState extends ChangeNotifier { // Ce n'est pas un widget. Hérite de Listenable. Notifie ses abonnés en cas de changement.
var current = WordPair.random(); // Il préviendra si on modifie ses attributs
void coucouIciLeChangeNotifier() { // On peut lui mettre une fonction que ses abonnés (enfant de celui qui nous contient) peuvent appeler.
print('ici le change notifier, coucou !');
print( WordPair.random());
current = WordPair.random();
notifyListeners(); // Il faut absolument dire aux abonnés qu'on a changé un truc (concrètement, va voir dans sa liste de callback).
}
}
Et enfin le widget "page principale" :
class MyHomePage extends StatelessWidget // Un autre Widget invisible qui réunit les Widgets de la page d'accueil.
@override
Widget build(BuildContext context) { // Sera appelé quand appState change. Context en entrée fait le lien avec le reste de l'app en donnant le contexte dans lequel est créé cet élément.
var appState = context.watch<MyAppState>(); // On "s'abonne" à MyAppState avec cette variable, mais en vérité c'est l'inverse : cette référence au contexte vient s'enregistrer en tant que callback dans une liste du ChangeNotifier.
// Pour chaque élément, dans le framework on a un "Widget build() => widget.build(this);", et "this" représente le contexte de l'élément en cours.
return Scaffold( // Un widget (Stateful -car mémorise "le menu latéral est-il ouvert", "le clavier déplié" etc- mais osef). Un échauffadage pour faire rapidement des structures/visuelles correspondant aux règles de design Google (couleur de fond, positionnements...). Va générer automatiquement des widgets visuels selon la config qu'on lui donne.
backgroundColor: Color.fromARGB(255, 0, 255, 0), // pour configurer le Widget (de type Material) que va générer Scaffold
body: Column( //Encore un widget invisible, définit les layout.
children: [ // Scaffold va générer un widget pour le font mais on peut lui donner nos propres widgets
Text('Une idée au pif :'), //Ce Widget là sera visible, il utilise RichText qui est un RenderObjectWidget.
Text(appState.current.asLowerCase),
ElevatedButton( // Un widget stateful (est-il pressé, survolé...)
onPressed: () {
print('button pressed!');
appState.coucouIciLeChangeNotifier(); //On appelle le state contenu par le widget ChangeNotifierProvider, qui est notre arrière grand père.
},
child: Text('Next'),
),
],
),
);
}
}
L'arbre (widget tree) sera le suivant :
-MyApp (Le widget racine)
--ChangeNotifierProvider (Un Widget invisible, contient l'état "MyAppState")
---MaterialApp (Widget pour configurer le visuel).
----MyHomePage (Widget invisible qui regroupe les widgets de la home page).
-----Scaffold
------(plein de widgets "par défaut" genre la direction du texte, le fond...)
------Column
------Text
------Text
------ElevatedButton
-------Text
Quand on clique sur le boutton ElevatedButton, il va déclencher la fonction contenue dans onPressed, faire le print et appeler le parent.
Il y a 3 arbres dans Flutter, donc on peut détruire un Widget parent :
-arbre des widgets (éphémère, contient la description de l'UI, c'est une recette)
-arbre des éléments (persistent, contient les States et le BuildContext -la position dans l'arbre et permet de chercher des éléments, se souvient de ce qui existait avant)
-arbre du rendu (gère le layout, le rendu, les interactions).
Le widget stateful, notez que les infos importantes se trouvent dans le STATE et pas dans la classe StatefulWidget elle même, les devs de Flutter
se défendent en disant que c'est plus facile à utiliser, plus souple et plus évolutif de faire ça :
class MyHomePage extends StatefulWidget {
@override
State<MyHomePage> createState() => _MyHomePageState(); // Retourne le state (et précise à quelle classe ce state est lié, pratique car on peut lire les attributs custom du widget depuis le state).
}
class _MyHomePageState extends State<MyHomePage> { //il faut cette sous classe, car son instance sera conservée (et donc les valeurs de ses attributs).
var indexChoisi = 0; //
@override
void initState() { //Par contre ça c'est appelé qu'une fois au debut, pratique si on veut faire une action en début de widget.
super.initState();
print("coucou");
}
@override
Widget build(BuildContext context) { // appelé après initState ou setState. On peut juste écrire build(context).
print("build myhomepage appelé");
var appState = context.watch<MyAppState>();
return Text("coucou");
}
Faire ses propres widgets
Pour éviter les classes de 5km de long, on peut créer son propre Widget qui sera une combinaison de plusieurs widgets. Pour ça, une classe qui extends StatelessWidget ou StatefulWidget avec une méthode build retournant la combinaison de Widgets qu'on veut utiliser.
Exemple :
void main() {runApp(MaterialApp(home: Scaffold(body: MonPetitWidget(),),),);}
class MonPetitWidgetextends StatelessWidget {
@override
Widget build(BuildContext context) { return Text("coucou");}
}
Constructeurs
Il y a plusieurs types de constructeurs en Dart.
Pour un widget on utilise général un constructeur "simplifié, plus rapide à écrire mais on ne peut pas avoir de code custom dedans :
class GrosseCarte extends StatelessWidget {
final WordPair pair; // un attribut, sera alimenté par le constructeur.
const GrosseCarte(// Le constructeur ! Avec const devant pour l'optimiser et le garder en mémoire.
{ //ici à cause de l'accolade on donne des "named parameters" mais on pourrait donner des params normaux
super.key, // c'est une sorte d'id unique au composant, super indique qu'on remonte ce qu'on reçoit auto au parent.
required this.pair, //Paramètre donn2 en entrée du constructeur, sera stocké dans this.pair.
//En Java on aurait genre "this.pair=param"... mais ici tout est implicite.
}); //ici on n'a pas d'accolade après le constructeur car il est obligatoirement vide avec cette syntaxe. En Java on aurait accolades vides.
@override
Widget build(BuildContext context) {
return Text(pair.asLowerCase); //
}
}
Pas de code dedans car l'objet créé est alors mis en cache par Flutter et le constructeur ne sera pas rappelé. Donc il ne peut pas avoir de code dedans (qui ne serait pas exécuté!). Et tous ses attributs doivent être "final". On pourra invoquer ce constructeur avec const ou non.
Si on veut ajouter du code dans le constructeur, on ne peut plus utiliser le mot-clé "const" et on doit écrire un corps :
GrosseCarte({
Key? key,
required this.pair,
}) : super(key: key) // ici c'est aussi une ancienne syntaxe, indique qu'on veut remonter l'info key en appelant le constrtucteur du parent ("initializer list")
{
print("La carte a été créée avec la valeur : $pair"); //là on est dans le corps du constructeur
}
Comme en Java on peut avoir plusieurs constructeurs sauf qu'ils s'écrivent un peu bizarrement, avec le nom de la classe, un point et le nom :
GrosseCarte.autreConstructeur({super.key}): monAttributdeMaClasseGrosseCarte=valeurForcee;
Vrac
Can't reach server
Si vous testez en mode web et que le serveur dart ne se lance pas :
-fermer vscode
-dans le gestionnaire de tâches, flinguer les process dart.exe et edge.exe
Classe
Mot clef "class", CamelCase, peut être dans son propre fichier (nommé lui en snake case) ou non.
Une classe préfixée avec un _ (genre _MaBelleClasse) est uniquement visible par une autre classe/variable/fonction globale
du même fichier. Inaccessible en dehors du fichier.
Garder les virgules pour le formattage
Dans "analysis_options.yaml" :
formatter:
trailing_commas: preserve
Déclaration de variable
On peut déclarer le type ou utiliser "var", ces 2 lignes sont strictement identiques :
Alignment beginAlignment = Alignment.topCenter; //la variable sera de type Alignment
var beginAlignment = Alignment.topCenter; //la variable sera aussi, évidemment, de type Alignment
Dans les 2 cas impossible d'y mettre null.
Il est conseillé d'utiliser "var" pour éviter d'avoir à changer dans x fichiers si on change d'avis plus tard.
Ne pas confondre "var" et le type "dynamic". "var" sera remplacé par le vrai type lors de la compilation, et "dynamic" est un type spécial pouvant devenir n'importe quel type quand le code est exécuté. Si on écrit "var a;" sans donner de type que Dart peut déduire, le type sera "dynamic".
On peut aussi utiliser un "?" derrière le type pour dire qu'il pourra être nul :
Alignment? beginAlignment = null; //c'est ok
On peut utiliser "const" si la variable ne changera jamais, elle sera initialisé lors de la compil.
On peut aussi utiliser "Final" si la "variable" sera vide et remplie une seule fois. Assez proche de const donc mais un poil plus souple. Typiquement on peut faire "final maVar = maFonctionQuiLaremplit()" mais pas avec const car la fonction n'est pas exécuté à la compil.
Typiquement :
class MaBelleClasse {
const MaBelleClasse(this.attribut, this.uneListeDeString); // Un constructeur, dit que l'objet créé sera constant donc on pourra l'invoquer avec const
final String attribut; // L'attribut DOIT être final sinon l'objet ne pourrait pas être une constante. On ne peut pas avoir const ici car on ne définit pas son contenu à la compilation mais une seule fois lors de l'execution
final List<String> uneListeDeString;
}
Autant que possible, utiliser des "const" car ça améliorera les performances vu que Dart va mettre en cache ("canonicalisation"). Permet à Flutter de garder un élément en mémoire et de le réutiliser plus tard vu qu'on lui dit que c'est une constante et qu'il ne change pas.
On peut avoir le const à DROITE du égal pour indiquer qu'on veut cacher un objet :
var activeScreen = const StartScreen();, c'est assez marrant la variable reste variable mais l'objet pointé est en cache ! Il est créé dès la compilation et reste dans le cache en attendant qu'on l'appelle. Ne sera pas garbage collecté.
Mais récemment
des benchmarks ont révélé que c'était pas forcément utile. Dans analysis_options.yaml
prefer_const_constructors: true si on les veut quand même.
Fonction
Les fonctions peuvent se passer comme en js, maBelleFonction() pour exécuter et maBelleFonction pour la passer.
Fonction anonyme :
(){} ou bien
()=>print("Une seule ligne de code")
Lifting the state
Désigne un concept où 2 enfants doivent synchroniser leurs states.
Exemple typique : 2 boutons A et B. A doit être désactivé quand B est activé et vice versa.
Plutôt que d'avoir un state true/false dans A et un state true/false dans B et donc risque de desynchro, on peut remonter le state au 1er parent commun des enfants.
Parameters
On peut utiliser des named parameters, attention ils sont optionaux par défaut (mettre required si besoin) :
maBelleFonction(nomDeLArgument: valeur, nomDeLAutreArgument:valeur2);
void maBelleFonction({nomDeLArgument, nomDeLAutreArgument}) {print(nomDeLArgument + nomDeLAutreArgument);}
Même en les mélangeant à des arguments positionnel qu'on peut passer avant :
void maBelleFonction(argumentPositionel, {nomDeLArgument, nomDeLAutreArgument}){
print(argumentPositionel + nomDeLArgument + nomDeLAutreArgument);
}
Et si on veut non-nommés et optionnels, utiliser des params entre crochets :
Icon([this.size])
Récupérer le widget parent
context.findAncestorWidgetOfExactType<Quiz>() mais attention c'est anti-pattern que l'enfant connaisse le parent, idéalement il ne devrait pas.
late
Pour initialiser un attribut un peu plus tard, lorsqu'on utilisera la variable, exemple :
class _QuizState extends State<Quiz> {
late Widget monAutreWidget = MonAutreWidget(maBelleFonction); // On essaie d'utiliser la fonction de l'instance alors que l'objet n'a pas été instancié.
void switchScreen() { //C'est bien une fonction d'instance car pas de static devant.
On peut aussi utiliser "late" sans assigner de valeur, ça devient une promesse au compilateur qu'on initialisera cette var avant sa première lecture.
Notez que dans l'exemple ci-dessus le state contient un widget. Ca marche mais va contre le paradigme UI Déclaratif où l'interface est calculée à partir du state : fonctionDeCalculDeLInterface(lesDonneesDuState). Dans ce paradigme ce n'est pas le dev qui change l'UI lors d'un clic 5vieille façon de penser), le dev change le state puis l'UI est calculée à partir de ça (permet de gérer plusieurs infos en plus du clic genre la langue, l'orientation de l'écran, le thème sombre ou non etc).
En outre si on fait fonctionDeCalculDeLInterface(lesDonneesDuState), les données peuvent être sérializée facilement et le state recalculé vite. Et il ne faut pas stocker un truc qui peut être recalculé !
if, for
On peut utiliser if ou les for dans les listes :
final maList = [
1,
2,
if (condition)
3
else
4,
condition ? 3 : 4
];
Pratique quand on donne des children à un widget.
opérateur de cascade
monObjet..maMethodeQuiRenvoieVoid().autreMethodeDansMonObjet()
Le double point applique la méthode qu'on vient d'appeler (maMethodeQuiRenvoieVoid) puis renvoie monObjet.
Exemple sans opérateur cascade :
var answersToDisplay = currentQuestion.immutableAnswersList.toList(); //on doit creer une nouvelle liste pour mélanger
answersToDisplay.shuffle(Random(0)); //shuffle renvoie void
return answersToDisplay.map(((e) => AnswerButton(e, () {}))).toList(); // on
Avec l'opérateur de cascade (notez qu'il faut entourer de parenthèse sinon on essaieraie d'appliquer le map au retour de shuffle directement) :
return (currentQuestion.immutableAnswersList.toList()..shuffle(Random(0))).map(((e) => AnswerButton(e, () {}))).toList();
getter
Pour utiliser une méthode qui ne prend pas d'arguments comme si c'était une propriété :
String get maString {
return "coucou";
}
print(maString); //pas besoin de parenthèses.
Ceci évite d'avoir un attribut potentiellement modifiable qui traine.
Test en debug sur son téléphone sans installer Android Studio
-télécharger les https://developer.android.com/studio#command-tools
-rajouter un dossier "latest" avant bin, le chemin doit être "C:\outils\androidsdk\cmdline-tools\latest\bin".
-faire
.\sdkmanager.bat "platforms;android-36" "build-tools;36.0.0" "build-tools;28.0.3" "platform-tools"
-vérifier que tout est vert en faisant
flutter doctor, si besoin taper ce que dit le docteur (licenses...).
-connecter son téléphone en mode usb debugging (faut être en mode dev)
-faire
flutter run, ça va être long car il va télécharger plein de libs dans .gradale.