Flutter
Présentation du cours
Présentation personnelle
- Architecture des Logiciels à l'ESGI
- Dévelopeur mobile : Flutter & Android natif
- Fondateur Flappy (flappy.tech)
- Formateur : Flutter, Android natif, Git, Algo, Firebase, Projet Annuel 5 AL et MOC
- Freelancing : ContentSquare
- Co-organisateur du Meetup Flutter Paris
- Adresse mail : thomasecalle+prof@gmail.com
Présentation du cours
Règles générales concernant :
- les retards
- l'attention en cours
- les supports de cours
- la notation
- un TP en solitaire
- un projet à faire en équipe
- je prends vos feedbacks !
Prérequis
Ce cours part du principe que vous avez des notions plus ou moins avancées dans les concepts suivants :
- programmation orientée objet
- développement mobile (Android, iOS, ReactNative)
Jusqu'où irons-nous ?
- 30h = non experts
- mais on aura quand même les bases pour être recrutables
- et cours d'initiation veut aussi dire :
- aller vite à l'essentiel
- nous sommes là pour apprendre un Framework
- des pistes vous seront données pour aller plus loin !
Introduction à Flutter
Un peu d'histoire

WebViews
WebViews++ (Cordova, Ionic, etc.)
React Native
Flutter
Qu'est-ce que c'est ?
- made in Google
- framework d'applications mobiles hybrides
Entendre par "hybride": un code pour X plateformes
- permet actuellement de créer des applications pour :
- iOS
- Android
- Web (stable mais pas de SEO)
- MacOS
- Linux
- Windows
- (Fuschia ?)
Les dates clef ?
- 2010 : Introduction de Dart à la GoTo Conference au Denmark
- 2013: Dart 1.x
- avril 2015 : Experimentations de Dart pour les apps mobiles (SKY)
- mai 2017: Alpha Release de Flutter
- 2018 : Dart 2.x
- décembre 2018 : Flutter en mode "production ready" v1.0
- mars 2021 : Flutter 2.0 à la suite du Flutter Engage (web stable, sound null-safety, etc.)
Comment ça marche ?
Dart
- made in Google
- langage fortement typé
- null safety
- à la croisée de Java/Kotlin et de Javascript
- 2 types de compilations:
- Just in Time (JIT) -> Debug
S'exécute dans la Dart VM
Rend exploitable le Hot Reload - Ahead of Time (AOT) -> Production
Produit du code machine ARM (C/C++)
- Just in Time (JIT) -> Debug
Comment ça marche ?
Moteur Graphique
- moteur graphique utilisé: Skia puis Impeller
- flutter révolutionne l'hybride
- toutes les vues dessinées au pixel près
- aucun temps de latence dû à l'interprétation
- 60 FPS
- rendu parfaitement similaire peu importe la version de l'OS
Par rapport à la concurrence ?
- moteur graphique
- pas d'interprétation
- 60 fps
- similarité entre les versions d'OS
- Dart
- langage fortement typé et moderne
- compilation JIT ET AOT
- notion de declarative UI
- communauté très importante
- qualité de la documentation et des packages
- expérience de développeurs... à vivre !
Avant de
commencer
Documentation officielle
- documentation très complète
- exemples de code testables
- grosse collection de widgets
Chaînes Youtube
Articles sur Flutter
Communautés Slack
Le Dartpad
- IDE en ligne pour:
- Dart
- Flutter
- Partage de code via Gist
Pub.dev
- plateforme du gestionnaire de dépendances pub

Installation
IDEs
Les IDEs pour faire du Flutter sont les suivants :
- Android Studio (recommandé)
- VS Code
- IntelliJ
Pour ce cours, nous partirons du principe que nous travaillons sur Android Studio
Les plugins
Il existe 2 principaux plugins à ajouter à votre IDE :
- plugin Flutter
- plugin Dart

Documentation officielle
Voici le lien avec les consignes d'installation pour chaque OS :
Ce qu'il vous sera demandé
- installation du SDK (où vous voulez)
- mettre le chemin vers flutter/bin dans votre PATH
- lancer la commande :
flutter doctorQu'est-ce que
flutter doctor ?
- analyse l'ensemble de votre environnement
- donne votre version de Flutter et de Dart
- donne le channel flutter sur lequel vous êtes
- indique si votre environnement est correct ou non
Les channels
Faites un coup de flutter channel, voici le résultat :

Ordre de stabilité : stable > beta > dev > master
Changer de channel :
flutter channel maChannelCréer sa première app
Arborescence des fichiers
Initialiser une application
- Placez-vous où vous voulez créer votre application
flutter create mon_application- ou alors, avec le plugin Flutter de votre IDE :

Analysons l'arborescence
ls -l mon_application
Analysons l'arborescence
android / ios

Analysons l'arborescence
android / ios
Dans ces dossiers se trouvent l'ensemble des fichiers d'un projet basique en Android et en iOS
Ils seront à priori peu modifiés mais peuvent l'être :
- pour modifier Plist ou Manifest
- changer la gestion du build
- faire un peu de code natif si nécessaire
- etc.
Analysons l'arborescence
android / ios
Attention !
Par défaut, flutter créé votre projet avec comme langage :
- Kotlin pour android
- Swift pour iOS
Ceci est modifiable à la création comme cela:
flutter create -i objc -a javaAnalysons l'arborescence
lib

Analysons l'arborescence
lib
Nous y reviendrons plus tard
Ce dossier est celui dans lequel nous écrirons la totalité de notre code Dart/Flutter
Analysons l'arborescence
pubspc.yaml/pubspec.lock

