Editer

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).

Premier lancement


Dans n'importe quel dossier : flutter create ma_belle_app_flutter va créer une quinzaine de fichiers/dossiers.

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 quasi tout est "widget" (l'appli elle même, les layouts, les boutons 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.

Il est très rapide de détruire et recréer un widget (60 fois par seconde sans problème). On peut détruire/reconstruire le widget parent (par exemple changer la couleur de l'app) sans détruire le widget enfant. Il est assez intelligent pour ne pas recréer un widget si rien n'a changé.

-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, d'autres StatefulWidget (car doivent mémoriser des choses un champ texte retiendra la position du curseur, la position de la scrollbar...).

-Un StatelessWidget doit absolument implémenter une méthode "build", qui retourne un Widget, sera appelée à chaque création du Widget. La méthode DOIT contenir des sous widgets qui sont déclarés dans la méthode Build (vu que la méthode est obligatoire et DOIT renvoyer un Widget...).

-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é, par contre "build" peut être appelé plusieurs fois par secondes sans souci :

-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"))); }


Application exemple en détail


Ci-dessous une analyse perso de l'appli exemple donnée par Google en 2026.


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 return ChangeNotifierProvider( // C'est (le constructeur d') un widget (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 :

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, la syntaxe :

class MyHomePage extends StatefulWidget { @override State<MyHomePage> createState() => _MyHomePageState(); // on peut pas direct avoir le build là, un peu WTF. } class _MyHomePageState extends State<MyHomePage> { //il faut cette sous classe, car elle est immuable et sera détruite plein de fois par secondes. var indexChoisi = 0; // @override void initState() { //Par contre ça c'est appelé qu'une fois, pratique si on veut faire une action en début de widget. super.initState(); print("coucou"); } @override Widget build(BuildContext context) { // BuildContext est faculatif 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); // } }


Si on utilise l'ancienne syntaxe ou 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


const : 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.

Classe


Mot clef "class", CamelCase, peut être dans son propre fichier (nommé lui en snake case) ou non.



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.

Autant que possible, utiliser des "const" car ça améliorera les performances vu que Dart va mettre en cache ("canonicalisation").

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); }


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.