Règles générales concernant :
Ce cours part du principe que vous avez des notions plus ou moins avancées dans les concepts suivants :
WebViews
WebViews++ (Cordova, Ionic, etc.)
React Native
Flutter
Les IDEs pour faire du Flutter sont les suivants :
Pour ce cours, nous partirons du principe que nous travaillons sur Android Studio
Il existe 2 principaux plugins à ajouter à votre IDE :
Voici le lien avec les consignes d'installation pour chaque OS :
flutter doctorFaites un coup de flutter channel, voici le résultat :
Ordre de stabilité : stable > beta > dev > master
Changer de channel :
flutter channel maChannelArborescence des fichiers
flutter create mon_applicationls -l mon_applicationandroid / ios
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 :
android / ios
Attention !
Par défaut, flutter créé votre projet avec comme langage :
Ceci est modifiable à la création comme cela:
flutter create -i objc -a javalib
lib
Nous y reviendrons plus tard
Ce dossier est celui dans lequel nous écrirons la totalité de notre code Dart/Flutter
pubspc.yaml/pubspec.lock
pubspc.yaml/pubspec.lock
Le fichier pubspec.yaml est l'un des fichiers les plus importants d'un projet Flutter
Nous y retrouvons:
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
test
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
web / windows
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
Test de l'application générée
Ouvrez l'application générée sur votre IDE
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
Lancez en cliquant sur la flèche verte
Vous pouvez aussi utiliser la ligne de commande :
flutter runTestons 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)
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
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
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);
}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
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");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
On va revenir sur ce concept de Nullable dans le chapitre suivant !
Alors que faire si vous ne voulez pas que votre paramètre nommé soit Nullable ?
Vous avez 2 choix :
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");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 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 !
class Knight {
final String name;
const Knight(this.name);
}
class Perceval extends Knight {
final String botteSecrete;
Perceval(String name, this.botteSecrete) : super(name);
}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 valeurL'intérêt d'un langage supportant le null-safety est énorme et bien connu des développeurs Kotlin ou encore Swift depuis longtemps :
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
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"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);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...
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-safetyVous 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
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
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) {
...
}
}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) {
...
}
}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,
),
),
);
}
}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
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,
),
),
);
}
}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 !
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,
),
),
);
}
}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
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.. !
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.
et la notion d'InheritedWidget
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 :
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,
),
),
);
}
}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
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.
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."
ce qui ressemble le plus à un "écran"
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 :
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
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 !
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,
),
),
);
}
}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)
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
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
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();
}
}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.
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,
),
),
),
);
}
}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),
),
),
),
),
);
}
}BON, passons aux choses sérieuses
Soit une variable color représentant l'état de la couleur du carré, nous pourrions écrire ceci :
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),
),
),
),
),
);
}
}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(...),
),
),
);
}
}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 !
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;
});
}Et voilà !
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)
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,
);
}
}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.
ROW
Permet de présenter les widgets sous forme de ligne
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(),
],
),
),
);
}
}COLUMN
Permet de présenter les widgets sous forme de colonne
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(),
],
),
),
);
}
}Les Columns et les Rows fonctionnent exactement pareil, mis à part leur orientation
Il est possible de jouer sur :
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
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(),
],
),
),
),
);
}
}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(),
],
),
),
),
);
}
}.start,.end,.spaceBetween,.spaceAround,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(),
],
),
),
),
);
}
}.start,.end,.stretch,STACK
Permet de positionner les widgets relativement les uns aux autres grâce aux widgets Positionned
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()
],
)
),
),
);
}
}Container(
color: Colors.amber,
width: 300,
height: 300,
child: Stack(
children: <Widget>[
Positioned(
right: 10,
bottom: 60,
child: RedSquare(),
),
],
),
),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(),
),
],
),
),Prendre en compte la taille de l'écran
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
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.)
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,
),
],
),
);
}
}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,
),
],
),
);
}
}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
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,
),
),
],
),
);
}
}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,
),
),
],
),
);
}
}Le widget Spacer
Spacer
Il est parfois nécessaire de réaliser ce type de vue :
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
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 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" :
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,),
],
);
}),
),
)
),
);
}
}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
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,),
],
);
},),
)
),
);
}
}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 :
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(),
);
},
),
),
),
);
}
}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();
},
),
),
),
);
}
}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();
},
),
),
),
);
}
}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
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
void main(List<String> arguments) {
print('1');
Future.delayed(Duration(seconds: 1), () {
print('2');
});
print('3');
}Ce code affichera :
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';
});
}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 :
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 !
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, 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());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 :
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;
}
},
),
),
);
}
}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.
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
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..)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 !
Ainsi :
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => ScreenA(),
),
);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
@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 "/"
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
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(),
},Ok, maintenant dernière étape : comment envoyer des paramètres ?
Prenons un écran B qui prend un identifiant en paramètre pour l'afficher
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: () {},
),
],
),
),
);
}
}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);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 :
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;
}
},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
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 !
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
Sur internet, vous allez trouver TOUT et n'importe quoi sur ce sujet et surtout de très nombreuses technos :
Et surtout : beaucoup de gens qui vous disent que leur techno est la meilleure...
Mais avant de commencer à comprendre COMMENT gérer ça, on va regarder POURQUOI c'est important