Analysons l'arborescence
pubspc.yaml/pubspec.lock
Le fichier pubspec.yaml est l'un des fichiers les plus importants d'un projet Flutter
Nous y retrouvons:
- le package name Dart
- la description du projet
- la version (version name + version code) commune à iOS et Android
- les dépendances (prod / dev)
- l'import des assets
- l'import des fonts
Analysons l'arborescence
pubspc.yaml/pubspec.lock
Le fichier pubspec.lock est un fichier renseignant sur les versions des dépendances utilisées dans le projet.
En effet, il est possible de donner un numéro de version non précis pour une dépendance comme: ^2.3.5
Cette indication indiquant que l'on accepte les versions de 2.3.5 à 3.0.0 exclu
Analysons l'arborescence
test

Analysons l'arborescence
test
Comme son nom l'indique, ce dossier va contenir tous nos tests Flutter.
Cependant, ce cours étant une initiation, nous n'irons pas plus loin sur ce sujet
Analysons l'arborescence
web / windows

Analysons l'arborescence
web / windows
Dossiers équivalents à ceux Android et iOS dans le sens où ils contiennent la structure native par défaut pour une app web ou Windows
Créer sa première app
Test de l'application générée
Lançons l'application
Ouvrez l'application générée sur votre IDE
Lançons l'application

Sélectionnez ici un émulateur pour tester votre application
Si vous n'en avez pas, créez-en un au niveau de la flèche rouge
Lançons l'application

