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

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 doctor

Qu'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 maChannel

Cré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 java

Analysons 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 run

Lanç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? email

Ce 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 pas

Les 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 valeur

Null-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-safety

Qu'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).textTheme

Ce 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.0
flutter packages get
import '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