Lancez en cliquant sur la flèche verte
Vous pouvez aussi utiliser la ligne de commande :
flutter runLançons l'application
Testons maintenant la puissance du Hot Reload !
Ouvrez le fichier lib/main.dart et modifiez la chaîne de charactère "Flutter Demo Home Page" :
home: MyHomePage(title: 'Flutter Demo Home Page'),Sauvegardez vos changements (CMD + S, CTRL + S ou l'icône de foudre en haut d'Android Studio)
C'est parti pour développer en Flutter !
Explications
Pour la suite du cours, nous nous baserons sur un dépôt GitHub regroupant l'ensemble des cours / tp que nous réaliserons ensemble pour apprendre à développer en Flutter
La suite des slides seront donc des slides "de cours", à prendre plus comme des fiches de révisions par rapport à des concepts vus en direct ensemble
Explications
Il est donc important de comprendre que le principal du cours se fera via la pratique ensemble sur l'IDE
Les slides qui suivent n'ont pour objectif que d'aider à la révision et à la reprise pour les absents
Quelques notions en Dart
Les constructeurs de classe
Dart possède quelques subtilités concernant ses constructeurs
Tout d'abord, il n'est pas nécessaire de "mapper" obligatoirement les paramètres reçus par le constructeur avec les champs de la classe, Dart le fait automatiquement
class User {
String name;
User(String name) {
this.name = name;
}
}
class User {
String name;
User(this.name);
}Les constructeurs de classe
Par défaut, les paramètres d'une classe ne sont ni optionnels ni nommés.
final User user = User("Perceval");Le compilateur m'impose en effet de renseigner ce champs pour que ça fonctionne.
final User user = User();OK
PAS OK
Les constructeurs de classe
Je peux toutefois rendre un ou plusieurs paramètres optionnels, ils seront alors nommés également.
Il suffit pour cela d'entourer d'accolades les paramètres souhaités
class User {
String name;
String? email;
User(this.name, {this.email});
}
final User user = User("Perceval", email: "perceval@gmaiL.com");Les constructeurs de classe
Attention, avez-vous noté le "?" sur le type du champs email ?
String? emailCe symbole "?" signifie que le champs est Nullable
Cela signifie que ce champs peut prendre la valeur NULL et que cela devra impérativement être pris en compte pour le reste du développement
Les constructeurs de classe
On va revenir sur ce concept de Nullable dans le chapitre suivant !
Les constructeurs de classe
Alors que faire si vous ne voulez pas que votre paramètre nommé soit Nullable ?
Vous avez 2 choix :
Les constructeurs de classe
Indiquer une valeur par défaut :
class User {
String name;
String email;
User(this.name, {this.email = "toto"});
}
final User user = User("Perceval", email: "perceval@gmaiL.com");Les constructeurs de classe
Indiquer le champs comme "required"
Le champs devra alors impérativement être renseigné
class User {
String name;
String email;
User(this.name, {required this.email = "toto"});
}
final User user = User("Perceval", email: "perceval@gmaiL.com"); // OK
final User user = User("Perceval"); // Ne compile pasLes constructeurs de classe
Les paramètres nommés sont très utiles en Flutter, car la totalité du rendu graphique se fait via le code et ils permettent donc celui-ci d'être plus lisible !
Vous comprendrez au fur et à mesure l'importance du nommage de paramètre en Flutter !
L'héritage
class Knight {
final String name;
const Knight(this.name);
}
class Perceval extends Knight {
final String botteSecrete;
Perceval(String name, this.botteSecrete) : super(name);
}Null-Safety
Null-Safety
Depuis sa version 2.12, le langage Dart supporte le Sound Null-Safety
Le Null-Safety, c'est le fait que l'on soit obligé de déclarer explicitement si un champs peut potentiellement être NULL
String? firstName; // Ce champs est "nullable"
String lastName = "toto"; // Ce champs n'est pas "nullable",
// on doit donc lui donner une valeurNull-Safety
L'intérêt d'un langage supportant le null-safety est énorme et bien connu des développeurs Kotlin ou encore Swift depuis longtemps :
- permet d'éviter toutes les NullPointerException
- code bien plus sécurisé et donc maintenable (on réduit les erreurs possibles)
Null-Safety
Lorsque vous débutez une application Flutter "From Scratch", il n'y a à priori aucune bonne raison de ne pas mettre en place le null-safety
Null-Safety
Pour ce faire, vous devez changer les contraintes de SDK dans votre pubspec.yaml afin de vous baser sur une version minimale de Dart à 2.12
Comment faire ?
environment:
sdk: ">=2.12.0 <3.0.0"Null-Safety
Si vous venez de créer une app par défaut, elle ne pourra normalement plus compiler car le fichier main.dart n'est plus compatible avec le null-safety, mais c'est normal !
A vous de convertir votre code pour le rendre compatible au null-safety !
MyHomePage({Key? key, required this.title}) : super(key: key);Null-Safety
Qu'est-ce que signifie SOUND null-safety ?
Ca signifie que, lors de la compilation de votre projet, Flutter peut vous renvoyer des erreurs si il considère que tout n'est pas safe
Notamment pour vos dépendances ! Si vous utilisez une dépendance qui n'a pas migré, alors potentiellement votre propre code pourra en pâtir
Et donc Flutter pourrait vous embêter avec ça...
Null-Safety
Heureusement, on peut quand même run (et build) son app en désactivant l'aspect "Sound" du null-safety :
flutter run --no-sound-null-safetyQu'est-ce qu'un Widget ?
Tout est Widget
Vous l'entendrez souvent en faisant du Flutter, tout est widget.
En effet, les widgets sont la base de Flutter, ils peuvent correspondre à la fois à un simple texte comme à l'application elle même
Tout est Widget
Un Widget est une classe héritant d'un certain type de Widget
class MyApp extends StatelessWidget {
}Il existe 3 principaux types de Widgets que nous décrirons plus tard : StatelessWidget, StatefullWidget et InheritedWidget
Attention, ces 3 types héritent eux-même du type Widget
Tout est Widget
Penchons-nous sur le StatelessWidget !
Un StatelessWidget hérite donc de la classe StatelessWidget et doit renseigner son propre rendu graphique grâce à la méthode build de celle-ci
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
...
}
}Tout est Widget
Vous remarquez que la méthode buil() retourne elle-même un Widget !
Ceci est donc tout à fait possible :
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return A();
}
}
class A extends StatelessWidget {
@override
Widget build(BuildContext context) {
return B();
}
}
class B extends StatelessWidget {
@override
Widget build(BuildContext context) {
...
}
}Tout est Widget
Le rendu graphique se fait donc en composant les widgets les uns les autres pour construire l'UI.
Voici un écran blanc avec un carré rouge de 50 pixels de côtés au centre
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Center(
child: Container(
height: 50,
width: 50,
color: Colors.red,
),
),
);
}
}
Tout est Widget
Container et Center font partie des très nombreux Widgets préexistant avec lesquels vous pouvez construire VOS widgets !
Il suffit encore une fois de composer vos widgets les uns aux autres en utilisant ceux existants
Tout est Widget
Voici un autre exemple de ce que l'on aurait pu écrire, juste pour vous montrer que cela ne change rien:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return A();
}
}
class A extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Center(
child: Container(
height: 50,
width: 50,
color: Colors.red,
),
),
);
}
}Tout est Widget
Les paramètres
Vous avez peut-être remarqué que les widgets Container et Center prennent des paramètres.
Un Widget étant une classe Dart, elle peut en effet prendre des paramètres !
Tout est Widget
Les paramètres
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return A(
squareColor: Colors.red,
);
}
}
class A extends StatelessWidget {
final Color squareColor;
const A({
Key? key,
required this.squareColor,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Center(
child: Container(
height: 50,
width: 50,
color: squareColor,
),
),
);
}
}Tout est Widget
Les paramètres
Le paramètre de type Key que l'IDE nous a automatiquement ajouté est un paramètre utilisé par la classe parente Widget (d'où l'appel à super()).
Nous ne l'utiliserons presque jamais dans ce cours, mais sachez qu'il agit comme un identifiant du widget, si un jour vous avez besoin d'identifier de manière unique un widget dans l'arbre des widgets
Fonctionnement d'une application Flutter
Fonctionnement
Nous l'avons vu, TOUT est widget en Flutter, une fois que vous avez compris ça, vous avez tout compris !
Par exemple, la méthode main() de Flutter doit, pour faire fonctionner l'application, renvoyer ce que la méthode runApp() retourne: Un Widget
void main() => runApp(Container());Ceci est donc en théorie l'application Flutter la plus courte possible.. !
Arbre de widgets
Une fois que vous avez compris la composition des widgets, vous réalisez que l'application entière n'est qu'une composition de widgets: un arbre de widgets.

Le widget MaterialApp
et la notion d'InheritedWidget
Le widget MaterialApp
et la notion d'InheritedWidget
Le widget MaterialApp sera souvent la racine de votre arbre de widgets, son premier parent.
Ceci est dû au fait que la MaterialApp amène avec elle beaucoup de concepts très importants pour la suite :
- la navigation
- le thème
- le context material
- etc.
Le widget MaterialApp
et la notion d'InheritedWidget
Créer un Thème dans la MaterialApp vous permet d'y avoir accès partout dans l'application comme ici:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
textTheme: TextTheme(
headline1: TextStyle(
color: Colors.amber,
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
),
home: Home(),
);
}
}
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Center(
child: Text(
"Hello",
style: Theme.of(context).textTheme.headline1,
),
),
);
}
}Le widget MaterialApp
et la notion d'InheritedWidget
C'est génial car cela permet de déclarer quelque chose dans un des widgets de l'arbre et que tous ses enfants y aient accès directement.
Comment cela fonctionne-t-il ?
Grâce aux InheritedWidget
Le widget MaterialApp
et la notion d'InheritedWidget
Un InheritedWidget est l'un des 3 types de widgets.
Ce widget est le seul à ne pas avoir de représentation graphique !
Sa seule utilité est, une fois créé, d'être accessible directement par tous ses enfants et donc de partager de la donnée facilement.
Le widget MaterialApp
et la notion d'InheritedWidget
Ainsi, la MaterialApp génère un InheritedWidget nommé Theme contenant tout le thème souhaité.
Il est alors possible de faire appel à cet InheritedWidget depuis n'importe quel enfant comme ceci:
Theme.of(context).textThemeCe code signifie : "va me chercher l'instance de 'Theme' la plus proche de moi dans l'arbre des widgets."
Le widget Scaffold
ce qui ressemble le plus à un "écran"
Scaffold
Le widget Scaffold peut être considéré comme étant le widget racine d'un écran, bien que non obligatoire.
Il est ainsi considéré car il possède de nombreux attributs permettant de représenter l'image que l'on se fait habituellement d'un écran mobile
Il permet de facilement mettre en place :
- une AppBar
- une BottomBar
- un floating button
- etc.
Scaffold

Scaffold
Si je n'ai pas besoin de ces composants pour mon écran, dois-je quand même l'utiliser ?
A priori, rien ne vous y oblige !
J'ai personnellement l'habitude de le mettre tout de même pour chaque écran car il apporte un context Material pouvant être utile pour ses enfants (ombrages, etc.) et qu'il ne coûte rien à mettre
Le StatefulWidget
SatefulWidget
Nous l'avons vu, le StatelessWidget est un Widget possédant une méthode build() permettant d'afficher un rendu graphique
Qu'en est-il lorsque nous souhaitons changer l'état d'un Widget ? Lorsque nous voulons le redessiner ?
C'est là que le StatefulWidget rentre en scène !
SatefulWidget
Prenons l'exemple de ce widget représentant un carré rouge centré :
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
height: 100,
width: 100,
color: Colors.red,
),
),
);
}
}
SatefulWidget
Comment faire pour que, en cliquant sur le carré , celui-ci devienne bleu, puis rouge, puis bleu, etc.
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: InkWell(
onTap: () {
print("Square clicked !");
},
child: Container(
height: 100,
width: 100,
color: Colors.red,
),
),
),
);
}
}Commençons par ajouter un widget capable de récupérer le clic de l'utilisateur (InkWell pour l'effet Material ou alors GestureDetector sinon)
SatefulWidget
Avec notre StatelessWidget, nous sommes maintenant coincés..
Lorsqu'un widget doit être capable de gérer son propre état, et donc de le modifier, ce widget doit alors être un SatefulWidget
Note: On appellera "état" l'ensemble des variables amenées à être modifiées au cours de la vie du widget en question
SatefulWidget
Un StatefulWidget ne définit pas lui-même son rendu graphique ! Un StatefulWidget est en fait séparé en 2 classes.
Il a une seule méthode à implémenter aussi, mais ce n'est pas la méthode build()
Le StatefulWidget va en effet devoir implémenter une méthode permettant de renvoyer l'état de ce widget
SatefulWidget
class StatefulExample extends StatefulWidget {
final String oneImmutableVariableOfTheWidget;
const StatefulExample({
Key key,
this.oneImmutableVariableOfTheWidget,
}) : super(key: key);
@override
_StatefulExampleState createState() => _StatefulExampleState();
}
class _StatefulExampleState extends State<StatefulExample> {
int myStateVariable;
@override
Widget build(BuildContext context) {
return Container();
}
}SatefulWidget
C'est donc la classe qui hérite de State qui va avoir comme responsabilité de faire le rendu graphique du widget !
Notre écran de tout à l'heure, sans changement fonctionnel, ressemblerait donc maintenant à ça :
Le StatefulWidget en soit ne servant qu'à récupérer les potentiels paramètres et à les fournir au State.
SatefulWidget
class Home extends StatefulWidget {
@override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: InkWell(
onTap: () {
print("Square clicked !");
},
child: Container(
height: 100,
width: 100,
color: Colors.red,
),
),
),
);
}
}SatefulWidget
Notons que le State ne doit contenir à priori que des variables mutables, c'est le principe !
Les variables immutables sont placées dans le StatefulWidget et seront toujours accessibles via widget.maVariable comme ceci :
class Home extends StatefulWidget {
final String squareText;
const Home({
Key? key,
this.squareText = "ON",
}) : super(key: key);
@override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: InkWell(
onTap: () {
print("Square clicked !");
},
child: Container(
height: 100,
width: 100,
color: Colors.red,
child: Center(
child: Text(widget.squareText),
),
),
),
),
);
}
}SatefulWidget
BON, passons aux choses sérieuses
Soit une variable color représentant l'état de la couleur du carré, nous pourrions écrire ceci :
SatefulWidget
class _HomeState extends State<Home> {
Color _color = Colors.red;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: InkWell(
onTap: () {
print("Square clicked !");
},
child: Container(
height: 100,
width: 100,
color: _color,
child: Center(
child: Text(widget.squareText),
),
),
),
),
);
}
}SatefulWidget
Il suffirait alors simplement, au clic sur le carré, de modifier cette couleur ! Comme ceci :
class _HomeState extends State<Home> {
Color _color = Colors.red;
void _changeColor() {
_color = _color == Colors.red ? Colors.blue : Colors.red;
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: InkWell(
onTap: _changeColor,
child: Container(...),
),
),
);
}
}SatefulWidget
Mais .... c'est un échec !
Si vous affichez la valeur de la variable, elle a pourtant bien été modifée !
C'est en fait une sécurité ! Flutter vous demande de spécifiquement dire lorsque un changement d'état nécessite au widget de se redessiner !
SatefulWidget
Pour cela, il faut appeler la méthode setState()
Celle-ci va alors provoquer un nouveau dessin du widget, appeler à nouveau la méthode build, et donc le widget sera redessiner avec la nouvelle couleur !
Remplaçons notre méthode changeColor() par ceci et testons:
void _changeColor() {
setState(() {
_color = _color == Colors.red ? Colors.blue : Colors.red;
});
}SatefulWidget
Et voilà !

Layouts
Layouts
Nous allons nous intéresser aux widgets nous permettant de placer les widgets les uns aux autres
En effet, Flutter est basé sur le concept du Declarative UI
Ce qui signifie que les règles de placements des widgets les uns aux autres sont décrits par des Widgets eux-même (comme le widget Center que nous avons déjà aperçu)
Layouts
Pour étudier les layouts, nous nous baserons sur le widget RedSquare nous créerons pour l'occasion
class RedSquare extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 80,
width: 80,
color: Colors.red,
);
}
}Layouts
Pour réaliser des séparations entre les éléments d'une ligne ou d'une colonne, nous préfèrerons utiliser le widget SizedBox (créant un espace vide) plutôt que d'user de Padding sur les élements
Cela permet de réduire les problèmes d'indentations et les responsabilités des widgets affichés.
Layouts
ROW
Permet de présenter les widgets sous forme de ligne
Layouts - Row
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Row(
children: <Widget>[
RedSquare(),
SizedBox(width: 10,),
RedSquare(),
SizedBox(width: 10,),
RedSquare(),
],
),
),
);
}
}
Layouts
COLUMN
Permet de présenter les widgets sous forme de colonne
Layouts - Column
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
children: <Widget>[
RedSquare(),
SizedBox(height: 10,),
RedSquare(),
SizedBox(height: 10,),
RedSquare(),
],
),
),
);
}
}
Layouts
Les Columns et les Rows fonctionnent exactement pareil, mis à part leur orientation
Il est possible de jouer sur :
- leur taille (min ou max)
- l'alignement des éléments sur l'axe principal
- l'alignement des éléments sur le second axe
- etc.
Layouts - Taille
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Container(
color: Colors.amber,
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
RedSquare(),
SizedBox(height: 10,),
RedSquare(),
SizedBox(height: 10,),
RedSquare(),
],
),
),
),
);
}
}
MainAxisSize.max par défaut
Layouts - Taille
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Container(
color: Colors.amber,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
RedSquare(),
SizedBox(height: 10,),
RedSquare(),
SizedBox(height: 10,),
RedSquare(),
],
),
),
),
);
}
}
Layouts - Main Alignment
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Container(
color: Colors.amber,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RedSquare(),
SizedBox(height: 10,),
RedSquare(),
SizedBox(height: 10,),
RedSquare(),
],
),
),
),
);
}
}
Layouts - Main Alignment
.start,
.end,
.spaceBetween,
.spaceAround,
Layouts - Cross Alignment
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Container(
color: Colors.amber,
width: 300,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
RedSquare(),
SizedBox(height: 10,),
RedSquare(),
SizedBox(height: 10,),
RedSquare(),
],
),
),
),
);
}
}
Layouts - Cross Alignment
.start,.end,.stretch,


Layouts
STACK
Permet de positionner les widgets relativement les uns aux autres grâce aux widgets Positionned
Layouts - Stack
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Container(
color: Colors.amber,
width: 300,
height: 300,
child: Stack(
children: <Widget>[
RedSquare()
],
)
),
),
);
}
}
Layouts - Stack
Container(
color: Colors.amber,
width: 300,
height: 300,
child: Stack(
children: <Widget>[
Positioned(
right: 10,
bottom: 60,
child: RedSquare(),
),
],
),
),
Layouts - Stack
Container(
color: Colors.amber,
width: 300,
height: 300,
child: Stack(
children: <Widget>[
Positioned(
right: 10,
bottom: 60,
child: RedSquare(),
),
Positioned(
right: 60,
bottom: 100,
child: RedSquare(),
),
],
),
),
Proportions
Prendre en compte la taille de l'écran
Proportions
Le développement mobile a depuis toujours une problématique très importante : la diversité des tailles d'écrans disponibles
Il est en effet important qu'un écran puisse s'afficher au mieux en prenant en compte les grands écrans comme les petits
Proportions
MediaQuery
MediaQuery est un InheritedWidget créé par la MaterialApp très important qui nous offre de nombreuses données sur notre écran !
Nous ne nous interesserons pour ce cours qu'à la taille de l'écran mais sachez que ce widget regorge de surprises intéressantes (ratio de pixels du device, contrasts, padding de l'écran, etc.)
Proportions - MediaQuery
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
final double screenHeight
= MediaQuery.of(context).size.height;
return Scaffold(
backgroundColor: Colors.white,
body: Column(
children: <Widget>[
Container(
color: Colors.blue,
height: screenHeight * .40,
),
Container(
color: Colors.amber,
height: screenHeight * .30,
),
Container(
color: Colors.pink,
height: screenHeight * .30,
),
],
),
);
}
}
Proportions - MediaQuery
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
final double screenWidth
= MediaQuery.of(context).size.width;
return Scaffold(
backgroundColor: Colors.white,
body: Row(
children: <Widget>[
Container(
color: Colors.blue,
width: screenWidth * .60,
),
Container(
color: Colors.amber,
width: screenWidth * .40,
),
],
),
);
}
}
Proportions
Expanded
Expanded est un widget permettant à son enfant de prendre tout l'espace disponible dans le cadre d'une Row ou d'une Colonne
Il est également possible de lui donner un flex, soit un poids permettant de lui donner une taille proportionnelle aux autres flex
Proportions - Expanded
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Column(
children: <Widget>[
Expanded(
child: Container(
color: Colors.blue,
),
),
Expanded(
child: Container(
color: Colors.amber,
),
),
],
),
);
}
}
Proportions - Expanded
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Column(
children: <Widget>[
Expanded(
flex: 2,
child: Container(
color: Colors.blue,
),
),
Expanded(
child: Container(
color: Colors.amber,
),
),
],
),
);
}
}
Bonus placements
Le widget Spacer
Bonus placements
Spacer
Il est parfois nécessaire de réaliser ce type de vue :

Bonus placements
Il peut sembler compliquer de réaliser ça correctement avec les armes que nous avons vu précédemment
Le widget Spacer est un excellent widget qui va se charger, dans un contexte de Row ou de Column, de prendre le plus de place possible entre 2 widgets
Bonus placements
Ainsi, nous pouvons réaliser ce genre de vue facilement
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Row(
children: <Widget>[
RedSquare(),
SizedBox(width: 10,),
RedSquare(),
Spacer(),
RedSquare(),
],
)
),
);
}
}Les listes
Les listes
Les listes sont parmi les composants essentiels des applications mobiles
Il est donc primordial de savoir les utiliser
la notion de recyclage est le fait que, si nous affichons une lsite de 1 million d'éléments, seuls ceux affichés à l'écran donc vraiment affichés pour de bien meilleures performances
Notons le fait qu'il est important d'avoir des listes "recyclables" :
Les listes
Il existe un composant SingleChildScrollview qui nous permet de rendre scrollable un widget
Nous pourrions donc faire quelque chose comme ça:
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: SingleChildScrollView(
child: Column(
children: List.generate(100, (int index) {
return Column(
children: <Widget>[
RedSquare(),
SizedBox(height: 10,),
],
);
}),
),
)
),
);
}
}
Les listes
Mais le widget SingleChildScrollview est plus pensé pour rendre scrollable une vue trop grande pour un petit écran, pas pour en faire une liste !
Il existe pour cela un widget bien plus intéressant, la ListView
Les listes
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: ListView(
children: List.generate(100, (int index) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
RedSquare(),
SizedBox(height: 10, width: 10,),
],
);
},),
)
),
);
}
}
Les listes
Niveau performances de scroll, nous sommes déjà bien mieux !
Cependant, une simple ListView comme ça n'est pas encore recyclable, ses enfant sont en effet tous dessinés à l'écran d'un coup
Pour avoir une ListView recyclable, il faut utiliser son constructeur builder, comme ceci :
Les listes
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Container(
width: 80,
child: ListView.builder(
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: EdgeInsets.only(bottom: 10),
child: RedSquare(),
);
},
),
),
),
);
}
}Les listes
Il existe aussi le constructeur separated qui permet d'avoir l'équivalent d'un Listview.builder et d'y ajouter le séparateur entre chaque éléments
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Container(
width: 80,
child: ListView.separated(
itemCount: 100,
separatorBuilder: (BuildContext context, int index) => SizedBox(height: 10,),
itemBuilder: (BuildContext context, int index) {
return RedSquare();
},
),
),
),
);
}
}Les listes
Enfin, les listes peuvent évidemment être horizontal, grâce au paramètre scrollDirection qui est à vertical par défaut
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Container(
height: 80,
child: ListView.separated(
itemCount: 100,
scrollDirection: Axis.horizontal,
separatorBuilder: (BuildContext context, int index) => SizedBox(width: 10,),
itemBuilder: (BuildContext context, int index) {
return RedSquare();
},
),
),
),
);
}
}
Rappel sur l'asynchronisme
Asynchronisme & Future
Asynchronisme - Définition Wikipedia
asynchrony, in computer programming, refers to the occurence of events independent of the main program flow and ways to deal with such events.
En gros ce sont tous les évènements qui peuvent prendre un temps certain mais dont votre programme n'est pas responsable
- IO (Input / Output)
- appels réseaux
- etc.
Asynchronisme & Future
En Dart, la classe la plus utile pour gérer de l'asynchrone est la Future : équivalent de la Promise en Javascript
Comprendre complètement la notion d'asynchronisme demanderait un cours plus avancé, mais voyons quelques cas d'utilisation
Asynchronisme & Future
void main(List<String> arguments) {
print('1');
Future.delayed(Duration(seconds: 1), () {
print('2');
});
print('3');
}Ce code affichera :
- 1
- 3
- 2 (après une seconde)
Asynchronisme & Future
Nous allons souvent utiliser des Future déjà crées, mais comment faire les notre ? Très simple, il suffit de faire une fonction renvoyant une Future
void main(List<String> arguments) {
print('1');
_getStringFromNetwork().then((String value) {
print('Getting value: $value');
}).catchError((error) {
print('Getting error: $error');
});
print('3');
}
Future<String> _getStringFromNetwork() {
return Future.delayed(Duration(seconds: 1), () {
return 'My String';
});
}Asynchronisme & Future
Async / Await
Les mots clés async/await sont simplement des aides pour avoir une synthaxe
Pour les utiliser, la fonction doit elle-même être notée async
Voici ce que ça change :
Asynchronisme & Future
void main(List<String> arguments) async {
print('1');
final String value = await _getStringFromNetwork();
print('Getting value: $value');
print('3');
}
Future<String> _getStringFromNetwork() async {
await Future.delayed(Duration(seconds: 1));
return 'My String';
}Bien plus lisible sans toutes ces callbacks !
Asynchronisme & Future
Attention, la gestion d'erreur ne se gère alors plus avec "catchError" mais en faisant un try catch
void main(List<String> arguments) async {
print('1');
try {
final String value = await _getStringFromNetwork();
print('Getting value: $value');
} catch(error) {
print("Error getting value: $error");
}
print('3');
}Appels réseaux
appels, gestion de l'asynchronisme, etc.
dependencies:
flutter:
sdk: flutter
http: 0.13.0flutter packages getimport 'package:http/http.dart' as http;Notez le as http, en effet ce package est un ensemble de fonctions. Mettre un alias permet d'avoir accès à ces fonctions même si vous en avez avec le même nom
Soit une classe User comme celle-ci :
class User {
final String lastName;
final String firstName;
final String address;
User(this.lastName, this.firstName, this.address);
}class ApiServices {
static Future<List<User>> getUsers() async {
[...]
}
}final http.Response response = await http.get(Uri.parse(URL_TO_GET_USERS));Nous avons alors accès au statusCode de réponse, ainsi qu'à son body
if (response.statusCode != 200) {
throw Error();
}final jsonBody = json.decode(response.body);import 'dart:convert';class User {
final String lastName;
final String firstName;
final String address;
User(this.lastName, this.firstName, this.address);
factory User.fromJson(Map<String, dynamic> json) {
return User(
json["lastName"],
json["firstName"],
json["address"],
);
}
}Il suffit donc ensuite de retourner un User en le remplissant avec les champs de l'objet json
C'est de cette manière que nous traiterons les désérialisation json en Flutter
static Future<List<User>> getUsers() async {
final http.Response response = await http.get(
Uri.parse(URL_TO_GET_USERS),
);
if (response.statusCode != 200) {
throw Error();
}
final jsonBody = json.decode(response.body);
final List<User> users = [];
TODO: parse users !
return users;
}users.addAll((jsonBody as List).map((user) => User.fromJson(user)).toList());- convertir l'objet en liste car l'API nous renvoie une liste
- map sur chaque item
- renvoyer pour chaque item le User parsé
- convertir la Map ainsi crée en Liste
- ajouter cette liste à la liste des Users
Nous partons du principe ici que l'API nous renvoie une liste quoi qu'il arrive, vide ou non
Si ce n'est pas le cas, évidemment le code suivant plantera car la conversion sera impossible :
(jsonBody as List)Méfiez-vous, on est jamais trop prudents avec une API que nous ne contrôlons pas !
Le champs statusCode de la Response d'http nous permet d'avoir une indication d'erreur de la part de l'API mais il arrive qu'il y ait d'autres erreurs comme l'absence d'internet !
Pour ce faire, n'oubliez jamais d'entourer votre appel d'un try catch pour être sûr de gérer la bonne erreur !
static Future<List<User>> getUsers() async {
try {
final http.Response response = await http.get(
Uri.parse(URL_TO_GET_USERS),
);
if (response.statusCode != 200) {
throw Error();
}
final jsonBody = json.decode(response.body);
final List<User> users = [];
users.addAll((jsonBody as List).map((user) => User.fromJson(user)).toList());
return users;
} on SocketException {
print('No internet !');
throw NetworkError();
} catch (error) {
print('Another error');
throw OtherError();
}
}Comment lancer l'appel et afficher les résultats ?
Il y a à priori 4 résultats possible pour un appel donnant lieu à autant d'affichages différents :
- chargement
- erreur
- succès
- succès mais vide
Pour gérer ces états, il est possible d'utiliser un StatefulWidget comme ceci par exemple (le code est moche, c'est le principe qu'il faut comprendre) :
class _ExampleStatefulState extends State<ExampleStateful> {
List<User> _users = [];
bool _isLoading = false;
Error _error;
@override
void initState() {
super.initState();
_isLoading = true;
ApiServices.getUsers().then((List<User> users) {
setState(() {
_isLoading = false;
_users = users;
_error = null;
});
}).catchError((error) {
setState(() {
_isLoading = false;
_users = [];
_error = error;
});
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) return Center(child: CircularProgressIndicator());
if (_error != null) return Center(child: Text("ERROR"));
if (_users.isEmpty) return Center(child: Text("users empty"));
return ListView.builder(
itemCount: _users.length,
itemBuilder: (BuildContext context, int index) {
return UserItem(
user: _users[index],
);
},
);
}
}Cela rend cependant le code un peu compliqué juste pour un appel
Il existe un widget très très intéressant pour gérer tous les états d'une Future, c'est le FutureBuilder
Il peut être utilisé dans un StatelessWidget et permet de gérer tous les états sans tous les setState que nous avons écris avant
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: FutureBuilder(
future: ApiServices.getUsers(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Center(
child: CircularProgressIndicator(),
);
break;
case ConnectionState.done:
if (snapshot.hasError) {
return Center(
child: Text("Error: ${snapshot.error}"),
);
}
if (snapshot.hasData) {
final List<User> users = snapshot.data;
if (users.isEmpty) {
return Center(
child: Text("Empty list"),
);
}
return ListView.builder(
itemCount: users.length,
itemBuilder: (BuildContext context, int index) {
return UserItem(
user: users[index],
);
},
);
} else {
return Center(
child: Text("No data"),
);
}
break;
default:
return Container();
break;
}
},
),
),
);
}
}Navigation
Navigation
La navigation est une des parties importantes d'une application mobile.
En Flutter, c'est la MaterialApp qui va créer un InheritedWidget appelé Navigator qui va nous permettre de gérer la navigation entre écrans
La "Navigation" en Flutter est tout ce qui touche à la Stack des écrans.
Navigation
Petit disclaimer :
Il y a dorénavant en Flutter une autre façon de gérer la navigation avec ce qu'on appelle souvent "Navigator 2.0". Attention cependant, c'est plus dur à utiliser et surtout utile pour Flutter WEB, pas forcément pour le mobile, nous ne le verrons donc pas
Navigation
Rappel : Un écran n'est rien d'autre qu'un autre widget !
Nous avons pris l'habitude de le commencer par un Scaffold, mais ce n'est pas une obligation
Pour "pousser" un nouvel écran, nous allons utiliser la méthode push du Navigator comme ceci :
Navigator.of(context).push(..param..)Navigation
Le paramètre que prend la méthode push n'est pas directement le nouvel écran mais une Route
Nous allons utiliser la MaterialPageRoute qui est celle par défaut et qui permet d'avoir les animations natives iOS comme Android
Sachez que vous pourrez plus tard faire votre propre implémentation de Route pour des animations custom très facilement !
Navigation
Ainsi :
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => ScreenA(),
),
);
Navigation
Sur une très grosse application, le fait de faire ce genre d'appel ne permet pas d'avoir un seul endroit de visualisation de la navigation, on peut vite d'y perdre
Pour résoudre ce soucis, nous pouvons utiliser le champs routes de la MaterialApp afin de définir les routes de l'applications
Navigation
@override
Widget build(BuildContext context) {
return MaterialApp(
routes: {
'/': (BuildContext context) => Home(),
'ScreenA': (BuildContext context) => ScreenA(),
},
home: Home(),
);
}
}Attention, en utilisant cette Map de routes, il faut alors retirer le champs home et le remplacer par la route correspondante "/"
Navigation
Nous pouvons ensuite appeler n'importe quel route de n'importe où, par son nom !
Remplaçons le code précédent par :
Navigator.of(context).pushNamed('ScreenA');Notez cette fois-ci l'utilisation de pushNamed et non push
Navigation
Astuce pour ne pas avoir 2 chaînes de caractères séparées :
class ScreenA extends StatelessWidget {
static const String routeName = "ScreenA";
[code]
}Navigator.of(context).pushNamed(ScreenA.routeName);routes: {
'/': (BuildContext context) => Home(),
ScreenA.routeName: (BuildContext context) => ScreenA(),
},Navigation
Ok, maintenant dernière étape : comment envoyer des paramètres ?
Prenons un écran B qui prend un identifiant en paramètre pour l'afficher
Navigation
class ScreenB extends StatelessWidget {
static const String routeName = "ScreenB";
final int id;
const ScreenB({
Key? key,
required this.id,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("identifier is : $id"),
RaisedButton(
child: Text("Go back"),
onPressed: () {},
),
],
),
),
);
}
}Navigation
Lorsqu'on fait un push, qu'il soit nommé ou non, nous avons la possibilité d'y ajouter un paramètre dynamic :
Navigator.of(context).pushNamed(ScreenB.routeName, arguments: 42);Navigation
Pour récupérer ce paramètre, nous n'avons d'autres choix que d'implémenter le paramètre onGenerateRoute de la MaterialApp
Il s'agit d'une callback nous donnant des informations sur la requête (notamment le fameux argument) et nous demandant de renvoyer la route associée
L'argument étant dynamic, il est intéressant de toujours se méfier de ce qui est envoyé et de ne pas faire un cast trop dangereux :
Navigation
onGenerateRoute: (RouteSettings settings) {
switch (settings.name) {
case ScreenB.routeName:
return MaterialPageRoute(builder: (BuildContext context) {
final dynamic argument = settings.arguments;
int value = 0; // default value
if (argument is int) {
value = argument;
}
return ScreenB(
id: value,
);
});
break;
default:
return MaterialPageRoute(
builder: (BuildContext context) => Screen404(),
);
break;
}
},Navigation
Notez que nous avons ajouté un écran de 404 en défaut dans notre switch
En effet, grâce à cela, si une route est mal appelée ou si plus aucune route n'existe avec le nom demandé, ce sera une page précise qui sera appelée, la 404
Navigation
Comment revenir en arrière ?
RaisedButton(
child: Text("Go back"),
onPressed: () {
Navigator.of(context).pop();
},
),Malgré toutes ces différentes techniques, nous venons simplement de push une vue, il suffit donc de la pop de la stack !
Navigation
Global State Management
Global State Management
Il s'agit de l'un des sujets les plus importants lorsqu'on parle de Frameworks par composants comme Vue, React, Compose, SwiftUI... et Flutter !
Maîtriser ce sujet est capital dès lors que vous souhaitez développer une application correcte
Global State Management
Sur internet, vous allez trouver TOUT et n'importe quoi sur ce sujet et surtout de très nombreuses technos :
- Redux (React & React Native)
- BloC
- Provider / RiverPod
- Etc.
Et surtout : beaucoup de gens qui vous disent que leur techno est la meilleure...
Global State Management
Mais avant de commencer à comprendre COMMENT gérer ça, on va regarder POURQUOI c'est important
Flutter cours complet (30h)
By Ecalle Thomas
Flutter cours complet (30h)
Cours d'initiation au framework Flutter. Initialement prévu pour un cours de 15h pour des 3ème ou 4ème années MOC à l'ESGI.
- 535