Académique Documents
Professionnel Documents
Culture Documents
• Utilisez la syntaxe d'initialisation littérale lorsque vous maîtrisez l'implémentation par défaut donnée par le langage. Au
lieu de cela, pour des implémentations spécifiques, utilisez des constructeurs ou des usines classiques.
• Lorsque vous parcourez un conteneur, préférez faire for(item in list) {} au lieu d'utiliser la méthode forEach() qui ajoute
de la verbosité au code. La boucle est plus claire. Cependant, si vous disposez d'une fonction qui peut être référencée
telle que print, vous pouvez utiliser list.forEach(print);.
• Il existe une fonction appelée cast() dont nous n'avons jamais parlé car elle n'est pas bonne à utiliser. Ne faites pas de
conversions avec cast<T>() car cela ajoute de la verbosité et cela ne fait pas un travail soigné.
Laissez tomber.
Chaque conteneur propose une série de fabriques, telles que unmodifiable() pour renvoyer une collection sans opérations
6
d'ajout/suppression (c'est un conteneur en lecture seule). Reportezvous à la documentation pour une liste complète des
méthodes utilitaires.
Très brièvement, imaginez une table de hachage comme une table à deux colonnes : la clé est à gauche
et la valeur est à droite. La clé est nécessaire pour rechercher des valeurs car, si elle est présente, vous aurez
accès à l'objet associé : c'est pourquoi les cartes sont des paires clé/valeur.
5https://dart.dev/guides/langue/effectivedart/usage#collections
6https://api.dart.dev/stable/2.7.0/dartcore/dartcorelibrary.html
Les implémentations courantes de cartes et d'ensembles sont basées sur des tables de hachage et le code de
hachage détermine comment un élément doit être stocké dans le conteneur. Vous devez toujours remplacer l'opérateur
d'égalité et le hashCode dans vos classes.
• opérateur bool ==(Objet autre). Nous avons déjà vu comment le remplacer dans la version 5.3.
• int récupère le hashCode. Il existe plusieurs façons de remplacer correctement ce getter, mais qu'estce que c'est ?
Il est important qu'un entier différent soit renvoyé pour chaque valeur différente.
classe Test {
final int a ; final
int b ; Chaîne
finale c ; Test(ce.a,
ceci.b, ceci.c);
En général, c'est une bonne pratique de prendre chaque variable d'instance de la classe et d'effectuer une
série de multiplications avec des nombres premiers. De cette façon, chaque fois que vous créez une instance
du même objet, telle que Test(0, 1, "a"); vous obtiendrez toujours le même code de hachage. Tout autre objet
renverra une valeur différente.
Si vous aviez une classe avec beaucoup de variables membres, il y aurait certainement beaucoup de code passe
7
partout. Le package Equatable de Felix Angelov remplace automatiquement Operator== et hashCode dans votre
classe afin que vous n'ayez pas à vous soucier des multiplications et des comparaisons.
7https://pub.dev/packages/equatable
Chaîne finale c ;
Test(ce.a, ceci.b, ceci.c);
@passer outre
Il vous suffit de sousclasser Equatable et de remplacer le getter d'accessoires en lui transmettant chaque champ final de
votre classe. Le package ne fait rien de spécial en interne : il remplace Operator== (avec identique()) et hashCode (avec une
série de XOR de la même manière que ce que nous avons fait). C'est beaucoup moins de code à écrire !
class Test étend SomeClass avec EquatableMixin { final int a; final int b ;
Chaîne finale c ;
Test(ce.a, ceci.b,
ceci.c);
@passer outre
L'extension d'Equatable n'est peutêtre pas toujours possible car, par exemple, votre classe peut déjà avoir une superclasse
et Dart n'autorise pas l'héritage multiple. Dans ce cas, utilisez un mixin qui fait le même travail.
Si votre classe n'est PAS immuable, car tous les champs d'instance ne sont pas définitifs, ne
remplacez PAS Operator== et hashCode. Remplacer hashCode par un objet mutable pourrait
interrompre les collections basées sur le hachage. Ceci est également écrit dans les directives officielles
8
de conception .de Dart
Si votre classe est mutable, ne définissez pas de logique d'égalité personnalisée car elle pourrait interrompre les collections
basées sur le hachage ; pour les mêmes raisons, n'utilisez pas Equatable si votre classe n'est pas immuable.
8https://dart.dev/guides/langage/effectivedart/design#avoiddefiningcustomequalityformutableclasses
Les collections vous offrent un très bon moyen de filtrer les données et d'agir sur elles avec une série de méthodes qui
peuvent être enchaînées. Il existe des similitudes en Java avec les flux et en C# avec les requêtes LINQ.
void main() { //
Génère une liste de 20 éléments en utilisant une liste finale d'usine
= List<int>.generate(20, (i) => i);
Dans cet exemple, nous créons une liste contenant des nombres de 0 à 19 à l'aide du constructeur de fabrique de
génération. La partie intéressante est la façon dont nous en avons construit un autre pour qu'il ne contienne que des
chaînes représentant des nombres pairs.
2. La méthode map() transforme un type en un autre. Puisque other doit être une liste de chaînes, nous transformons
chaque élément filtré (représenté par une valeur int ) en une valeur String .
3. Maintenant que nous disposons d'une liste filtrée de valeurs transformées, la fonction terminal renvoie un
instance d'une liste.
Effectuer manuellement ce type d'opération est en fait verbeux car vous devez créer une fonction avec des variables
temporaires, des boucles et des instructions conditionnelles. Cette syntaxe est plutôt élégante et très facile à comprendre.
Pendant que vous parcourez une collection avec ces méthodes, ne modifiez PAS le contenu du
conteneur luimême ! Si vous avez une liste de chaînes, ne modifiez pas chaque élément appelant, par
exemple, toUpper() lorsque vous êtes dans une condition Where.
L'exemple est affiché avec une liste mais ces méthodes sont également disponibles pour les ensembles et les cartes. Il existe
deux types d'opérations : les opérations "intermédiaires" pour traiter les données et les opérations "terminales" pour renvoyer
des valeurs. Les intermédiaires sont destinés à élaborer des données et peuvent être enchaînés tandis que les terminaux
sont appelés à la fin pour « regrouper » les données.
• Intermédiaires. Il s'agit d'une catégorie de fonctions qui peuvent être chaînées comme vous l'avez vu cidessus
pour créer des expressions complexes. La plupart d'entre eux acceptent une fonction dont le paramètre est
l'élément de la collection auquel on accède.
–where() : parcourt toute la liste et ignore les éléments qui évaluent la condition
c'est faux.
• Terminaux. Il s'agit d'une catégorie de fonction qui ne peut être appelée qu'en fin de chaîne
pour renvoyer une valeur ou un objet.
– toList()/toSet()/toMap() : rassemble les données élaborées via les "pipes" et renvoie une instance d'une
liste/ensemble/carte.
– contain() : renvoie vrai ou faux si la collection contient ou non l'objet que vous recherchez.
– réduire() : réduit une collection à une valeur unique qui peut être le résultat d'opérations dans les éléments
du conteneur. Vous ne pouvez pas utiliser réduire() sur des collections vides. Par exemple:
imprimer(somme); // 15
La variable sum contient la somme des éléments de la liste puisque réduire((a,b) => c) prend 2 éléments
de la source (a, b) et exécute l'action donnée sur eux (dans ce cas, elle additionne les valeurs).
pli(). C'est très similaire à réduire() mais il demande une valeur initiale et le retour
le type ne doit pas nécessairement être le même que celui de la collection.
imprimer(somme); // 15
Réduire() et plier() peuvent faire les mêmes choses mais ce dernier est plus puissant. Tout d'abord,fold()
peut définir une valeur initiale personnalisée pour les opérations :
final sum1 = list.fold(0, (int a, int b) => a + b); final sum2 = list.fold(5,
(int a, int b) => a + b);
imprimer(somme1); // 15
print(somme2); // 20
Avec Fold(), vous pouvez effectuer des opérations sur différents types de données, alors qu'avec Reduction(), vous ne le pouvez pas.
Dans cet exemple, nous calculons la somme des longueurs de chaînes d'une collection.
valeur finale = list.fold(0, (int count, String item) => count + item.length); imprimer(valeur); // dix
count a le même type de valeur initiale (0 dans ce cas, qui est un int) et item représente un objet de la
collection. La valeur renvoyée par la fonction doit correspondre au type de la valeur initiale. Vous ne pouvez
pas faire la même chose autrement :
// Il ne compile pas
list.reduce((String a, String b) => a.length + b.length); imprimer(valeur);
Cette version ne fonctionne pas car réduire() s'attend à ce que le type de retour du rappel soit une chaîne,
le même type que le conteneur. Avecfold() vous n'avez pas cette contrainte : cela fonctionnera toujours. En
réalité, réduire() peut être vu comme un raccourci de ce qui suit :
Les deux versions sont équivalentes mais withReduce est juste plus court. Nous vous encourageons fortement à
utiliser cette syntaxe fluide lorsque vous devez travailler sur des collections plutôt que d'utiliser des variables
temporaires et/ou des instructions conditionnelles.
7 | Programmation asynchrone
7.1 Présentation
De nos jours, les ordinateurs et les appareils mobiles sont très rapides et les utilisateurs en sont bien conscients ; ils détestent
quand l'application « se fige » pendant un moment ou si elle ne réagit pas toujours immédiatement aux entrées.
Il existe cependant certaines situations dans lesquelles l'utilisateur doit attendre :
• l'utilisation de la connexion Internet, qui pourrait être lente et donc l'ensemble du processus pourrait prendre plus de temps
que prévu ;
• De nombreuses opérations d'E/S peuvent ralentir les performances de votre application en raison des politiques adoptées.
par le système d'exploitation.
Dans Flutter par exemple, vous utilisez souvent une connexion Internet et, dans le pire des cas, l'utilisateur doit attendre
quelques secondes. Vous devez utiliser une programmation asynchrone pour afficher quelque chose comme une barre de
progression animée tandis que, en même temps, les données sont traitées en arrièreplan.
Nous en parlerons dans la partie Flutter mais l'idée est que l'application ne doit jamais s'arrêter à une seule
tâche longue. La programmation asynchrone est conçue pour exécuter des opérations chronophages en arrière
plan afin que, entretemps, nous puissions faire autre chose.
Disons que vous travaillez en équipe et qu'un de vos collègues vous envoie cette fonction qui va être utilisée très souvent dans
votre application.
}
}
retourner httpGetRequest(valeur);
}
Supposons que les deux boucles imbriquées puissent prendre jusqu'à 2 secondes et que la requête réseau à la fin ajoute
d'autres centaines de millisecondes. Bien sûr, cette fonction est lente car elle renvoie l' int après un certain temps (de l'ordre
de quelques secondes).
vide main() {
données finales = processData(1, 2.5);
imprimer (données);
La fonction main() va être "bloquée" pendant quelques secondes en raison du long temps d'exécution de
processData. L'ensemble du flux est bloqué en raison d'un goulot d'étranglement produit par un appel de
fonction et l'application ellemême semble gelée.
Un Future<T> représente une valeur ou une erreur qui sera disponible dans le futur. Cette classe générique
doit être utilisée chaque fois que vous travaillez avec des fonctions chronophages renvoyant un résultat après
un laps de temps notable. Voici ce que vous pouvez faire pour affiner facilement votre flux d'exécution :
}
}
C'est presque identique au code original : nous venons de changer le type de int en Future<int> et l'instruction
finale, qui utilise un constructeur nommé de Future pour renvoyer une nouvelle instance. Bien sûr, vous devez
être sûr que l' objet Future<T>.value() est construit avec le type approprié.
Le code n'a pas beaucoup changé mais il y a une énorme différence dans la façon dont la
fonction sera appelée. De plus, chaque fois que vous voyez Future<T> comme valeur de retour,
vous en comprenez immédiatement l'utilisation et vous écrivez ainsi votre code en conséquence.
Le rappel then() est appelé une fois l’exécution terminée, lorsque la valeur est prête à être utilisée. Grâce à un
Future<T>, vous pouvez exécuter la tâche fastidieuse en arrièreplan et être informé de l'achèvement via then().
Les méthodes peuvent être enchaînées ; la capture des exceptions potentielles levées lors de l'exécution en
arrièreplan se produit via catchError(), qui est l'équivalent d'un bloc try catch . Bien entendu vous pouvez
créer des chaînes plus complexes telles que :
Il n'y a pas de limites, vous pouvez toujours ajouter un then() ou un catchError(). Vous avez peutêtre remarqué que cette
approche est assez verbeuse et n'est pas si facile à lire lorsque de nombreuses méthodes sont enchaînées.
C'est la raison pour laquelle async et wait doivent être votre choix principal ; ils réduisent considérablement
la verbosité, ce qui rend le code presque identique à son homologue synchrone.
Dans certains cas, vous souhaiterez peutêtre attendre la fin d'une série de Future<T>, mais vous ne souhaitez toujours pas
bloquer l'exécution. C'est le cas d'utilisation parfait pour Future.wait<T>().
Future.wait<int>([
un,
deux,
trois
]).then(...).catchError(...);
La méthode wait() prend une liste de Future<T>, les exécute et attend que tout le monde ait fini. Vous pouvez enchaîner
then() et/ou catchError() car wait() renvoie un Future<T>.
L'API est très riche en constructeurs nommés utiles que vous pouvez utiliser :
• Future<T>.delayed()
Crée un objet Future<T> qui commence à s'exécuter après le délai donné (dans ce cas, il s'exécute après 1 seconde).
• Future<T>.erreur()
Crée un objet Future<T> qui se termine par une erreur. Outre le message, vous pouvez également passer en
deuxième paramètre la trace de la pile.
• Future<T>.value()
Crée un objet Future<T> qui termine immédiatement en renvoyant la valeur donnée. Fondamentalement,
ce constructeur est utilisé pour « envelopper » une valeur non future dans une valeur future.
• Futur<T>.sync()
Crée un objet Future<T> qui appelle immédiatement le rappel donné. Généralement, lorsque vous appelez
then(), vous ne savez pas quand son corps sera exécuté. Dans ce cas, vous savez que le rappel est appelé
immédiatement. Ce constructeur est destiné à être utilisé lorsqu'un Future<T> doit s'exécuter immédiatement
mais en pratique, il y a très peu d'usages (nous en verrons un en 13.2.2 par exemple).
7.2.1 Comparaison
Dans cette section, nous comparons un extrait de code synchrone, qui utilise un int « simple », et sa version asynchrone,
qui utilise un Future<int>, pour mettre l'accent sur les différents comportements.
void main()
{ données finales = processData(31, 2.5); print("
résultat fonctionnel = $données");
Rien de difficile à comprendre jusqu'ici, on peut facilement prédire ce que la console va sortir (10 est juste là à
titre d'exemple, ce n'est pas grave) :
résultat fonctionnel = 10 ;
L'avenir est brillant
Les deux instructions print() sont exécutées séquentiellement (comme d'habitude) mais elles apparaissent après
quelques secondes. Il y a un délai visible qui "gèle" temporairement le programme car
void main()
{ processus final = processData(1, 2.5);
process.then((data) => print("result = $data"));
Avec l'utilisation d'un objet Future<T>, le flux d'exécution n'est plus bloqué. Le rappel then(...) revient immédiatement
afin que d'autres opérations puissent avoir lieu ; son corps sera exécuté plus tard, une fois que les données seront
réellement prêtes. Si tu avais écrit...
});
résultat = 10 ;
L'avenir est brillant
... comme dans le premier exemple. Le corps de then() s'exécute de manière synchrone dans notre exemple afin
que les opérations soient exécutées dans l'ordre.
Dans un rappel then(), vous pouvez également exécuter du code asynchrone, mais il est difficile à lire, en raison de la
verbosité du code, et difficile à comprendre, au cas où il y aurait trop d'asynchronie dans le flux.
Pour le dire très simplement, lorsque vous utilisez then() vous dites à Dart : "Continuez à faire votre
travail, je ne veux pas attendre la fin de l'opération. Lorsque le résultat sera prêt, prévenezmoi avec le
rappel " .
Si vous devez gérer des opérations fastidieuses, l'utilisation d'un Future<T> est fondamentalement indispensable car
bloquer le flux d'exécution est dangereux et erroné. Grâce au code asynchrone, vous gardez votre application toujours
occupée et réactive, ce qui est un facteur fondamental de l'expérience utilisateur.
• Utilisation alors.
void main()
{ processus final = processData(1, 2.5);
process.then((data) => print("result = $data"));
}
Les extraits cidessus sont équivalents car le résultat est le même mais la syntaxe est différente.
Commençons par trois faits très importants :
1. Vous ne pouvez utiliser wait que dans une fonction marquée async.
2. Pour définir une fonction asynchrone, placez le motclé async avant le corps.
Notre main() a le modificateur async afin que nous soyons autorisés à appeler wait. L'appel de wait sur un Future
déplace l'exécution en arrièreplan et se poursuit une fois le calcul terminé (ce qui est exactement ce que fait then()).
Pour être clair, écrire...
... car les lignes après le motclé wait ne sont exécutées que lorsque le Future<T> est terminé (sans blocage). Par
rapport à l'exemple précédent, ce code...
void main()
{ processus final = processData(1, 2.5);
process.then((data) { print("result
= $data"); print("L'avenir est
radieux");
});
}
void main()
{ processus final = processData(1, 2.5);
process.then((data) { print("result
= $data");
});
print("L'avenir est radieux");
}
car tout après wait n'est exécuté que lorsque le Future est terminé. Vous êtes assuré que les fonctions sont exécutées
de manière asynchrone et vous ne bloquerez pas le flux d'exécution normal.
Les exceptions sont également plus faciles à détecter car...
void main()
{ processData(1,
2.5) .then((result) => print(result)) .catchError((e)
=> print(e.message));
}
}
La deuxième version est plus proche de ce que l'on a l'habitude de voir dans le monde synchrone traditionnel.
De plus, il n'y a pas de méthodes/rappels imbriqués et le code est donc beaucoup plus court et plus lisible.
}
}
S'il n'y avait pas d' attente, cela ressemblerait à du code synchrone "normal" sans aucun rappel. C'est plus net et plus lisible
si on le compare à ce qui suit :
1https://dart.dev/guides/langue/effectivedart/usage#asynchrony
anotherRequest(data).then((otherData) {
renvoyer d'autresDonnées ;
}); }).catchError((e) {
print(e.message); retour "";
});
}
Ce n'est pas une question d'efficacité ou de performances car les deux exemples sont bons. Le problème consiste
à écrire du code avec potentiellement de nombreux rappels et fonctions imbriqués qui deviennent impossibles à
lire (ce qu'on appelle « l'enfer des rappels »).
Encore une fois, grâce à wait, le code asynchrone peut être écrit de la même manière que le code synchrone.
En plus de conduire à moins de passepartout, il est également plus facile à lire, à maintenir et à comprendre !
Le renvoi d'un Future<T> à partir d'une fonction peut être effectué via le constructeur nommé Future.value() ou, plus facilement,
en rendant la fonction asynchrone et en renvoyant la valeur simple. Le compilateur effectuera une conversion automatique.
Les deux méthodes sont valables, mais vous devriez peutêtre préférer la deuxième approche car elle est un peu moins
verbeuse.
7.3 Flux
Dans Dart, un flux est une séquence d'événements asynchrones ou synchrones que nous pouvons écouter. Il n'est pas nécessaire
de vérifier les mises à jour car le flux nous avertit automatiquement lorsqu'un nouvel événement est disponible.
Cette image vous aide à visualiser comment les flux sont destinés à être utilisés et qui sont les principaux acteurs impliqués.
Un générateur est une source d'informations qui génère paresseusement de nouvelles données avec une certaine
fréquence ; le flux est le canal dans lequel les données générées circulent.
• Flux. C'est l'endroit dans lequel circulent les données générées. Vous pouvez commencer à écouter un
stream afin que, lorsque le générateur émet de nouvelles données, vous en soyez averti.
• Les abonnés. Un abonné est une personne intéressée par les données circulant dans le flux. Si de nouvelles données
sont envoyées sur le flux par le générateur, toutes les personnes qui écoutent (abonnés) en seront informées.
Un générateur doit émettre des données uniquement dans un flux et nulle part ailleurs car un flux est la seule référence à
laquelle les auditeurs peuvent s'abonner. Il existe deux types de générateurs :
1. Générateurs asynchrones : ils renvoient un objet Stream<T>. Pour cette raison, vous devez gérer un flux de données
asynchrone qui doit être géré par un abonné avec wait.
2. Générateurs synchrones : ils renvoient un objet Iterable<T>. Pour cette raison, vous devez gérer un flux de données
synchrone qui peut être traité en boucle car les données sont envoyées dans un ordre séquentiel.
Un développeur Flutter est habitué à travailler avec Stream<T> car le framework comporte de nombreuses asynchrones.
générateurs chronologiques. En pratique, à moins que vous ne créiez un package ou un outil spécifique, vous ne créerez
pas de générateurs trop souvent, mais vous devez néanmoins au moins être conscient de leur fonctionnement général.
Les contrats à terme et les flux sont à la base du modèle asynchrone de Dart.
}
} // 5.
1. Puisque la fonction est un générateur d'événements asynchrones (nombres aléatoires), le type de retour
doit être de type Stream<T>. Le modificateur async* permet d'utiliser le rendement pour émettre des données.
3. Le constructeur nommé Future.delayed(...) crée un Future qui revient après un certain délai donné par l'
objet Duration2. Il est utilisé pour « endormir » le flux d'exécution pendant un certain temps sans le bloquer.
4. Le motclé rendement pousse les données sur le flux. Il se charge d'envoyer les nouveaux événements sur
le flux et cela ne modifie pas la boucle (il continue de cycler régulièrement).
5. Lorsqu'une fonction a le modificateur async* , il ne peut pas y avoir d'instruction return . Ce serait également
logiquement faux car les données sont déjà envoyées sur le flux par rendement et vous n'auriez donc rien
à retourner lorsque le générateur "s'éteint".
Ainsi les générateurs sont créés avec le modificateur async* et les événements sont émis sur le flux avec le mot clé
rendement . Pour faire une comparaison, voici la version synchrone du générateur qui a une structure similaire.
Le modificateur sync* star indique au compilateur que cette fonction est un générateur synchrone. En raison de
sa nature synchrone, vous ne pouvez pas utiliser de futures et nous devons donc utiliser sleep() au lieu d'attendre
Future.delayed().
Les générateurs asynchrones sont destinés à être utilisés avec du code asynchrone.
Les générateurs synchrones sont destinés à être utilisés avec du code synchrone.
Intuitivement, si votre code doit attendre quelque chose, vous aurez besoin d'un générateur asynchrone.
Les deux types de générateurs commencent à émettre des données à la demande, ce qui signifie que des
valeurs sont produites lorsqu'un auditeur commence à itérer sur Iterator<T> ou commence à écouter Stream<T>.
Il peut y avoir plusieurs instructions de rendement dans le même bloc et cela pousserait simplement plusieurs
valeurs, en séquence, sur le flux :
Ce code émet 3 nombres aléatoires à chaque itération donc le flux va générer 300 valeurs avant de se terminer.
Les flux peuvent également être créés à l'aide d'une série de constructeurs nommés utiles pour une
« configuration rapide » :
• Stream<T>.périodique()
Crée un nouveau flux qui émet à plusieurs reprises des événements à l'intervalle de durée donné.
L'argument de la fonction anonyme commence à 0 puis s'incrémente de 1 pour chaque événement
émis (c'est un "compteur d'événements").
• Stream<T>.value()
• Stream<T>.erreur()
Crée un nouveau flux qui émet un seul événement d'erreur avant de terminer ; le comportement est très
similaire à Stream<T>.value().
• Stream<T>.fromIterable()
Crée un nouveau flux à abonnement unique qui émet uniquement les valeurs de la liste.
• Stream<T>.fromFuture()
);
Crée un nouveau flux à abonnement unique à partir de l' objet Future<T> donné. En particulier, lorsque le
Future<T> se termine, 2 événements sont émis : un avec les données (ou l'erreur) et un autre pour signaler
que le flux est terminé (l'événement "done").
• Stream<T>.empty()
Ce type de flux ne fait rien : il envoie juste immédiatement un événement « done » afin de signaler la fin.
Vous devriez vraiment consulter la documentation en ligne Stream<T> pour une référence complète de toutes les
méthodes auxquelles vous pouvez faire appel. Les plus « populaires » sont par exemple :
• drain(...) : il rejette tous les événements émis par le flux mais signale quand il est soit
effectué ou une erreur s'est produite ;
• map(...) : transforme les événements du flux courant en événements d'un autre type ;
7.3.2 Abonnés
Le générateur est maintenant prêt à émettre périodiquement de nouveaux nombres aléatoires et nous pouvons ainsi nous
abonner pour être averti des nouvelles valeurs.
print("Flux asynchrone!"); // 4.
1. Puisque nous avons affaire à un flux asynchrone, il est nécessaire de marquer la fonction avec async car
nous allons bientôt attendre .
2. Nous nous abonnons au flux en obtenant simplement une référence à celuici. C'est le moment où le
générateur commence à émettre des données parce que quelqu'un vient de commencer à écouter
(initialisation à la demande).
3. Lorsqu'il s'agit de flux, la boucle wait for est capable de "capter" les valeurs envoyées sur le flux par un
rendement dans le générateur. Cela fonctionne exactement comme une boucle for normale .
Pour être précis, l'exemple est presque bon car les générateurs sont généralement placés dans des classes et
exposés avec un getter, qui renvoie une instance Stream<T>, ou avec une méthode Listen() qui effectue un
abonnement manuel. Jetons un coup d'œil à la version synchrone du générateur :
Hormis un simple for au lieu d'une attente, le code est identique. Les classes List<T> et Set<T> sont toutes deux
Iterable<T>, exactement comme la plupart des types de la bibliothèque de collections. Les exceptions sont
détectées de la manière habituelle :
}
} sur Quelque chose catch (e)
{ print("Whoops :(");
}
}
Outre les événements de données, un flux peut également envoyer des événements d'erreur, qui peuvent se produire
lorsqu'une exception a été levée dans le générateur. Voici un autre exemple de flux envoyant continuellement des
données à un intervalle donné :
while (true) { if
(count == maxCount) {
casser;
}
wait Future.delayed(délai); rendement +
+compte ;
}
}
}
}
Chaque seconde, un nouveau numéro est imprimé sur la console jusqu'à ce que maxCount soit atteint ; si
vous voulez que la boucle dure éternellement, supprimez simplement la condition if . Pour une couverture
encore plus détaillée des flux, consultez la chaîne YouTube officielle de Flutter ainsi que la documentation
qui regorge de détails et d'exemples :
2. Générateurs : https://dart.dev/articles/libraries/creatingstreams
Dans Flutter, l' objet StreamBuilder<T> est utilisé pour s'abonner à un flux et nous allons utiliser
7.3.3 Différences
Les principales différences entre les flux asynchrones et synchrones sont résumées dans ce court tableau :
Asynchrone Synchrone
Les abonnés doivent utiliser wait pour Les abonnés doivent utiliser pour
Dans les deux cas, le rendement est utilisé pour envoyer des données sur le flux et il ne doit pas y avoir d' instruction return
dans la fonction. Vous vous demandez peutêtre : "Si je voulais écrire un générateur, devraisje le faire
synchrone ou asynchrone ? »
Comme nous l'avons déjà dit, vous avez rarement besoin d'écrire un générateur si vous travaillez avec
Flutter, à moins que vous ne fassiez quelque chose de spécifique. Dans la plupart des cas vous vous abonnerez
aux flux afin que vous n'ayez rien à décider car le générateur est exposé par
la bibliothèque.
Une réponse raisonnable à la question serait « ça dépend » car il y a des cas et des cas. Comme
une ligne directrice générale, nous pouvons dire qu'un bon aspect décisionnel est de savoir si vous devez utiliser Future<T>
ou non.
• Si vous devez gérer des Future<T> en raison de l'utilisation du réseau ou des opérations d'E/S pour
Par exemple, vous êtes obligé d'utiliser un générateur asynchrone sinon vous ne pouvez pas utiliser wait.
• Lorsque vous n'avez pas à gérer l'asynchronie et que vous avez besoin d'envoyer une série de
données séquentielles, optez pour un flux synchrone.
Parfois, notamment lors de l'utilisation de la bibliothèque flutter_bloc que nous aborderons au chapitre 11, cela peut être
utile pour diviser la logique d'un Stream<T> en plusieurs morceaux. C'est le cas où le rendement* est requis :
rendement
0 ; rendement* evenNumbersUpToTen();
rendement
0 ; } else
{ rendement
1 ; rendement* oddNumbersUpToTen();
rendement 1 ;
}
}
Fondamentalement, rendement* est utilisé pour "mettre en pause" l'exécution et commencer à émettre des valeurs depuis
l'autre flux ; une fois terminé, le flux source est "redémarré" afin qu'il puisse à nouveau envoyer régulièrement ses valeurs.
Pour être plus clair, voici un exemple de ce qui se passe lorsque numberGenerator(true) est appelé :
• La valeur 0 est émise avec Yield, la manière "normale" d'envoyer des données sur le flux.
• En raison du rendement*, numberGenerator fait une pause et commence à émettre des valeurs générées à partir du
autre flux (evenNumbersUpToTen).
En pratique, rendement* est utilisé pour dire "arrêtez ici, émettez les valeurs de l'autre flux et quand il est terminé, vous êtes
libre de redémarrer votre flux régulier". Ce modèle est utilisé lorsqu'un flux doit utiliser en interne un autre flux pour diviser la
logique en plusieurs fonctions afin de rendre le code plus lisible.
Le flux est démarré dès que nous faisons le stream = someStream(); affectation. Il s'agit d'une manière
manuelle de créer des flux, mais elle ne s'adapte pas bien aux applications plus volumineuses. Surtout
dans Flutter, vous découvrirez que StreamController<T> est un moyen plus pratique de travailler avec des flux.
Nous allons créer un flux plus complexe qui produit périodiquement des nombres aléatoires.
/// Expose un flux qui génère en continu des nombres aléatoires class RandomStream {
Minuteur? _minuteur;
tard int _currentCount ; fin
StreamController<int> _controller;
/// Gère un flux qui génère en continu des nombres aléatoires. Utilisez /// [maxValue] pour définir la
valeur aléatoire maximale à générer.
RandomStream({this.maxValue = 100}) {
_currentCount = 0 ;
_controller = StreamController<int>( onListen :
_startStream, onResume :
_startStream, onPause :
_stopTimer, onCancel :
_stopTimer
);
}
Un timer peut être arrêté avec Cancel(). Nous l'utilisons pour pousser chaque seconde de nouveaux nombres aléatoires dans le
flux, gérés par StreamController<T>. Cette classe est essentiellement un wrapper autour d'un flux avec de nombreuses
fonctionnalités permettant d'envoyer facilement des données, des erreurs et des événements terminés.
• onListen : ce callback est appelé lors de l'écoute du flux (nouvel abonnement effectué).
• onCancel : ce rappel est appelé lorsque le flux est annulé (abonnement annulé).
• onPause : ce rappel est appelé lorsque le flux est en pause (abonnement en pause).
Un StreamController<T> est un outil très pratique pour gérer facilement un Stream<T>. Grâce à get stream, nous exposons
à l'extérieur une référence au flux afin que les auditeurs puissent s'abonner et recevoir des événements. Voici comment
nous avons défini les rappels du contrôleur :
vide _startStream() {
_timer = Timer.periodic(const Duration(secondes : 1), _runStream); _currentCount = 0 ;
void _stopTimer()
{ _timer?.cancel();
_controller.close();
}
}
}
Nous avons déclaré Timer? _timer comme variable nullable car nous ne pouvons pas initialiser immédiatement le
timer dans le constructeur. Cela serait une erreur car les événements commenceraient à être émis sur le flux dès le
début, même s'il n'y a pas d'auditeurs !
RandomStream({this.maxValue = 100}) {
_currentCount = 0 ;
_controller = StreamController<int>(...);
// FAUX! De cette façon, le timer est démarré et donc les événements sont // immédiatement
émis sur le flux (même si personne n'écoute) _timer = Timer.periodic(...);
Nous utilisons en toute sécurité le ?. opérateur pour accéder à la variable nullable. Dans _startStream(), nous
initialisons en fait le minuteur afin qu'il envoie de nouvelles valeurs aléatoires toutes les secondes. Le traitement
réel est effectué dans _runStream() :
// Lorsque la valeur maximale est atteinte, nous devons arrêter à la fois le // timer ET
fermer le contrôleur pour arrêter le flux. si (_currentCount == maxValue) {
_stopTimer();
}
Nous pouvons maintenant jouer avec notre classe RandomStream. Dans cet exemple, nous nous abonnons
au flux en utilisant Listen() puis nous annulons l'abonnement après un certain délai. Vous verrez que des
nombres aléatoires ne sont imprimés sur la console que 3 fois au total.
imprimer (aléatoire);
});
Au bout de 2 secondes, on s'abonne au flux en utilisant Listen() mais les numéros ne sont imprimés que trois fois car, 3
secondes plus tard, on annule l'abonnement. Comme vous pouvez le constater, StreamController<T> est plus complexe à
utiliser mais plus puissant et évolutif : c'est le moyen privilégié pour travailler avec des flux dans Dart et Flutter.
7.4 Isolats
De nombreux langages de programmation populaires tels que Java et C# disposent d'une API très large pour fonctionner
avec plusieurs threads et des calculs parallèles. Ils peuvent gérer des scènes multithread complexes grâce aux différentes
primitives qu'ils prennent en charge. Cependant, Dart n'a aucun des éléments suivants :
• il n'y a aucun moyen de démarrer plusieurs threads pour des calculs lourds en arrièreplan ;
• il n'existe pas d'équivalent, par exemple, aux types threadsafe tels qu'AtomicInteger ;
• il n'y a pas de mutex, sémaphores ou autres classes pour empêcher les courses aux données et tout ça
problèmes découlant de la programmation multithread.
Le code Dart (et donc les applications Flutter) est exécuté dans un isolat qui possède sa propre zone de mémoire privée et
une boucle d'événements. Un isolate peut être considéré comme un thread spécial dans lequel une boucle d'événements
traite les instructions. Si vous n'êtes pas familier avec ces concepts, nous allons détailler pour vous ce qui se passe :
Tout programme s'exécute dans un processus qui peut être composé d'un ou plusieurs threads. Certains langages de
programmation (image de gauche) vous permettent de créer manuellement plusieurs threads pour exécuter des tâches de
longue durée en "arrièreplan" afin de ne pas bloquer l'interface utilisateur.
Tous les threads vivant sur un processus partagent la même mémoire. Vous devez en être conscient car
écrire les mêmes données, au même moment, dans la même zone mémoire peut conduire à des situations
problématiques appelées courses aux données.
Dans Dart, un processus est composé d'un ou plusieurs isolats contenant une boucle d'événements. Contrairement aux threads
classiques, chaque isolat alloue sa propre zone mémoire afin qu'il n'y ait aucun problème de partage de données. En d’autres
termes, la principale différence est que les threads partagent la même mémoire, contrairement aux isolats.
Grâce à ce fait, Dart n'a besoin d'aucune primitive de synchronisation de données puisque des problèmes tels que des courses
de données ne peuvent jamais se produire par défaut. Si on faisait un zoom sur un isolat, cela ressemblerait à ceci :
Les rectangles d'événements blancs peuvent correspondre à des opérations d'E/S sur disque, des requêtes
HTTP, des actions déclenchées par une pression du doigt dans le framework Flutter, etc. L'engrenage de droite
est la boucle d'événements, une sorte de machinerie qui exécute des événements en continu. Vous vous
demandez peutêtre : s’il n’y a qu’un seul thread, comment le code asynchrone estil exécuté ? Jetons un coup
d'œil à ces 2 exemples simples :
1. Disons que quelque part dans notre programme Dart (ou application Flutter), il existe deux méthodes qui
sont appelées en séquence. Ils sont synchrones, car à l’intérieur ils n’utilisent aucun code asynchrone
(pas de Stream<T> ou Future<T>).
La boucle d'événements traite les événements entrants dans l'ordre un par un, de sorte qu'elle exécute
d'abord l'opération d'E/S, puis le calcul. Voici une représentation visuelle de la situation :
Le deuxième événement n'est traité que lorsque le premier est terminé. S'il n'y avait aucun événement disponible,
la boucle d'événements serait "inactive" en attendant qu'un nouveau travail soit effectué. La boucle d'événements
est le "moteur" qui exécute réellement le code Dart que vous avez écrit.
2. Voyons maintenant un autre exemple dans lequel un Future<T> est impliqué afin de comprendre comment le code
asynchrone est traité. La même stratégie est également appliquée lorsqu'il s'agit de
ruisseaux.
imprimer(nom);
}
Comme vous le savez déjà, ce qui vient après wait n'est exécuté que lorsque Future<T> est terminé. Dans ce
cas, la valeur ne sera imprimée que lorsque la requête HTTP sera terminée.
Faites semblant d'avoir ce code :
printName();
heure finale = getTime();
Le premier événement à exécuter est printName() mais comme il appelle en interne wait, ce qui vient après
(l'instruction print(name)) est séparé et ajouté plus tard en tant que nouvel événement dans la file d'attente ! Voici
la séquence réelle qui sera traitée :
En pratique, les appels asynchrones sont divisés en plusieurs événements : la partie synchrone et les rappels.
Ce qui vient après une attente n'est pas exécuté immédiatement car il est nécessaire d'attendre la fin du
Future<T>. Afin de ne pas perdre de temps, le rappel est séparé de l'événement, "mémorisé" et ajouté à la file
d'attente plus tard (une fois le Future<T> terminé).
// Cette partie est exécutée immédiatement ; c'est le rectangle le plus à droite de // l'image final int id =
generateId(); nom
de chaîne final = attendre
HttpModel.getRequest(id);
// Ce rappel est exécuté ultérieurement ; c'est le rectangle le plus à gauche de l'image //. Cette partie
"séparée" et ajoutée plus tard dans la boucle d'événements // pour terminer l'exécution print(name);
Le fractionnement des appels de fonction est fondamental car cela évite aux événements de la file d'attente
d'attendre la fin des contrats à terme. Si printName() avait été exécuté entièrement, la boucle d'événements
aurait été bloquée jusqu'à ce que Future<T> soit terminé et les autres événements auraient dû attendre.
La boucle d'événements doit toujours être occupée mais elle ne doit pas exécuter d'événements de longue durée, sinon
d'autres seront bloqués. De plus, outre les événements déclenchés par votre application, il existe également d'autres
types d'actions à effectuer, telles que le garbage collection. Pour résumer, voici une comparaison avec d'autres
langages de programmation :
• Java ou C#. Vous pouvez créer plusieurs threads pour exécuter un travail fastidieux en arrièreplan. Les threads
partagent de la mémoire, ce qui peut être dangereux, mais vous disposez d'une API riche avec des mutex, des
types atomiques, etc. pour maintenir la cohérence de votre programme.
• Fléchette. Il n'y a qu'un seul thread avec sa propre mémoire. Vous ne pouvez pas créer plusieurs fils de
discussion. La boucle d'événements traite tout de manière séquentielle dès que possible. Pour
Pour ne pas perdre de temps, les appels asynchrones sont fractionnés afin que les rappels soient exécutés dans
un second instant afin de ne pas bloquer la boucle.
Chaque isolat possède un port à partir duquel les messages entrent et sortent ; ils sont respectivement représentés par
Receiver et SendPort. Un message est régulièrement traité par la boucle d'événements comme n'importe quelle autre
action mais c'est vraiment le seul moyen de communiquer.
Il est également possible de générer de nouveaux isolats dans Flutter, mais vous devrez utiliser
Future<T> calculate(...) plutôt que Isolate.spawn.
Travailler avec des isolats est un niveau assez faible et c'est quelque chose que vous ne faites généralement pas
régulièrement. Utiliser async/await est presque toujours suffisant. Pour donner un exemple pratique, si vous aviez une
application Flutter avec un calcul de données très coûteux en temps, votre fréquence d'images pourrait chuter en dessous de
La méthode calculate() nécessite que la fonction soit exécutée (qui ne peut pas être une fonction anonyme) et les
paramètres dont elle a besoin (le cas échéant). Si vous avez besoin de plusieurs paramètres d'entrée, enveloppezles
simplement dans une classe de modèle et transmettezla en tant que dépendance, comme ceci :
// Classe de classe
modèle PrimeParams {
limite int finale ; final
double un autre; const
PrimeParams(this.limit, this.another);
}
Dans calculate<Q,R> les paramètres sont définis comme suit : Q est le type du paramètre nécessaire à
la fonction et R est le type de retour. Au chapitre 16, nous verrons quand il est pratique d'utiliser des
isolats séparés dans les applications Flutter pour optimiser les performances.
Ce chapitre est une grande section de « bonnes pratiques » car il contient quelques suggestions bien connues du monde de
la POO. Nous aimerions également parler des modèles de conception, du TDD, du code propre et bien plus encore, mais
ces contenus dépassent le cadre de ce livre. De nombreuses personnes ont écrit des livres et des articles sur ces sujets,
nous vous recommandons de les lire pour plus de détails.
• Les modèles de conception sont une série de solutions réutilisables à des problèmes courants et bien connus. Le
concept original est venu d'un groupe de quatre personnes, appelé Gang of four, mais aujourd'hui de nouveaux
modèles apparaissent parallèlement à l'évolution des langues.
– Recherchez tout livre ou ressource récente incluant le plus large éventail de modèles. Ils s'appliquent à n'importe
quel langage de programmation ; le langage de programmation dans lequel ils sont expliqués n'est pas si
pertinent.
• TDD, abréviation de Test Driven Development, est un style de programmation fortement centré sur les tests de code.
Vous devez d’abord écrire les tests, couvrir tous les cas possibles et seulement après ce processus, vous pouvez
commencer à coder.
– https://resocoder.com/fluttercleanarchitecturetdd
• DDD, abréviation de Domain Driven Design, est un style de programmation axé sur la maintenabilité du code et la
séparation des préoccupations. Nous vous recommandons de suivre le cours DDD de Reso Coder qui donne une
explication étape par étape sur DDD à l'aide de Dart et Flutter.
– https://resocoder.com/flutterfirebasedddcourse
• Dart dispose d'une documentation très complète et conviviale dans laquelle vous pouvez trouver des exemples sur
presque tous les sujets. Il s'agit d'une ressource en pleine croissance qui vous explique comment écrire correctement
du code Dart à travers de bonnes pratiques et d'articles.
– https://dart.dev/guides/langue/effectivedart
– https://dart.dev/tutorials
Cela dit, vous pouvez bien sûr ignorer complètement cette partie car elle n'a pas de concepts de base Dart ou
Flutter, mais nous vous encourageons à au moins savoir ce que sont SOLID et DI. Ces concepts sont valables
quel que soit le langage de programmation dans lequel ils sont appliqués.
Formes de classe {
Liste<String> cache = Liste<>();
// Calculs double
SquareArea(double l) { /* ... */ } double circleArea(double r) { /* ...
*/ } double triangleArea(double b, double h) { /* ... */ }
Cette classe détruit totalement le SRP car elle gère les requêtes Internet, la peinture et les calculs en un seul
endroit. Vous devrez apporter des modifications très souvent à Shape car il comporte de nombreuses tâches,
toutes regroupées au même endroit ; la maintenance pour cette classe ne va pas être agréable. Et ça?
}
classe Carré étend la forme {} classe
Cercle étend la forme {} classe Rectangle
étend la forme {}
// Classe de peinture
d'interface utilisateur ShapePainter {}
// Classe de mise
en réseau ShapesOnline {}
Il existe 3 classes séparées se concentrant sur une seule tâche à accomplir : elles sont plus faciles à lire, tester,
maintenir et comprendre. Avec cette approche, l'attention du développeur se concentre sur un certain domaine
d'intérêt (tel que les calculs mathématiques sur Shape) plutôt que sur un ensemble désordonné de méthodes,
chacune ayant des objectifs différents.
classe Cercle
{ double rayon final ;
Rectangle(ce.radius);
}
}
}
Le rectangle et le cercle respectent le SRP car ils n'ont qu'une seule responsabilité (qui représente
une seule forme géométrique). Le problème se situe à l'intérieur de AreaCalculator car si nous
ajoutions d'autres formes, nous devrons modifier le code pour ajouter d'autres conditions if .
classe AreaCalculator {
double calculer ( forme de l'objet) { if (la forme
est un rectangle) {
// code pour le rectangle... } else if
(la forme est un cercle) { // code pour le
cercle... } else if (la forme est
un triangle) { // code pour le triangle... } else
if (la forme est un losange) { //
code pour Rhombus... } else { // code pour
Trapezoid...
}
}
}
Après avoir ajouté 3 nouvelles classes, le double calculate(...) doit être modifié car il nécessite
plus si les conditions permettent de gérer les conversions de type appropriées. En général, chaque fois qu'une nouvelle forme
est ajoutée ou supprimée, cette méthode doit être conservée en raison de la présence de fontes de caractères. Nous pouvons
faire mieux !
Grâce à l'interface, nous avons désormais la possibilité d'ajouter ou de supprimer autant de classes que nous le
souhaitons sans changer AreaCalculator. Par exemple, si nous ajoutions la classe Square implémente Area, elle
serait automatiquement "compatible" avec la méthode double calculate(...).
Nous avons ici un gros problème de logique. Un carré doit avoir 4 côtés de même longueur mais le rectangle n'a pas cette
restriction. Nous sommes capables de faire ceci :
vide main() {
Échec du rectangle = Carré(3);
échec.largeur = 4 ;
échec.hauteur = 8 ;
}
À ce stade, nous avons un carré avec 2 côtés de longueur 4 et 2 côtés de longueur 8... ce qui est absolument faux ! Les
côtés d'un carré doivent être tous égaux, mais notre hiérarchie est logiquement erronée. Le LSP est défectueux car cette
architecture ne garantit PAS que la sousclasse maintiendra l'exactitude logique du code.
Cet exemple montre également qu'hériter de classes ou d'interfaces abstraites, plutôt que de classes
concrètes, est une très bonne pratique. Préférez la composition (avec interfaces) à l’héritage.
Pour résoudre ce problème, faites simplement de Rectangle et de Square deux classes indépendantes. La rupture du LSP
ne se produit pas si vous dépendez des interfaces : elles ne fournissent aucune implémentation logique car elle est reportée
aux classes réelles.
// Classe abstraite
d' interface Worker {
travail nul ();
annuler le sommeil ();
}
Les robots n'ont pas besoin de dormir et la méthode est donc inutile, mais elle doit quand même être là sinon le code ne
sera pas compilé. Pour résoudre ce problème, divisons simplement Worker en plusieurs interfaces :
// Classe abstraite
d' interface Worker {
travail nul ();
}
classe abstraite Sleeper { void
sleep();
}
C'est nettement mieux car il n'y a pas de méthodes inutiles et nous sommes libres de décider quels
comportements les classes doivent implémenter.
Ceci est très important et utile : DIP déclare que nous devons coder sur des abstractions et non sur des implémentations. Étendre une classe
abstraite ou implémenter une interface est une bonne chose, mais descendre d'une classe concrète sans méthodes abstraites est une mauvaise
chose.
L'injection de dépendances (DI) est un moyen très connu d'implémenter le DIP. Dépendre des abstractions donne
la liberté d'être indépendant de l'implémentation et nous avons déjà traité de ce sujet. Regardez cet exemple :
}
}
La classe FileManager ne sait rien du fonctionnement de l'algorithme, elle sait simplement que la méthode encrypt()
sécurise un fichier. Ceci est essentiel pour la maintenance car nous pouvons appeler la méthode comme nous le souhaitons.
vouloir:
fm.secureFile(AlgoAES());
fm.secureFile(AlgoRSA());
Si nous ajoutions un autre algorithme de chiffrement, il serait automatiquement compatible avec secureFile car il
s'agit d'un soustype de EncryptionAlgorithm. Dans cet exemple, nous respectons ensemble les 5 principes
SOLID.
Deux classes sont dites « couplées » si au moins l’une d’elles dépend de l’autre. La classe A dépend de la classe B
alors que vous ne pouvez pas compiler la classe A sans la présence de la classe B. Cela peut être très dangereux,
voyons pourquoi.
classe PaymentProcessor {
_validateur final tardif ;
Processeur de paiement (chaîne numéro de carte) {
_validator = PaymentValidator(DateTime.now(), cardNumber);
}
Checker et PaymentProcessor ont tous deux une forte dépendance à PaymentValidator car il est essentiel à la
compilation. Bien entendu, les sousclasses héritent également de la dépendance.
Disons que vous avez écrit ce code au travail. Un jour, votre chef de projet vous dit d'abandonner Mastercard et
de la remplacer par PayPal. Vous aurez vite mal au ventre dès que vous réaliserez qu'à partir d'un changement
apparemment minime, c'est toute l'architecture qui doit être remaniée.
1. Paypal nécessite simplement un email, mais votre implémentation Mastercard nécessite la date et le
numéro de carte. Vous êtes obligé de modifier entièrement PaymentValidator, mais par conséquent, vous
devez également mettre à jour PaymentProcessor et Checker car ce sont de fortes dépendances.
class PaymentProcessor
{ PaymentValidator final tardif _validator ;
Il y a eu un effet « cascade » car les modifications apportées à une seule classe ont également des conséquences
sur les autres classes.
2. Les modifications cidessus cassent une autre partie du code : la classe abstraite Checker dépend
également de PaymentValidator, il est donc nécessaire de corriger le code.
Ce changement a des conséquences sur toute sousclasse de Checker qui doit être mise à jour. Les
dépendances sur les superclasses sont héritées par ses enfants et ainsi le couplage se propage.
3. Il n'existe aucun moyen de résoudre ce problème autre que la mise à jour manuelle de chaque sousclasse de
Checker. Votre IDE viendra à la rescousse avec un outil de refactorisation mais la maintenance est de toute façon
pénible.
Comme vous venez de le voir, un changement apparemment petit et rapide sur une classe s'est propagé à toute
une hiérarchie et à d'autres composants importants de notre projet. Tout cela est dû au fait que les classes sont
fortement couplées et dépendent d’implémentations plutôt que d’abstractions.
La classe PaymentProcessor aura toujours une dépendance PaymentValidator mais elle est faible car ce n'est
qu'une interface. En utilisant "l'injection de constructeur", nous transmettons de l'extérieur une implémentation
concrète, qui peut ensuite être remplacée par n'importe quoi d'autre.
Dans ce cas, nous transmettons une instance d'une classe concrète via un constructeur et c'est fondamental.
PaymentProcessor ne sait rien des détails d'implémentation de l'objet validateur, il sait juste qu'il doit appeler
validatePayment(int). Nous pouvons également utiliser des constructeurs const maintenant !
• Ce code est très flexible et maintenable. Si votre patron vous demandait d'ajouter également la prise en
charge du circuit Visa, vous devrez simplement créer un nouveau soustype de PaymentValidator.
Aucune modification n'est requise dans le code existant et, en même temps, vous adoptez toujours les
principes SOLID. L'architecture est robuste !
• PaymentProcessor ne se soucie désormais plus de Mastercard, Paypal ou autre parce qu'ils sont donnés
de l'extérieur. En interne, il dépend faiblement d'une abstraction qui donne juste une abstraction :
l'implémentation est passée via le constructeur.
• Vous disposez d'une série de cours, un par mode de paiement, très faciles à tester. Toi
pourrait créer une classe "simulée" pour les tests unitaires, tout comme un type de validateur classique :
La dernière chose que nous devons refactoriser est la classe Checker car elle doit renvoyer une abstraction plutôt
qu'une implémentation.
Puisque PaymentValidator est abstrait, toute classe de la hiérarchie hérite d’une dépendance faible qui est sûre.
const PaypalCheck(this._api);
Ces classes se connectent à Internet, effectuent certaines requêtes GET et renvoient vrai ou faux , que le
fournisseur de services soit en ligne ou non. Disons que cette fonctionnalité n'est pas indispensable dans notre
architecture mais c'est sympa de l'avoir. Cela pourrait être utilisé mais ce n'est pas certain.
De cette façon, si nous voulions vérifier la disponibilité du service, nous pourrions faire ceci :
vide main() {
API finale = MasterCardApi(...); processeur final
= MasterCard(api); vérificateur final =
MastercardCheck();
Notez la différence : alors que le processeur est fondamental, et donc passé via le constructeur, le vérificateur de connexion
effectué avec isProcessorActive n'est pas toujours requis. Donc en général :
• L'injection de constructeur concerne les dépendances essentielles que votre classe utilisera toujours ;
• L'injection de méthode concerne les dépendances facultatives que votre classe pourrait utiliser.
En fait, les deux types d'injection utilisent le même concept, qui dépend des abstractions et des implémentations passantes
de l'extérieur, mais ils diffèrent par ordre "d'importance". Les dépendances passées via le constructeur sont fondamentales
tandis que celles passées via la méthode sont simplement utiles mais pas essentielles.
Partie II
Le cadre Flutter
"Les programmes doivent être écrits pour que les gens puissent les lire, et seulement
201
Machine Translated by Google
Machine Translated by Google
9 | Bases du Flutter
1 pour
La documentation officielle de Flutter vous propose un guide d'installation étape par étape
Windows, macOS et Linux. Veuillez le suivre attentivement pour configurer correctement votre
environnement. Nous avons utilisé Android Studio 4.0.1, qui est la dernière version à l'époque
d'écrire ce livre.
Tout nouveau projet Flutter, qu'il soit créé avec Android Studio ou VS Code, nécessite une série
de fichiers et de répertoires pour vous et l'EDI. La plupart d’entre eux peuvent être ignorés en toute sécurité car
vous passerez pratiquement tout votre temps dans lib/ et test/.
1https://flutter.dev/docs/getstarted/install
C'est ce que nous obtenons sous Windows en utilisant Android Studio, mais selon l'IDE que vous utilisez, il peut y avoir
des fichiers de configuration différents. Quel que soit le système d'exploitation et l'EDI, il y aura toujours au moins :
• android/ et ios/ : ces dossiers contiennent du code spécifique à la plateforme pour chaque système d'exploitation et
ils sont automatiquement gérés par l'EDI et le compilateur. La structure est exactement la même que celle que vous
obtiendriez avec un nouveau projet Android sur Android Studio ou un projet iOS sur XCode.
• lib/ : Ce dossier est indispensable : il contient le code source Dart de votre application Flutter. Tu es
je vais passer d'innombrables heures ici.
• test/ : les tests unitaires, les tests de widgets et les tests d'intégration vont tous dans ce dossier. Le chapitre 16 est
un guide détaillé sur la façon de tester correctement vos applications Flutter.
• pubspec.yaml : ce fichier est fondamental car il définit un package Dart et répertorie les dépendances de votre
application Flutter.
• README.md : c'est le fichier markdown typique que vous pouvez trouver dans n'importe quel référentiel git. Il est
utilisé pour votre dépôt git et en même temps comme "page d'accueil" sur https://pub.dev au cas où vous souhaiteriez
publier un package.
Tous les autres fichiers ou dossiers que nous n'avons pas mentionnés dans la liste cidessus sont automatiquement gérés
par l'EDI (ou le compilateur), vous ne devriez donc pas vous en soucier.
• lib/. Le code source de votre application va ici. Regrouper les fichiers dans des dossiers appropriés est essentiel si vous
ne voulez pas vous perdre dans votre propre architecture. Avant de coder, faites attention à la structure.
– itinéraires/
des modèles/
– widgets/
– main.dart
– routes.dart
Vous pouvez vous occuper de la structure des dossiers de votre projet à partir de ce simple squelette. routes/ contient
les pages de votre application, models/ est pour la « logique métier » et widgets/ est pour les widgets d'interface
utilisateur réutilisables.
– localisations/
– itinéraires/
– widgets/
des modèles/
blocs/
prestataires/
référentiels/
* ...
– main.dart
– routes.dart
Si vous envisagez de rendre vos applications disponibles dans plusieurs langues, envisagez de regrouper toute la
logique de localisation dans localisations/. Une couverture complète des techniques de localisation dans Flutter sera
abordée en détail au chapitre 13. Structurer les dossiers avec de nombreux sousdossiers.
• test/. Flutter dispose d'une puissante suite de tests automatisés ; nous vous recommandons de diviser les fichiers de
test en fonction de leur cas d'utilisation.
unité/
– widget/
l'intégration/
Il y aurait la possibilité de regrouper tous vos fichiers dans un seul dossier (par exemple lib/) sans structure mais... non !
Dans une architecture d'applications de taille moyenne à grande, la maintenance va être pénible car il n'y a pas d'organisation
logique.
YAML est un langage de sérialisation de données couramment utilisé pour les fichiers de configuration.
Il expose une série de paramètres de manière lisible par l'homme ; il n'a pas de ponctuation car il repose sur
l'indentation et les sauts de ligne.
L'indentation et les sauts de ligne sont très importants car il n'y a pas de pointsvirgules ou de virgules comme séparateurs.
Nous donnons seulement un aperçu des attributs les plus importants mais bien sûr de la documentation officielle
2
vous donnera une référence complète.
• version. Tout package doit spécifier un numéro de version qui s'incrémente à chaque version ; au chapitre 24, nous
verrons comment écrire un package Flutter pouvant être téléchargé sur https://pub.dev. Ici vous voyez une
bibliothèque avec son numéro de version :
2https://dart.dev/tools/pub/pubspec
Lorsque vous publiez une application pour le Google Play Store ou l'App Store d'Apple, ce numéro est utilisé pour
attribuer une valeur de version au produit. Par exemple, si vous aviez...
version : 1.1.0+5
... cela signifierait que le nom de version de votre application serait 1.1.0 et que le numéro de build serait 4. Dans le
monde Android, à l'intérieur de build.gradle, le champ versionName serait 1.1.0 et versionCode serait 5.
• SDK. Cette section contient les contraintes indiquant les versions du SDK prises en charge par votre application. L'équipe
Dart recommande de toujours inclure une limite inférieure et supérieure, mais vous pouvez simplement utiliser ">= 2,7"
et ce serait de toute façon valide.
environnement:
SDK : ">=2.7.0 <3.0.0"
Avec la plage cidessus, vous pouvez utiliser tout ce qui vient de la version 2.7, donc les extensions Dart par exemple
(introduites dans la version 2.7) sont prises en charge.
• usagesmatériauxconception. Garantit que votre application Flutter est capable d'utiliser les icônes du projet Google. Ils
3
Conception matérielle sont assez courants dans le monde de Google, notamment dans
Android car ce sont les icônes par défaut utilisées dans de nombreuses applications.
Avoir des utilisationsmatériauxconception : de vraies icônes sont déjà disponibles, vous n'avez rien à télécharger ou
à configurer car elles sont regroupées dans le SDK Flutter. Les icônes sont en fait des images vectorielles et peuvent
donc être redimensionnées sans perte de qualité.
3https://material.io/
• les dépendances. C'est probablement l'étiquette la plus importante car elle déclare tout package dont l'application va
dépendre. Il vous suffit d'aller sur https://pub.dev, recherchez un package et ajoutez une nouvelle ligne.
dépendances : flutter :
SDK : flottement
http : ^0.12.2
fournisseur : ^4.3.2+2
flutter_svg : ^0.18.1
À gauche se trouve le nom du package tandis qu'à droite se trouve la version en cours de téléchargement depuis le
référentiel. Ils ont été ajoutés de cette manière très simple :
1. ouvrez https://pub.dev ;
• actifs. Cette étiquette spécifie les chemins d'accès aux ressources statiques que votre application utilisera, telles que
des images, des vecteurs SVG, des fichiers audio/vidéo ou du texte simple. Par exemple, vous pouvez créer un
dossier appelé images/ et y mettre tout sans avoir à lister les fichiers un par un.
battement:
actifs:
images/
fichiers/texte/monFichier.txt audio/
Dans la deuxième ligne nous avons importé un fichier texte donnant l'emplacement exact. La racine du projet est le
répertoire dans lequel se trouve le fichier pubspec.yaml. Lorsque vous déclarez un actif, quel qu'il soit, le point de
départ du chemin est la racine.
• polices. Par convention ce label est placé en bas du fichier, après les biens. Vous pouvez télécharger des fichiers de
polices depuis https://fonts.google.com et importezles directement dans votre application.
battement:
actifs:
images/
polices :
famille : Roboto
polices :
actif : fonts/RighteousRegular.ttf
poids : 400
Une fois que vous avez téléchargé les fichiers .ttf depuis Google Font, créez un dossier appelé (par convention)
fonts/ et placezy les fichiers. Il n'y a plus rien à faire car Flutter se chargera de les charger automatiquement.
– https://pub.dev/packages/google_fonts
Depuis janvier 2020, il existe un package Flutter officiel appelé google_fonts qui récupère les polices de https://
fonts.google.com/ et les met en cache. C'est idéal pour le développement : pas besoin de placer les ressources de
police dans le dossier font/ car elles seront automatiquement téléchargées et mises en cache.
Texte(
'Voici Google Fonts', // Téléchargez
'pacific' et mettezle en cache dans le style :
GoogleFonts.pacific(),
),
Cependant, vous pouvez quand même télécharger des fichiers de polices et les inclure en tant qu'actifs, car c'est plus
rapide et plus sécurisé. Vous pourriez avoir des problèmes si l'utilisateur ouvre votre application pour la première fois
sans connexion Internet. Le package Google Fonts donnera la priorité aux fichiers préregroupés par rapport à la
récupération HTTP, vous pourrez donc effectuer les opérations suivantes :
1. Lors du développement, utilisez la récupération de polices http sur Internet, ce qui est très pratique.
3. Ouvrez le fichier pubspec.yaml et ajoutez font/ sous Assets afin que google_fonts puisse charger automatiquement
les fichiers de polices à partir de là.
battement:
actifs:
images/
polices/
Il n'est pas nécessaire d'avoir la section des polices car les fichiers sont déjà inclus en tant qu'actifs.
De cette façon, google_fonts chargera les polices au démarrage plutôt qu'à la première utilisation.
Cependant, si une police donnée est requise et qu'elle ne figure pas dans les ressources, elle sera automatiquement
téléchargée et mise en cache.
En résumé, nous recommandons l'utilisation de google_fonts pour le développement, mais vous devez fournir les fichiers de
polices en tant qu'actifs afin qu'ils puissent être chargés au démarrage (plutôt qu'au moment de l'exécution, via une requête
HTTP).
Si vous connaissez les bases du HTML, vous savez que toute modification apportée à un fichier .html est visible immédiatement
en cliquant sur le bouton d'actualisation du navigateur. C'est littéralement une question de secondes car il vous suffit de
sauvegarder le fichier et d'appuyer sur F5. Flutter fonctionne de la même manière !
Grâce à la fonction de rechargement à chaud, vous pouvez actualiser l'interface utilisateur de votre émulateur (ou appareil
physique) tout en écrivant du code Dart. Il n'est pas nécessaire de créer une compilation à chaque fois et d'attendre la fin de
Gradle/Xcode. C'est comme si vous appuyiez sur F5 dans votre navigateur pour actualiser le fichier source HTML.
Vous devez exécuter l'application en mode débogage pour la première fois, mais vous pouvez ensuite appuyer sur
l'éclair jaune qui est le bouton de rechargement à chaud. Vous verrez l'interface utilisateur immédiatement mise à jour
et entièrement fonctionnelle, en synchronisation avec le dernier code que vous avez écrit.
Le rechargement à chaud est extrêmement rapide car il faut moins d’une seconde pour actualiser l’interface utilisateur. Cela augmente
considérablement la productivité car les modifications sont immédiatement appliquées et prêtes à être testées, pas besoin d'attendre
les processus de construction. Le rechargement à chaud fonctionne dans la plupart des cas, mais dans certaines circonstances, vous
devez arrêter et réexécuter entièrement l'application :
• lorsque vous apportez des modifications à la méthode initState() (plus d'informations dans le chapitre suivant)
• lorsque vous apportez des modifications aux champs statiques dans les classes,
• lorsque vous apportez des modifications au code dans void main() {}.
En mode débogage, Flutter utilise le modèle de compilation JIT qui, en combinaison avec Dart Virtual Machine, permet une injection
rapide du code source et des reconstructions incrémentielles rapides. En d’autres termes, on peut dire que « le rechargement à chaud
de Flutter est super rapide ! ».
Lors de la création d'applications Flutter, il est courant d'avoir un simulateur Android ou iOS à droite de l'écran et
votre IDE préféré dans l'espace restant. C'est le moyen le plus rapide d'écrire du code et de voir les résultats
immédiatement grâce à la fonction de rechargement à chaud.
Après des heures de codage, vous pourriez oublier de donner le type à une classe générique et ainsi le compilateur attribue
automatiquement la dynamique. Tout va bien car la compilation s'exécute avec succès mais le code n'est pas de type sécurisé et vous
ne suivez donc pas les bonnes pratiques.
Un linter est un outil très utile qui lit le code source et détecte les erreurs de syntaxe, les constructions suspectes,
les erreurs de style et bien plus encore. Par défaut, le linter de Dart est très permissif et marque quelque chose comme
erreur uniquement lorsque cela est vraiment nécessaire.
Rendre le linter plus sévère est très productif car il peut découvrir des problèmes et des bugs potentiels avant même d'exécuter le code.
Pour ce faire, il est nécessaire de créer un fichier appelé Analysis_options.yaml dans le même dossier que pubspec.
analyseur :
mode fort : casts
implicites : faux dynamique
implicite : faux
linter :
règles:
éviter_unused_constructor_parameters wait_only_futures
directives_ordering
empty_constructor_bodies
empty_statements hash_and_equals
Implementation_imports
null_closures
package_api_docs
slash_for_doc_comments
test_types_in_equals
throw_in_finally
type_init_formals
4
Visitez la documentation officielle de Dart pour obtenir une liste complète de toutes les règles de linter. Nous vous
encourageons fortement à créer un fichier Analysis_options.yaml pour chaque application Flutter ou projet Dart que vous
créez. Il y a aussi la possibilité de changer le comportement par défaut du linter :
analyseur :
les erreurs:
include_file_not_found : erreur
dead_code : avertissement
Par exemple, par défaut, lorsqu'un fichier d'inclusion donné est introuvable, un avertissement est émis. Si vous souhaitez que
ce problème soit plus important, il peut être traité comme une erreur en remplaçant sa gravité par l'un de ces niveaux :
• avertissement : l'analyse statique n'échoue que si les avertissements sont traités comme des erreurs par l'analyseur ;
• info : juste un message d'information qui ne fait pas échouer l'analyse statique ;
4https://dartlang.github.io/linter/lints/
Fondamentalement, dans les erreurs, vous pouvez redéfinir la gravité des avertissements et des erreurs à votre guise.
En général, la configuration par défaut est correcte, vous n'avez pas besoin de remplacer les règles et en particulier
5
d'éviter d'utiliser ignorer. Visitez la documentation officielle pour obtenir une liste complète de toute propriété remplaçable.
Le fichier Analysis_options.yaml résume les bonnes pratiques de Dart pour que vous n'ayez pas
à vous souvenir de tout. L'EDI est capable de lire ce fichier et d'émettre des messages visuels pour
vous.
En termes très simples, avoir des règles appropriées définies sur Analysis_options.yaml, c'est comme avoir quelque
chose qui vous guide pour suivre les meilleures pratiques de Dart. Accédez à la page Ressources de notre site Web
pour télécharger un bon modèle que nous vous recommandons d'utiliser.
• Mode débogage.
if (kDebugMode) { //
code à exécuter lors de l'exécution de l'application en mode débogage...
}
• Mode Profil.
if (kProfileMode) { // code
à exécuter lors de l'exécution de l'application en mode profil...
}
• Mode de libération.
if (kReleaseMode) { // code
à exécuter lors de l'exécution de l'application en mode release...
}
Lors de la création d'une application Flutter (dans n'importe quel mode), le « secouement de l'arbre » est automatiquement
effectué. Il s'agit essentiellement du compilateur qui supprime le code mort en fonction du fait que les variables sont constantes
ou non, ainsi que d'autres facteurs. Par exemple, regardez ce morceau de code :
5https://pub.dev/documentation/analyzer/latest/analyzer/analyzerlibrary.html
Le bouton Exécuter d'Android Studio et VS Code crée l'application en mode débogage afin que le code cidessus renvoie
toujours "Démo". L'autre instruction (return _real();) est automatiquement supprimée par le compilateur car elle ne sera
jamais atteinte. Selon le mode build, après la compilation, le même morceau de code peut ressembler à ceci :
String get name { return Chaîne get nom { return Chaîne get nom { return
"Démo" ; _real(); _real();
} } }
Vous devriez vraiment utiliser ces constantes lors du développement de vos applications car elles sont très utiles. Il n'est
pas non plus nécessaire de les gérer car le compilateur supprimera automatiquement les parties inutilisées (le code mort
est automatiquement supprimé). Le tremblement des arbres fonctionne avec n’importe quelle valeur constante :
si (est bon) {
print("Bien!"); } else
{ print("Mauvais!");
}
Le compilateur supprimera la branche else car elle est considérée comme du code mort.
Si vous parlez de widgets, vous faites référence aux boutons, aux champs de texte, aux animations, aux
conteneurs et même à la page d'interface utilisateur ellemême. Tout ce qui apparaît à l'écran ou interagit avec
celuici est un widget. Des widgets partout !
Lorsque vous imbriquez des widgets les uns dans les autres, vous créez une hiérarchie appelée « arborescence
des widgets » dans laquelle se trouvent les parents et les enfants. Dans un nouveau projet Flutter, l'EDI prépare un
exemple d'application dans main.dart ayant cette structure minimale :
importer 'package:flutter/material.dart';
void main()
{ runApp(MonApp());
}
Comme vous le savez, tout programme Dart doit avoir un point d'entrée void main() {} et Flutter ne fait pas
exception ; la méthode runApp() prend une instance d'un widget et en fait la racine de l'arborescence des widgets.
Au début, vous obtenez un arbre avec une seule feuille (la racine ellemême) :
Dans la section suivante, nous verrons qu'une classe dans Flutter devient un widget lorsqu'elle hérite de
StatelessWidget ou StatefulWidget. Pour l'instant, notez que la méthode runApp() a effectué le
classe appelée MyApp la racine de l'arborescence. Ajoutons plus de contenu pour voir comment l'arborescence des widgets se
développe.
@passer outre
La méthode Widget build(BuildContext context) ajoute de nouvelles feuilles à l'arborescence des widgets afin de placer de
nouveaux éléments graphiques dans l'interface utilisateur. Les widgets sont imbriqués les uns dans les autres grâce à des
paramètres nommés dans les constructeurs pour rendre la lecture du code très expressive. Voici la nouvelle situation :
L'ajout de widgets agrandit l'arborescence et place de nouveaux éléments à l'écran. Le paramètre de contexte dans
build(BuildContext context) donne des informations importantes sur la position de la feuille dans l'arborescence. En
particulier:
• Une instance BuildContext est utilisée par Flutter pour connaître les détails du widget lorsque le
l'arbre est traversé ;
• Nous verrons que l'appel de SomeWidget.of(context) renvoie le widget le plus proche dans l'arborescence dont le
type est SomeWidget ;
• Chaque widget possède sa propre instance BuildContext qui devient le contexte parent du ou des widgets renvoyés
par sa méthode build.
À part transmettre une instance de BuildContext aux constructeurs de widgets, vous ne ferez rien d'autre avec. Il est
destiné à être utilisé par Flutter pour obtenir des informations sur l'arborescence des widgets ; il n'est pratiquement
jamais demandé au développeur de l'utiliser directement.
Nous vous encourageons fortement à visiter le catalogue en ligne car il présente les widgets
d'interface utilisateur les plus importants pour les styles Material (Android) et Cupertino (iOS). L'équipe
Flutter est très active et le catalogue est amélioré/élargi très souvent.
9.2.1.1 Texte
Vous ne seriez pas surpris d'apprendre que le widget Texte est utilisé pour afficher un morceau de texte à l'écran.
Il est hautement personnalisable car vous pouvez modifier la couleur, la police à l'aide des ressources de police ou
du package Google Font et bien plus encore.
const Texte (
"Texte à l'écran", style :
TextStyle ( couleur :
Colors.amber, fontSize : 16,
wordSpacing : 3,
),
);
Cela nécessite simplement une chaîne comme paramètre, qui est le texte affiché sur l'interface utilisateur, et le style
6
est créé avec la classe TextStyle(). Il définit de nombreuses propriétés du texte luimême et c'est également l'endroit
où le package google_fonts peut être utilisé.
Texte(
"Texte à l'écran", style :
GoogleFonts.lato( textStyle : const
TextStyle( couleur : Colors.amber,
fontSize : 16, wordSpacing :
3,
),
6https://api.flutter.dev/flutter/painting/TextStyleclass.html
),
);
9.2.1.2 Ligne
Ce widget place un ou plusieurs enfants sur l'axe horizontal avec les contraintes d'espace données.
Il est très souvent utilisé lorsque vous devez aligner plusieurs éléments côte à côte. Il n'y a pas de constructeur
const pour Row mais vous pouvez l'attribuer, lorsque cela est possible, à la valeur children.
],
),
Vous pouvez facilement comprendre qu'il y aura trois widgets Texte côte à côte au centre de l'écran. Par défaut,
une ligne tente de couvrir totalement l'espace horizontal disponible ; vous pouvez vous assurer qu'il se rétrécit
pour s'adapter à la largeur de son contenu en utilisant :
Ligne
( mainAxisSize : MainAxisSize.min,
),
Les widgets en lignes peuvent être placés de différentes manières en fonction de la valeur de mainAxisAlignment.
Le comportement par défaut est start mais bien sûr il peut être modifié en transmettant différentes valeurs au constructeur.
• espaceAutour. Place les éléments à égale distance les uns des autres et des marges.
• l'espace entre. Place les éléments avec un espace uniforme entre eux.
9.2.1.3 Colonne
Ce widget place un ou plusieurs enfants sur l'axe vertical avec les contraintes d'espace données. Une colonne est à
l’opposé d’une ligne car elle a le même objectif mais elle fonctionne dans la direction opposée (verticale plutôt qu’horizontale).
Colonne
( mainAxisAlignment : MainAxisAlignment.center, enfants :
const [ Text("Bonjour"),
Text("Flutter!"),
Text("!!"),
],
),
C'est identique à une Ligne mais ici les éléments sont placés les uns audessus des autres car une Colonne fonctionne
sur l'axe vertical. Il essaie de couvrir totalement l'espace vertical disponible ; vous pouvez vous assurer qu'il se rétrécit
pour s'adapter à la hauteur de son contenu en utilisant
Colonne(
mainAxisSize : MainAxisSize.min,
),
Vous pouvez placer les widgets de différentes manières en passant simplement un nouvel alignement au paramètre
mainAxisAlignment du constructeur. C'est complètement identique à une ligne car les alignements fonctionnent dans le
de la même façon.
Une colonne n'a PAS de comportement de défilement, donc s'il n'y a pas assez d'espace, vous obtiendrez une erreur de débordement
au moment de l'exécution.
Un ListView est essentiellement une colonne avec un comportement de défilement car elle place un ou
plusieurs enfants sur l'axe vertical, dans l'ordre. Ce widget est très largement utilisé car il offre la possibilité de
faire défiler les contenus lorsqu'ils sont plus grands que la taille de l'écran.
],
),
Les widgets sont alignés vers le haut et le sens de défilement est vertical par défaut mais vous pouvez bien sûr le modifier.
Avec scrollDirection, vous pouvez décider si la liste doit défiler sur l'axe horizontal ou vertical.
Lorsque le contenu de la liste est connu à l'avance, les enfants sont simplement déclarés à l'intérieur d'une
liste comme vous l'avez vu cidessus. Le constructeur nommé ListView.builder(...) est très utile lorsque la liste
doit être construite sur la base d'une collection existante.
// Quelque part dans le code, il y a une liste de 100 entiers final myList =
List<int>.generate(100, (i) => i);
// Le 'builder' nommé constructor construit une liste de widgets // en prenant la liste 'myList'
comme source de données.
ListView.builder( itemCount :
maListe.longueur, itemBuilder :
(contexte, index) {
return Text("${maListe[index]}"),
},
),
7https://flutter.dev/docs/cookbook/lists/longlists
en remplissant un long ListView avec une boucle for, utilisez son builder() qui est plus efficace.
9.2.1.5 Conteneur
Ce widget est l'équivalent d'une balise <div></div> dans le monde HTML ; c'est un conteneur à usage général que vous
pouvez utiliser pour personnaliser la peinture, le positionnement, le dimensionnement et bien plus encore. Un conteneur
est très largement utilisé car il permet de réaliser de nombreux cas d'utilisation tels que la création de bordures arrondies
ou le travail avec des formes.
Cela peut sembler un résultat complexe à réaliser, mais c'est en réalité très simple car un conteneur est fait
exactement à cet effet.
Taille de la police : 25
)
)
);
La rotation est obtenue grâce à la transformation : Matrix4.rotationZ(0.25), qui définit comment placer un objet dans l'espace
3D. Ceci est souvent utilisé dans les animations mais il faudra attendre le chapitre 14 pour en savoir plus. Pour styliser un
Container, vous devez utiliser une classe BoxDecoration :
couleur : Colors.grey,
spreadRadius : 5,
blurRadius : 7,
offset : Offset(0, 3),
),
],
dégradé : LinearGradient ( début :
Alignement.topCenter, fin :
Alignement.bottomCenter, couleurs :
[ Color.fromARGB(...),
Color.fromARGB(...)
],
)
),
);
Dans cet exemple, nous avons créé un cercle avec une ombre derrière et un dégradé linéaire comme
arrièreplan. La classe BoxShadow est très similaire à la propriété CSS boxshadow , exactement comme
LinearGradient qui peut bien entendu interpoler plus de deux couleurs. Ce n'est pas le seul type de
dégradé que vous pouvez utiliser :
• LinearGradient : une transition progressive de deux couleurs ou plus le long d'une ligne droite ;
• RadialGradient : une transition progressive de deux couleurs ou plus rayonnant autour d'un
indiquer;
• SweepGradient : une transition progressive de deux couleurs ou plus avec un balayage circulaire sur un
point central.
Vous pouvez également avoir des bordures arrondies avec borderRadius: BorderRadius.circular(30.0) ou une simple couleur
d'arrièreplan unie avec la propriété color :. Assurezvous de consulter la documentation officielle sur BoxDecoration
8
pour voir comment vous pouvez entièrement personnaliser un conteneur.
Grâce au widget Stack, vous pouvez superposer les widgets et les positionner librement sur l'écran en utilisant Positioned.
Même si les enfants sont placés en dehors des limites de l'interface utilisateur, aucune erreur de débordement n'apparaîtra
car une pile ne contraint pas les limites de largeur et de hauteur.
Pile
( enfants : [
Récipient(
largeur : 40,
hauteur : 40,
décoration : const BoxDecoration(
couleur: Couleurs.rouge
)
),
const Texte("Bonjour"),
]
)
Avec cet exemple simple, l'interface utilisateur est créée avec une boîte rouge et le widget Texte est peint devant le conteneur.
L'ordre dans lequel vous placez les widgets est vraiment important car les enfants en bas de la liste sont placés relativement
devant ceux du haut. Le widget de premier plan va à la fin de la liste.
Pile
( enfants : [
const Texte("Bonjour"),
Récipient(
8https://api.flutter.dev/flutter/painting/BoxDecorationclass.html
largeur : 40,
hauteur : 40,
décoration : const BoxDecoration(
couleur: Couleurs.rouge
)
),
]
)
Dans ce cas la case rouge serait placée devant le widget Texte car, dans l'ordre, elle vient après et donc "Bonjour" n'est
pas visible car couvert par le Container. Vous pouvez cependant décider de déplacer le texte à une position précise de
l'écran :
Pile
( enfants : const [
Positionné(
en haut : 40,
à gauche : 65
enfant : Texte("Bonjour"),
),
]
)
Vous auriez également pu utiliser un décalage négatif tel que left: 15 pour positionner le texte en dehors des limites de la
zone visible.
Vous savez déjà grâce aux exemples précédents comment les widgets sont disposés dans build()
pour composer l'interface utilisateur. Ils sont imbriqués les uns dans les autres.
Avant de créer un widget, le développeur doit décider si l'état changera ou non au cours du temps. Si l’état change à un
moment donné, cela signifie que quelque chose s’est produit.
comme:
• l'utilisateur a appuyé sur un bouton et quelque chose dans l'interface utilisateur doit donc changer ;
• il y a un nouvel événement sur un flux et un widget en fonction est notifié (et donc une reconstruction
se produit afin de refléter les changements apportés par le flux).
Autrement dit, il faut se demander si le widget est immuable ou s'il est "dynamique", dans le sens où quelque
chose peut changer au fil du temps. La décision se traduit dans le code Dart en étendant l'une de ces deux
classes.
• Widget apatride. Utilisez ce type de widget lorsque vous devez créer un élément d'interface utilisateur qui
ne changera pas avec le temps. Il s'agit d'un bloc « autonome » qui ne dépend pas d'événements ou de
sources externes ; il repose simplement sur son constructeur et les données internes.
@passer outre
]
);
}
}
C'est un exemple parfait de StatelessWidget car le contenu sera toujours le même ; aucune dépendance
ou flux externe ne modifiera le texte ou l'icône. Une fois créé, le widget est « statique » car il ne changera
jamais : c'est un « bloc solide ».
});
@passer outre
Par convention, les widgets Flutter ont nommé des paramètres facultatifs dans le constructeur ; au cas où ils seraient
requis, utilisez le motclé requis. Puisque cette classe est immuable, c'est une très bonne idée de marquer les
variables d'instance comme finales afin qu'un constructeur const puisse être déclaré.
Même si la classe prend une chaîne de l'extérieur, via le constructeur, elle ne change toujours pas
avec le temps. Le nom donné sera toujours le même et donc le widget ne changera/reconstruira jamais,
donc une solution sans état convient.
Si vous travaillez avec Dart 2.9 ou des versions antérieures, au lieu du motclé requis, vous utiliserez l' annotation
@required.
• Widget avec état. Utilisez ce type de widget lorsque vous devez créer un élément d'interface utilisateur qui va changer
au fil du temps. Dans ce cas, l'interface utilisateur va changer dynamiquement en raison d'événements externes tels
que la réponse reçue d'une requête HTTP ou le rappel déclenché par une pression sur un bouton.
@passer outre
// Notez le trait de soulignement : l'état est une classe de classe privée du package _CounterState
extends State <Counter> {
int _counter = 0 ;
@passer outre
enfants: [
Texte("$_compteur"),
IcôneBouton(
enfant : Icon(Icons.add), onPressed :
() { setState(() =>
_counter++);
}
),
],
);
}
}
Les boutons et la conception de l'interface utilisateur seront présentés dans la section suivante, ils ne sont plus un point clé pour le moment.
Dans ce widget, l'acteur principal est l'IconButton : une fois appuyé par l'utilisateur, le rappel onPressed est déclenché
et la variable _counter est incrémentée.
1. Counter est le widget luimême et est donc inséré dans l'arborescence des widgets ; _CounterState
est l'état mutable du widget Counter. Lorsque Flutter reconstruit l'arborescence des widgets pour
actualiser l'interface utilisateur, la méthode build(...) de State<T> est appelée.
2. Il s'agit du modèle standard pour la création d'un widget avec état et vous devriez vraiment le suivre. Android
Studio et VS Code peuvent créer automatiquement le passepartout pour vous.
4. Les instances membres, telles que _counter, survivent aux reconstructions. Seulement ce qu'il y a à l'intérieur du
La méthode build() est actualisée.
Lorsque vous appuyez sur le bouton, le rappel onPressed est activé : setState(...) exécute son corps puis Flutter
reconstruit le widget. Dans notre exemple, int _counter est incrémenté de 1, puis le widget est actualisé afin que Text
puisse afficher la nouvelle valeur mise à jour.
La variable _counter appartient à l'objet state State<Counter> et pour cette raison elle n'est pas
réinitialisée lorsque le widget est reconstruit. N'oubliez pas que l'état « survit » lorsqu'une construction a
lieu.
Counter est le widget (ce qui est inséré dans l'arborescence) tandis que _CounterState est l'état.
L'état survit aux reconstructions, mais pas sa méthode build(). Si vous n'êtes pas encore convaincu, nous allons vous
dire que cet extrait fonctionne comme prévu.
@passer outre
}
}
L'état survit aux reconstructions donc _counter n'est pas réinitialisé à zéro, il conserve le décompte.
Seul ce qui se trouve à l'intérieur de la méthode build est actualisé. Si tu as fait ça...
retourner la colonne(...);
}
}
...votre compteur serait toujours à zéro ! Il est toujours correctement incrémenté par setState() qui fait bien _counter+
+ mais ensuite le widget est reconstruit et la première ligne fait _counter = 0 qui le remet à zéro.
Si vous avez utilisé le constructeur d'un StatefulWidget pour définir certaines données, la classe State<T> associée peut
obtenir une référence à cellesci en utilisant simplement le getter de widget. Encore une fois, essayez d'utiliser const
autant que possible.
@passer outre
Dans l'exemple cidessus, WidgetDemo est autorisé à avoir un constructeur const car une fois inséré dans l'arborescence
des widgets, il ne changera jamais. Ce qui change vraiment, c'est son état, représenté par _WidgetDemoState, qui en fait
ne peut pas avoir de constructeur constant.
Tout d'abord, il faut dire qu'il n'y a AUCUNE différence de performances entre un widget avec état et un widget sans état.
Vous n'êtes pas obligé de penser qu'un widget sans état est une version optimisée d'un widget avec état ou vice versa.
En fait, un StatelessWidget peut être considéré comme un StatefulWidget sans la méthode setState().
Lors de la création d'un widget avec état, l'état est clairement visible car il s'agit d'une classe privée séparée :
// La classe
Widget Counter étend StatefulWidget { ... }
// La classe d'état du
widget _CounterState étend State<Counter> { ... }
Un widget sans état n'est qu'un sucre syntaxique pour les cas dans lesquels vous n'avez pas besoin
de créer un état personnalisé. Un StatelessWidget a également un état mais vous ne pouvez pas le
voir car il n'est pas destiné à être modifié manuellement.
Vous pouvez utiliser StatefulWidgets toute la journée et toute la nuit sans avoir de problèmes, mais cela n'aurait aucun
sens. Si l'état ne change pas, optez pour un StatelessWidget qui contient moins de code passepartout et expose moins de
méthodes. Voici un guide pour vous aider à décider lequel utiliser :
• Lorsque chaque variable d'instance de votre widget peut être marquée avec le modificateur final , utilisez un widget
sans état avec un constructeur const .
@override
Il s'agit d'une classe immuable car elle possède des variables finales et un constructeur constant : une fois instancié,
le widget ne changera jamais. Il s'agit d'un "bloc statique", widget qui ne change pas dans le temps.
• Lorsque votre widget comporte des variables qui ne peuvent pas être définitives car elles peuvent changer
au fil du temps, utilisez un widget avec état. Cela peut arriver lorsque vous devez initialiser paresseusement
certaines valeurs ou que vous attendez une requête asynchrone.
@passer outre
@passer outre
Dans ce cas, _counter ne peut pas être final car la méthode build() va le modifier.
Lorsqu'une variable d'instance peut être modifiée au fil du temps, l'état du widget changera également. Dans ce cas,
un StatefulWidget est le bon choix.
• Dans tous les cas où un widget est quelque chose de "statique" qui ne dépend de rien
externe, envisagez de le rendre apatride car vous n'avez pas besoin de changer son état.
@passer outre
Ce widget n'a pas besoin de changer d'état et ne dépend pas de données externes. C'est juste un seul "bloc"
réutilisable.
Pour résumer, un StatelessWidget est utile lorsque vous devez créer des widgets indépendants "réutilisables"
ou lorsque vous n'avez pas besoin de changer l'état de votre widget. Dans tous les autres cas, pensez à
utiliser un StatefulWidget.
9.2.3 Clés
Vous avez peutêtre remarqué que tout widget fourni par Flutter possède le paramètre key facultatif.
Très simplement, il est utilisé pour identifier de manière unique un widget dans l'arborescence, comme lorsqu'une clé primaire est
affectée à une colonne d'une base de données relationnelle. Il existe principalement quatre types de clés (ce sont tous des sous
types de clé) :
• ValueKey<T>. Supposons que vous ayez créé une liste de courses à l'aide d'un ListView et d'une série de Text
widgets (sans chaînes dupliquées). Une clé peut être attribuée de cette manière :
Utilisez un ValueKey lorsque vous avez un objet représenté par une valeur unique et constante.
Dans ce cas, la chaîne ne change pas et la liste ne contient pas de doublons, c'est donc un bon choix. Il s'agit
du type de clé par défaut renvoyé par Key :
• CléObjet. Supposons que vous disposiez d'une liste d'objets complexes, tels que List<Task> où Task est
composé en interne d’autres classes.
Lorsque vous n'êtes pas assuré qu'un seul champ est unique mais qu'une combinaison de plusieurs valeurs
l'est, optez pour une ObjectKey. Dans ce cas, nous sommes sûrs que chaque Tâche est unique mais certaines
peuvent avoir la même date par exemple. Cependant, nous savons qu'il ne peut pas y avoir 2 tâches avec la
même combinaison propriétaire/date/durée donc l'objet luimême est unique.
• Clé Unique. Cette clé n'est égale qu'à ellemême : il n'y en a qu'une dans toute l'application. Utilisez une
UniqueKey lorsqu'il n'y a pas de valeurs uniques constantes (donc pas de ValueKey) ni de combinaisons
uniques de valeurs (donc pas d'ObjectKey).
• Clé globale. Vous le verrez en action au chapitre 19 car il est également utilisé pour travailler avec la validation
des entrées sur les champs de formulaire. Généralement, les clés globales sont utiles pour synchroniser l’état
de plusieurs widgets.
En pratique, une ValueKey est utilisée lorsqu'une seule valeur peut représenter de manière unique un objet (comme un
identifiant). ObjectKey est utile lorsqu'il n'y a pas une seule valeur unique, mais qu'une combinaison de propriétés (telles
que le nom, le prénom, la date de naissance et le code fiscal) peut être unique. Dans Flutter, nous verrons que vous aurez
besoin d'une GlobalKey. Dans tous les autres cas, optez pour une UniqueKey.
La barre en bas représente le nombre de champs combinés qui représentent une entité unique.
En général, les clés peuvent être ignorées en toute sécurité car elles ne sont utiles que dans quelques cas (en fait, la clé
est facultative). Voici les cas où vous pourriez avoir besoin d'identifier de manière unique un widget avec une clé :
1. Au chapitre 16 vous verrez qu'une clé peut être utile lors des tests pour identifier facilement un widget dans l'arborescence. Étant unique,
Flutter peut rapidement atteindre le widget sur l'arborescence et en obtenir une référence.
2. Imaginez que vous ayez deux onglets ayant, sur les deux pages, une liste déroulante. Vous souhaitez stocker la
position de défilement même lorsque les onglets sont glissés afin que l'utilisateur n'ait pas à commencer à faire
défiler depuis le haut à chaque fois.
Les touches sont également utiles lorsque vous souhaitez faire glisser votre doigt pour supprimer un élément d'une liste : vous
verrez un exemple plus loin dans 19.2.1. Bien sûr, vous pourriez décider de définir une clé unique pour tout widget que vous
créez, mais cela ne servirait à rien.
• appeler setState,
Quand nous disons « le framework effectue de nombreuses reconstructions », nous voulons dire que la méthode build()
d'un widget spécifique est appelée plus d'une fois. En raison de la structure de l'arborescence des widgets, tous les
enfants seront également reconstruits car il doit y avoir une cohérence tout au long de la hiérarchie.
Flutter est très efficace pour parcourir l'arborescence des widgets et reconstruire les feuilles.
Cependant, si vous écrivez du mauvais code, votre application peut souffrir de problèmes de performances ou elle ne
fonctionnera pas toujours à 60 ips (en moyenne).
Même si Flutter est très rapide, vous n'avez pas besoin d'abuser de son efficacité car votre objectif doit toujours
être : "autoriser les reconstructions de widgets uniquement lorsque cela est vraiment nécessaire". Voyons ce qui
peut être fait pour écrire du bon code qui ne perd pas de temps ni de mémoire.
Vous savez déjà quelque chose sur les "constructeurs const" 4.3.1 et il est maintenant temps de voir pourquoi ils sont si utiles
dans Flutter. Disons que vous aviez ce simple widget :
@passer outre
Puisqu'un constructeur constant est défini pour cette classe, vous êtes autorisé à créer une liste constante de widgets. Bien
sûr, cela aurait été la même chose si vous aviez utilisé une Ligne ou une Colonne :
Si vous marquez une liste avec const , par conséquent, chaque objet qu'elle contient sera également constant.
Flutter crée des widgets constants une seule fois. Utiliser des constructeurs const sur des widgets revient à les
mettre en cache : une fois créés, ils ne seront plus jamais reconstruits.
C'est vraiment logique ! Si la classe est autorisée à avoir un constructeur const , alors elle est immuable.
Cela ne changera jamais au fil du temps, Flutter n'aura donc pas à le reconstruire plus d'une fois. Un
constructeur constant sur un gros sousarbre peut économiser beaucoup de temps de calcul.
Essayez d'utiliser autant que possible les constructeurs const car la méthode build d'un widget constant n'est exécutée
qu'une seule fois (au moment de la création). Toute reconstruction ultérieure ignorera simplement tous les widgets dont le
constructeur a été marqué avec const.
@passer outre
Dans ce cas, il n'y a pas de constructeur constant et donc le widget ne peut pas être inséré dans l'arborescence
en utilisant const SampleWidget(). Les widgets avec état peuvent bien sûr aussi avoir un constructeur const :
@passer outre
Essayez d'utiliser autant que possible des constructeurs constants, mais ne soyez pas obsédé par eux car ils ne peuvent
pas être créés dans toutes les situations. Dans certains cas, les constructeurs const peuvent mettre en cache de très
gros sousarbres et gagner beaucoup de temps de calcul !
ListView ListView
( enfants : const
La différence visuelle est minime mais la différence informatique est grande. Si SampleWidget avait une méthode
build() très complexe, l’écart de performances serait encore plus grand. Sans const, la liste entière est inutilement
reconstruite plusieurs fois.
@passer outre
]
),
const Text("Développé par X"),
]
);
}
}
Nous avons décidé de rendre FooterWidget apatride car il s'agit d'un bloc de code réutilisable qui ne change
pas d'état et qui n'est pas influencé par des événements externes. Il vit tout seul et peut être réutilisé dans de
nombreuses pages différentes pour afficher un pied de page en bas :
• dans Column, nous ne pouvons pas utiliser d'enfants : const [...] car Row ne définit pas de constructeur
constant. Néanmoins, nous pouvons mettre manuellement const dans l'enfant unique à l'intérieur, lorsque
cela est possible.
C'est ainsi que vous devez écrire des widgets et le même concept s'applique s'il s'agissait d'un widget avec état,
aucune différence. Au lieu de cela, ce que vous n’avez absolument PAS à faire est ceci :
( mainAxisAlignment : MainAxisAlignment.center,
C'est une fonction renvoyant le widget Colonne avec ses enfants : il ne faut absolument JAMAIS préférer les fonctions aux
widgets car :
• Flutter est obligé de reconstruire les widgets renvoyés par une fonction à chaque fois car il sait
rien à leur sujet (aucun BuildContext n'est fourni).
• Les classes sont des feuilles de l'arborescence des widgets mais les fonctions ne le sont pas et il n'y a donc pas de BuildContext
disponible.
Les widgets peuvent être mis en cache grâce aux constructeurs const ; les fonctions ne peuvent PAS être mises en cache
et sont donc exécutées à chaque fois. Vous devriez (ou en fait... devez !) toujours vous fier aux widgets réutilisables plutôt
qu'aux fonctions.
9.4 Architecture
Dans cette section, nous donnons un aperçu général de l'architecture du framework en approfondissant un
peu plus les détails. Flutter est divisé en trois couches (on dit qu'il s'agit d'un système en couches) où
chacune dépend de celle située en dessous. Les couches sont constituées de bibliothèques écrites dans
différents langages.
L'embedder est écrit dans différents langages selon la plateforme sur laquelle Flutter doit s'exécuter : Objective C++
pour iOS / macOS, Java / C++ pour Android et C++ pour Linux / Windows. C'est une application native qui se charge
de « héberger » vos contenus Flutter sur le système d'exploitation.
Lorsque l'application est démarrée, l'intégrateur fournit un point d'entrée valide, obtient des threads pour l'interface
utilisateur et le rendu, démarre le moteur Flutter et bien plus encore.
L'intégrateur se trouve au niveau de la couche la plus basse et interagit directement avec le système
d'exploitation en fournissant des points d'entrée pour l'accès aux services. Le développeur travaille
principalement sur la troisième couche et parfois sur la deuxième, mais jamais sur la première.
Le moteur est le cœur de Flutter, il est principalement écrit en C++ et il est toujours empaqueté dans le binaire produit par
l'outil de construction Flutter. Il s'agit d'un environnement d'exécution portable pour héberger l'application Flutter qui
comprend des bibliothèques de base pour les E/S réseau, les fichiers, les animations et les graphiques. Le moteur est
exposé au développeur via l'importation "dart:ui", qui enveloppe essentiellement les sources C++ dans des classes Dart.
Pour le monde du web, la situation est différente :
Le moteur C++ est conçu pour fonctionner avec le système d'exploitation, mais pour le Web, Flutter doit
gérer un navigateur. C’est pour cette raison que l’approche doit être différente. Dart peut être compilé en
JavaScript grâce au compilateur dart2js hautement optimisé. Par conséquent, les applications Flutter peuvent
également être portées. Il existe 2 manières de déployer une application pour le web :
2. Mode WebGL. Flutter utilise CanvasKit, qui est compilé par Skia pour WebAssembly.
Pour le Web, le runtime Dart n'est pas nécessaire car votre application Flutter est compilée en JavaScript comme
nous l'avons déjà dit. Le code produit est déjà minifié et peut être déployé sur n'importe quel serveur. Au moment
de la publication de ce livre (septembre 2020), le support Web de Flutter n'est disponible que dans le canal bêta.
9
Au cas où vous ne le sauriez pas, WebAssembly est reconnu par le W3C comme le
4ème langage à fonctionner nativement sur les navigateurs avec HTML, CSS et JavaScript. We
bAssembly peut être compilé à la fois AOT et JIT.
Vous avez déjà vu que, pour construire l'interface utilisateur, le développeur doit créer une arborescence de
widgets en imbriquant les widgets les uns dans les autres. En réalité, Flutter ne s'appuie pas uniquement sur des
widgets car en interne, il existe deux autres types d'arbres maintenus en parallèle 10. Dans cette section, nous
supposons que SomeText n'est qu'un simple widget affichant du texte.
@passer outre
texte : "Bonjour"
),
);
9https://www.w3.org/TR/wasmcore1/
10Voir « The Layer Cake » de Frederik Schweiger sur Medium
}
}
Au moment du rendu, Flutter appelle la méthode build() du widget qui peut introduire de nouveaux widgets, en cas d'imbrication. Dans
notre cas, l'arborescence des widgets contiendra Container, SomeText et d'autres que vous ne voyez pas réellement. En fait, si vous
regardez la définition d'un conteneur...
if (décoration != null)
current = DecoratedBox(décoration : décoration, enfant : actuel);
... vous remarquerez qu'une instance de DecoratedBox est ajoutée sous le capot si une décoration est donnée. Pour
11
cette raison, si vous effectuez une inspection DevTools, vous verrez plus d'enfants que vous n'en avez réellement
insérés. C'est parce que les widgets peuvent insérer d'autres widgets à l'intérieur mais vous ne le voyez tout
simplement pas ; l'arbre ressemble en fait à ceci :
Certaines cases sont en gris pour visualiser le fait qu'elles n'ont pas été ajoutées par vous. Parallèlement à l'arborescence
des widgets, Flutter construit également en parallèle l'arborescence des éléments et l'arborescence de rendu. Ils sont créés
en appelant respectivement createElement() et createRenderObject() sur le widget parcouru. Notez que createElement()
est toujours appelé sur les widgets mais createRenderObject() n'est appelé que sur les éléments dont le type est
RenderObjectElement. Alors oui, au final Flutter fonctionne avec 3 arbres.
Un élément peut contenir une référence à un widget et au RenderObject respectif. Il y a beaucoup de nouveautés
que vous n'avez jamais vues jusqu'à présent, alors analysons attentivement les arbres pour comprendre comment
fonctionne réellement Flutter.
• Arbre de rendu. Un RenderObject contient toute la logique pour restituer le widget correspondant et sa création
est coûteuse. Ils s'occupent de l'agencement, des contraintes, des hit tests et de la peinture. Le framework
les garde en mémoire autant que possible, modifiant leurs propriétés chaque fois que l'occasion se présente.
Ils peuvent être de plusieurs types :
– RenderFlex
– RenderParagraph
– Boîte de rendu...
Pendant la phase de construction, le framework met à jour ou crée un nouveau type de RenderObject
uniquement lorsqu'un RenderObjectElement est rencontré dans l'arborescence des éléments.
• Arbre des éléments. Un élément est le lien entre un widget et son RenderObject respectif, il contient donc des
références à l'intérieur. Les éléments sont très efficaces pour comparer des éléments et rechercher des
modifications, mais ils n'effectuent pas de rendu. Ils peuvent être de deux types :
– ComposantElement. Un élément qui contient d'autres éléments. Il est associé à un widget qui peut
emboîter d'autres widgets à l'intérieur.
– RenderObjectElement. Un élément qui participe à la peinture, à la mise en page et aux tests de frappe
étapes.
Chaque fois que l'arborescence des widgets est modifiée (par une bibliothèque de gestion d'état par exemple), Flutter utilise
l'arborescence des éléments pour faire une comparaison entre la nouvelle arborescence des widgets et l'arborescence de rendu.
Un élément est une « voie médiane » entre un widget et un RenderObject utilisé pour effectuer des comparaisons
rapides nécessaires pour maintenir les arbres à jour.
1. Un widget est « léger » et il est instancié rapidement, donc les reconstructions fréquentes ne posent aucun problème.
Les widgets sont tous immuables et c'est pourquoi l'état d'un StatefulWidget est implémenté dans une autre
classe séparée. Un widget avec état luimême est immuable mais l'état qu'il renvoie
peut muter.
@passer outre
Le widget luimême (Example) est immuable et donc son état mutable (_ExampleState) est implémenté dans
une autre classe. Un StatelessWidget est également immuable.
2. Un RenderObject est relativement « cher » et son instanciation prend du temps, il n'est donc recréé que lorsque
cela est vraiment nécessaire. La plupart du temps, ils sont modifiés en interne (la réutilisabilité est la clé).
texte : "Bonjour"
),
);
}
C'est ce que nous avions plus tôt. Bien sûr, lors du premier build les 3 arbres sont entièrement créés mais
désormais, le framework va essayer de recréer le moins possible l'arbre de rendu. Disons que notre
bibliothèque de gestion d'état a modifié le texte de SomeText.
),
);
}
Lorsqu'une reconstruction a lieu, grâce à l'arborescence des éléments, Flutter remarque que le type est
toujours le même (SomeText) mais qu'une propriété interne (texte) a changé. Par conséquent, le RenderObject
associé a juste besoin d’une mise à jour, ce qui est peu coûteux.
Ce processus est très rapide car le RenderObject n'est pas recréé mais il est simplement modifié. Les widgets et les
éléments sont également rapides à mettre à jour, c'est donc une bonne situation. Disons maintenant que notre
bibliothèque remplace SomeText par le widget Text de Flutter.
Le RenderObject associé n'est pas mis à jour : il doit être entièrement recréé car le widget a un type différent et il n'y
a donc aucun moyen de réutiliser l'ancienne instance. En résumé, Flutter s'appuie sur 3 arbres pour gérer efficacement
le rendu et essaie de réutiliser au maximum les RenderObjects. Grâce à Elements, le framework sait quand quelque
chose a changé sur les Widgets.
Le paramètre BuildContext que vous voyez dans n'importe quelle méthode build() représente
essentiellement l'élément associé aux widgets. En réalité, les objets BuildContext sont des objets
Element. L'équipe Flutter a créé BuildContext pour éviter l'interaction directe avec Element, qui doit être
utilisée par le framework et non par vous.
L'arbre de rendu est celui qui s'occupe réellement de peindre les éléments sur l'interface utilisateur. L'arborescence
des widgets est construite manuellement par vous, le développeur. L'arborescence des éléments est maintenue par le
framework pour décider s'il est temps de mettre à jour ou de recréer un RenderObject.
// demo.h
void print_demo() {};
// démo.c
#include <stdio.h>
#include "démo.h"
int main() {
print_demo();
renvoie 0 ;
}
Nous allons appeler void print_demo() écrit en C dans une application Dart grâce à FFI. Pour garder l'exemple simple, nous
supposons que chaque fichier se trouve dans le même dossier et que le code Dart suivant se trouve entièrement dans
main.dart. Commençons par les fondamentaux :
Le premier typedef utilise FFI pour représenter la signature de la fonction C que nous allons appeler. Il est
essentiellement utilisé pour représenter la fonction C dans son homologue Dart, identifié par PrintDemo. Bien
entendu, vous devez déclarer deux typedef dont les signatures correspondent.
void main() { //
Ouvrir le chemin final de la
bibliothèque = "demo_lib.dll"; // Sous Windows final lib =
FFI.DynamicLibrary.open(path);
En général, lorsque vous travaillez avec FFI, vous devez toujours créer deux typedef : un pour le "côté C" et l'autre pour le
"côté Dart". Lors de la construction du code C, divers fichiers sont créés mais seul celui avec l'extension suivante vous
intéresse : .dll sous Windows, .so sous Linux et .dylib sous macOS. Sous Windows, assurezvous que votre compilateur
exporte correctement vers la DLL les fonctions que Dart doit utiliser.
Le code cidessus peut facilement être utilisé par Dart de la même manière que nous l'avons fait plus tôt dans la fonction de démonstration.
Dans dart:ffi, vous trouverez de nombreux types représentant les types primitifs C, tels que Int32, Double, UInt32, Handle
et bien plus encore.
typedef sum_c = FFI.Int32 Fonction (FFI.Int32 a, FFI.Int32 b); typedef Somme = int Fonction
(int a, int b);
A titre d'exemple, disons que le code cidessus va appeler la fonction getPersonName(): String déclarée dans une
application Android native écrite en Kotlin. Il y a une configuration similaire à faire dans le natif
12https://api.dart.dev/stable/2.9.2/dartffi/dartffilibrary.html
Dans les deux cas, l'instance MethodChannel doit être créée avec le même nom (« personne ») , sinon le « lien » entre Dart
et Kotlin ne fonctionnera pas. Le nom de la fonction correspond au nom réel du côté natif juste pour plus de commodité,
mais ce n'est pas obligatoire. Avec EnsureMethod<T>() vous pouvez également transmettre des paramètres au cas où la
fonction en demanderait. Par exemple, si vous avez appelé ceci dans Dart...
... cela signifierait que vous attendez une méthode dans le langage natif appelée getRandom demandant un seul
paramètre entier. Cet exemple est plutôt écrit en Swift mais la logique est toujours la même (juste une syntaxe
différente) :
Grâce à call.arguments vous accédez à l'argument passé via le canal de méthode qui peut être, par exemple, un
type primitif ou une map. Un MethodChannel est une interface commune à Dart et à l'autre langage natif qui
permet à Flutter d'envoyer/recevoir des messages. Voici un schéma de la façon dont les canaux de méthode sont
implémentés :
Dans le code natif, les canaux de méthode doivent être appelés dans le thread principal et non en arrièreplan
un (sous Android, le thread "principal" est en fait appelé thread UI). En résumé, le flux de communication fonctionne comme
ceci :
1. Flutter envoie un message à la partie iOS ou Android de l'application à l'aide d'un canal de méthode ;
2. le système sousjacent écoute sur le canal de méthode et le message est donc reçu ;
3. une ou plusieurs API spécifiques à la plateforme sont appelées, en utilisant le langage de programmation natif ;
Vous ne pouvez pas faire la même chose avec FFI car il n'y a pas de bibliothèques à lier et les données nécessitent une
sérialisation/désérialisation : les canaux de méthodes fonctionnent différemment et nécessitent également une implémentation
native.
Flutter permet au développeur de personnaliser complètement l'interface utilisateur : vous contrôlez chaque pixel de
l'écran. Les mises en page peuvent être créées à partir de zéro, mais pour les cas d'utilisation courants, il existe une
série de widgets intégrés qui permettront d'économiser des heures de travail.
10.1 Matériel
Flutter vous propose une série de composants prédéfinis pour créer des applications adoptant le design typique
d'Android, également connu sous le nom de Material Design. Il est très probable que vous ayez déjà vu ce genre
d'apparence d'interface utilisateur quelque part, en mode paysage :
Il s'agit d'un exemple classique de conception matérielle avec un bouton d'action flottant (FAB) en bas à droite et
une barre d'application en haut. Il existe deux manières possibles de créer la disposition des matériaux cidessus :
• Non recommandé. Créez la mise en page entière à partir de zéro à l'aide de widgets sans état et avec état.
En fait, il y aurait beaucoup de travail à faire car vous devrez gérer les dimensions de l'écran, le
positionnement, les boutons, etc.
• Recommandé. Importez le package Material.dart et utilisez le widget MaterialApp() fourni par Flutter. Il
représente le « squelette » d’une interface utilisateur suivant les directives de conception matérielle
1
; c'est très pratique :
}
}
Le gros avantage est que vous n'avez pas besoin d'écrire des milliers de lignes de code pour essayer de
1https://material.io/design/guidelinesoverview/
imiter l’aspect de la conception matérielle. Flutter vous offre déjà tout ce dont vous avez besoin et avec
MaterialApp, vous êtes assuré de créer une belle application matérielle.
Le constructeur de la classe MaterialApp(...) possède de nombreux paramètres intéressants que nous aborderons plus
loin dans le livre, tels que ceux pour la navigation et la localisation des pages de configuration. Nous vous proposons
désormais une vitrine des widgets matériels les plus pertinents.
10.1.1 Échafaudage
Comme vous l'avez vu dans l'extrait de code précédent, la classe Scaffold(...) implémente pour vous la structure de
base de conception des matériaux. En plus de fournir l'apparence typique d'Android, il donne la possibilité de gérer de
nombreux autres widgets :
• Barre d'applications. Elle est toujours placée en haut de l'écran et c'est l'équivalent Java/Kotlin de la classe
Toolbar. Si l'échafaudage avait un tiroir, un bouton hamburger serait automatiquement ajouté pour gérer
l'ouverture/fermeture du menu.
Lors de la navigation entre les pages de votre application (ou les itinéraires, dans le monde Flutter), l'AppBar
ajoute automatiquement un bouton "retour", la flèche gauche typique.
Échafaudage
( appBar : AppBar(
// Définissez ceci sur false si vous ne souhaitez pas que le
bouton // retour apparaisse automatiquement à côté // du titre
lors de la navigation automatique parmi les pages.ImplyLeading :
false,
// Titre du
titre : const Text(" Barre d'application sans bouton Retour"),
)
)
Les boutons de droite sont appelés boutons d'action et ils peuvent être définis en transmettant une liste de
widgets aux actions nommées paramètre. En général, les actions sont des icônes cliquables qui représentent
visuellement le but de ce bouton.
Scaffold( appBar :
AppBar( actions :
[ IconButton( icône : const Icon(Icons.info),
onPressed : () {...} ),
]
)
)
• Tiroir. Un tiroir est un conteneur qui glisse horizontalement depuis un côté de l'écran pour afficher une série
d'éléments. En général, il est utilisé pour afficher une combinaison d'icônes et de textes qui redirigent
l'utilisateur vers des pages spécifiques de l'application.
Par défaut un tiroir coulisse de gauche à droite mais vous pouvez également créer un endDrawer qui coulisse dans le sens
inverse, de droite à gauche. C'est toujours la même classe Drawer() mais elle est affectée à un autre constructeur nommé
paramètre.
Scaffold( // Le
),
ListTile( en
)
)
),
// Comme avant mais cela glisse // de droite à gauche
endDrawer: Drawer()
• Bouton d'action flottant. Également connu sous le nom de FAB, il s'agit d'un bouton arrondi spécial avec élévation qui apparaît
généralement dans le coin inférieur droit de l'écran. En utilisant un floatingActionButtonLocation, vous pouvez décider de la
position du widget :
Échafaudage
),
)
Cet extrait ajoute un FAB à la position par défaut de l'écran (en bas à droite) :
Voulezvous l’avoir au centre plutôt qu’à droite ? Il existe de nombreux postes disponibles que vous pouvez utiliser :
),
floatActionButtonLocation :
FloatingActionButtonLocation.centerFloat,
Le widget Scaffold est certainement très important car il constitue l'élément de base des interfaces utilisateur matérielles et
vous l'utiliserez probablement très souvent. Dans la troisième partie du livre, vous verrez de nombreux exemples mettant
2
en évidence la force de ce widget.
Flutter fournit une très grande collection de widgets qui suivent les directives officielles en matière de matériel. L'équipe
Flutter les améliore constamment et en ajoute de nouveaux au fil du temps. Nous n'allons pas faire une liste complète de
tous les types de widgets car vous les rencontrerez dans les exemples pratiques de la partie III.
2https://api.flutter.dev/flutter/material/Scaffoldclass.html
10.1.2.1 Boutons
Les boutons sont fondamentaux dans tout type d'application car ils constituent le moyen le plus intuitif d'indiquer à l'utilisateur
que lorsqu'on appuie dessus, quelque chose va se produire. Flutter propose de nombreux widgets matériels créés pour suivre
les directives matérielles pour les boutons.
• Bouton surélevé.
Il s'agit d'un bouton Android typique avec une forme rectangulaire et une élévation par défaut (l'ombre derrière). Lors du
survol, l'élévation augmente afin que vous puissiez voir visuellement l'interaction avec le bouton.
• Bouton Plat.
C'est très similaire à un RaisedButton à la différence qu'ici il n'y a pas d'élévation et rien ne se passe visuellement
lorsque vous appuyez dessus. C'est comme une version "statique" d'un RaisedButton.
• Barre de boutons.
C'est un conteneur horizontal contenant une série de boutons. Cela peut être utile dans les cas où vous avez une boîte
de dialogue et que vous souhaitez afficher deux boutons comme NON et OUI.
Barre de boutons (
alignement : MainAxisAlignment.center,
enfants: [
• BoutonIcône.
Vous avez déjà vu un exemple dans l'AppBar car ils rendent les icônes cliquables ; c'est un bouton
dont le contenu est une icône matérielle plutôt qu'une simple chaîne.
Le type de dialogue le plus courant est celui qui vous demande la confirmation ou le rejet d'une
action ; ce type de widget est connu sous le nom d'AlertDialog. Vous devez utiliser la méthode
showDialog(...) pour le faire apparaître à l'écran.
showDialog<void>( contexte :
contexte, constructeur :
(contexte BuildContext) { return AlertDialog( titre :
const Text("Exemple"),
contenu : const Text("Aimez vous ce livre ?"),
actions : [ FlatButton(
]
);
}
);
Par défaut, une boîte de dialogue peut être fermée en appuyant simplement en dehors de la zone blanche de la boîte. Si
vous ne souhaitez pas ce comportement, définissez simplement barrièreDismissible: false et la boîte de dialogue ne
disparaîtra que si un bouton est enfoncé.
Bien sûr, vous pouvez entièrement personnaliser la boîte de dialogue autant que vous le souhaitez car le paramètre de contenu
peut être n'importe quel widget tel qu'une image. Vous pouvez utiliser des icônes ou des boutons en relief au lieu de boutons
plats. Vous pouvez même modifier les bordures de la boîte de dialogue.
autre.
showDialog<void>( context:
context, builder:
(BuildContext context) { return AlertDialog( title:
const Text("Example"),
content: const Text("Aimez vous ce livre?"),
actions: [... ], forme : RoundedRectangleBorder (
borderRadius : BorderRadius.circular(30),
)
);
}
);
Si vous souhaitez que la boîte de dialogue ressemble à un cercle, utilisez plutôt CircleBorder comme forme de bordure. Afin de
fermer une boîte de dialogue, il est nécessaire d'utiliser la classe Navigator {} qui sera entièrement abordée au chapitre 12.
AlertDialog( actions :
[ FlatButton( enfant :
const Text("Fermer"), onPressed : () =>
Navigator.pop(context),
),
]
);
Il existe également d'autres types de boîtes de dialogue que vous pouvez utiliser dans vos applications :
• SimpleDialog. Il s'agit d'une boîte de dialogue simple dans laquelle l'utilisateur peut choisir entre une
série d'options. La valeur sélectionnée sera renvoyée de manière asynchrone par T showDialog<T>().
SimpleDialogOption( onPressed:
() => Navigator.pop(context, "chocolate"), enfant :
const Text("Chocolate"),
),
SimpleDialogOption( onPressed:
() => Navigator.pop(context, "apple"), enfant :
const Text("Apple"),
),
]
);
}
);
Très rapidement, la classe Navigator est utilisée pour naviguer entre les itinéraires (les pages de votre
application) et fermer les boîtes de dialogue d'alerte (qui sont également des itinéraires). La méthode pop()
supprime l'itinéraire actuellement visible de l'écran et ainsi l'alerte disparaît.
Comme toujours, ceci est entièrement personnalisable car au lieu d'un texte brut ennuyeux, vous auriez pu mettre
n'importe quel widget tel que des images ou un texte coloré avec une police sophistiquée.
• showBottomSheet. Cette méthode anime une boîte de dialogue qui glisse du bas de l'écran.
écran jusqu'à une certaine hauteur, qui est déterminée par la taille du widget contenu. En général, c'est une
bonne idée d'avoir un conteneur comme enfant "de base" qui a une configuration simple en termes de
hauteur, de forme et de couleurs.
showBottomSheet<String>( contexte :
contexte, constructeur :
(contexte BuildContext) { return Container( couleur :
Colors.blueAccent,
hauteur : 40, enfant : const
Center( enfant :
Text(
"BottomSheet", style :
TextStyle( couleur :
Colors.white,
)
)
),
);
}
);
Le résultat de ce code est une bande bleue en bas de l'écran qui glisse vers le haut et affiche son contenu. Appel de
Navigator.pop (contexte); ferme la boîte de dialogue qui glisse vers le bas jusqu'à disparaître.
10.2 Cupertino
Le widget CupertinoApp est l'équivalent Apple de MaterialApp car il se concentre sur la conception iOS typique. Il
s'agit d'une série de composants prédéfinis qui vous permettent de créer des applications qui suivent le style
typique de l'interface utilisateur iOS.
Les mêmes recommandations que nous avons faites pour le matériel s’appliquent également ici. Vous pouvez créer un
thème iOS à partir de zéro, mais cela nécessiterait beaucoup de temps et de tests ; cela n'en vaut pas la peine car vous
pouvez utiliser les composants cupertino de Flutter.
Le constructeur de la classe CupertinoApp() possède de nombreux paramètres intéressants que nous aborderons plus
loin dans le livre, tels que ceux permettant de configurer la navigation et la localisation des pages. Nous vous proposons
maintenant une vitrine des widgets Cupertino les plus pertinents.
Il existe deux principaux types d'échafaudages dans la bibliothèque de Cupertino : le CupertinoPageScaffold, qui a une
barre de navigation en haut, et le CupertinoTabScaffold, qui utilise des onglets pour afficher le contenu.
CupertinoApp( home:
const CupertinoPageScaffold( // C'est une barre
"simple" avec le titre et quelques icônes navigationBar:
CupertinoNavigationBar( middle: Text("Page title"),
trailing: Icon(CupertinoIcons.info),
),
enfant : Centre(
enfant : Texte ("Corps de l'application")
)
),
);
Le widget CupertinoTabBar implémente à la place le modèle de navigation par onglets dans un style iOS typique. Il permet
d'afficher plusieurs pages dans une seule vue en appuyant sur les icônes en bas, mais aucun geste de balayage n'est activé. Le
rappel onTap est déclenché lorsque l'utilisateur appuie sur une icône :
Le seul widget requis est l'icône, mais le texte qui se trouve en dessous peut être omis s'il n'est pas nécessaire. Visitez la
3
documentation officielle pour voir tout paramètre personnalisé pouvant être défini pour ce widget.
CupertinoTabScaffold(
3https://api.flutter.dev/flutter/cupertino/CupertinoTabBarclass.html
[ BottomNavigationBarItem(
icône : Icône (Icons.home),
titre : Texte ("Accueil"),
),
BottomNavigationBarItem(
icône : Icône(Icons.email), titre :
Texte("Email"),
),
],
),
tabBuilder : (contexte, index) {...}
),
Une fois les icônes définies via les éléments, le paramètre tabBuilder définit quelles pages doivent être affichées
lorsqu'un élément est appuyé. Lorsque l'onglet devient inactif, son contenu est automatiquement mis en cache dans
l'arborescence des widgets pour une meilleure réutilisation lors des appels ultérieurs.
CupertinoTabScaffold( tabBar:
CupertinoTabBar(...) tabBuilder:
(contexte, index) => CupertinoTabView( builder:
(contexte) { switch
(index) {
cas 0 :
En général, CupertinoTabScaffold devrait être votre premier choix lorsqu'une disposition à onglets est requise pour
votre interface utilisateur ; si ce n'est pas le cas, optez pour CupertinoPageScaffold qui est essentiellement un iOS "simple"
page.
Une boîte de dialogue d'alerte iOS qui avertit l'utilisateur et nécessite une action, définie par des boutons. Généralement,
une instance de CupertinoAlertDialog est transmise en tant que widget enfant à showDialog(), qui affiche la boîte de
dialogue.
showDialog<void>( contexte :
contexte, constructeur :
(contexte) {
return CupertinoAlertDialog( titre : const
Text("Cupertino Alert"), contenu : const Text("iOS alert
dialog"), actions : <Widget>[ CupertinoButton( enfant : const
Text("Ok"), onPressed: () =>
Navigateur.pop(contexte),
4https://github.com/flutter/flutter/wiki/Roadmap
],
);
}
),
Généralement, les boutons de dialogue iOS sont rouges, en cas d'action de suppression, ou en vieux bleu, en cas
d'option par défaut. Plutôt que d'utiliser un CupertinoButton, un CupertinoDialogAction serait préférable :
CupertinoAlertDialog( actions :
<Widget>[
CupertinoDialogAction(
isDefaultAction : true, enfant :
const Text("Ignore"), onPressed : () {...},
),
CupertinoDialogAction( isDestructiveAction :
true, enfant : const Text("Delete"),
onPressed : () {...},
),
],
);
isDestructiveAction : true rend le texte rouge tandis que isDefaultAction : true le rend
bleu.
• Bouton Cupertino.
Il s'agit d'un bouton iOS plat typique qui n'a pas de couleur d'arrièreplan par défaut mais qui peut bien sûr être
défini via la propriété color. Lorsque vous appuyez dessus, le rappel onPressed est déclenché.
CupertinoButton( enfant :
const Text(" Bouton iOS", style : TextStyle(
couleur: CupertinoColors.blanc
),
),
couleur : CupertinoColors.activeBlue,
onPressed : () {...},
),
La bibliothèque de Cupertino contient moins de widgets que de matériel car un style de bouton iOS a moins de « variantes »
que son homologue Android, mais il est néanmoins entièrement personnalisable. Assurezvous de consulter le catalogue de
5
Cupertino dans la documentation officielle de Flutter.
Vous venez de voir que le framework contient de nombreux composants prédéfinis utiles que vous pouvez utiliser pour
créer de belles interfaces utilisateur. Avant de commencer le développement, vous devez réfléchir aux besoins de
l'utilisateur final et au nombre de platesformes que vous devez prendre en charge en même temps.
Au moment de la rédaction de ce livre, Flutter est en qualité de production uniquement pour les appareils mobiles. Le
support Web est en version bêta tandis que le support de bureau en est encore au début de l’alpha. Néanmoins, ce n'est
qu'une question de temps car à l'avenir, Flutter ciblera n'importe quelle plateforme. Pour cette raison, vos applications
doivent s'adapter à différentes tailles d'écran et types d'entrée.
Le cas le plus simple est celui dans lequel vous devez créer une application qui va fonctionner exclusivement
sur Android ou iOS, pas sur les deux. Si la structure de l'app est assez « standard », les widgets Material ou
Cupertino suffisent à faire le travail. Pour être plus précis, dire « standard » dans le monde Android fait
référence au fait d'avoir au moins :
• une barre horizontale en haut avec le titre et éventuellement une série de boutons ;
5https://flutter.dev/docs/development/ui/widgets/cupertino
Un échafaudage vous aide à créer facilement une mise en page avec les caractéristiques cidessus, mais si vous souhaitez avoir une
structure complètement différente ou quelque chose de très particulier, ne l'utilisez pas. Regardez ces deux exemples :
Les deux sont des applications Android et vous pouvez immédiatement voir que celle de gauche est très proche d'une application
matérielle "traditionnelle" : elle possède un tiroir, une barre d'applications, des icônes et un onglet glissant en bas.
L'exemple de droite serait impossible à réaliser avec un Scaffold car il a une structure complètement
différente de celle proposée par un Scaffold, donc dans ce cas il faudrait tout créer à partir de zéro.
Le problème est le suivant : n'utilisez pas la bibliothèque de matériaux lorsque la conception de votre application n'a rien à voir
avec les directives en matière de matériaux. Si la mise en page est très particulière, il vaudrait mieux la créer à partir de zéro et
c'est parfaitement réalisable dans Flutter. Ne vous forcez pas à utiliser les bibliothèques de matériaux/cupertino de Flutter car
elles ne constituent pas toujours le bon choix pour l'interface utilisateur que vous allez implémenter.
Bien entendu, les mêmes recommandations s'appliquent également à la bibliothèque de Cupertino. Si votre
application iOS doit être très différente de ce qui est une conception iOS "standard", optez pour une interface
utilisateur personnalisée complète et ne comptez pas sur Cupertino.
C’est le cas le plus difficile et probablement le plus courant car, en général, les entreprises souhaitent que leur application soit
disponible à la fois sur Android et sur iOS. C'est ici que les outils multiplateformes, tels que Flutter, viennent à la rescousse.
Comme toujours, ils peuvent devenir vos ennemis s’ils sont mal utilisés, mais nous avons quelques recommandations à vous
faire.
Dans les paragraphes suivants, imaginons qu'on vous demande de créer une application pour un restaurant
avec la possibilité de faire une réservation, de voir le menu et une galerie d'images.
La cohérence de l'interface utilisateur est très importante : l'application doit avoir exactement la même apparence quel que soit le système d'exploitation sur lequel elle s'exécute.
Votre client demandera sûrement non seulement le même design, mais aussi des fonctionnalités identiques.
Voici ce que nous proposons :
• N'utilisez PAS Material et Cupertino pour créer deux versions différentes pour la même application.
Par exemple, faire ceci est absolument faux :
vide main() {
if (Platform.isAndroid) { runApp(const
AndroidVersion()); } autre { runApp(const
iOSVersion());
}
}
// Utilise la classe
MaterialApp . AndroidVersion étend StatelessWidget {}
// Utilise la classe
CupertinoApp iOSVersion étend StatelessWidget {}
Avec cette approche, vous êtes obligé de conserver 2 versions distinctes de la même application et tous les
avantages du développement multiplateforme disparaissent. C'est la même chose que d'avoir deux projets
natifs séparés en Java/Kotlin et ObjectiveC/Swift !
• Créez une interface utilisateur agréable pour les deux systèmes d'exploitation afin que vous ne puissiez écrire le code qu'une seule fois.
Même si cela peut paraître évident, cela vaut la peine de le dire plutôt que de le prendre pour acquis.
Flutter est conçu à cet effet.
}
return "Bienvenue utilisateur Android !";
}
Dans cet exemple, la même base de code unique fonctionne pour Android et iOS. Certains paramètres
peut différer selon le type de système d'exploitation, mais ce n'est pas grave puisqu'il y a toujours un seul projet en cours
de maintenance.
Vous devriez essayer autant que possible de ne pas dépendre des paramètres ou des configurations spécifiques au système d'exploitation ; essayez
toujours, lorsque cela est possible, de faire fonctionner votre architecture correctement quel que soit le système d'exploitation sur lequel elle s'exécute.
Écrire des applications multiplateformes robustes n'est pas facile, mais Flutter vous donne un grand coup de pouce dans la bonne direction !
Les applications de haute qualité sont réactives car elles s'adaptent automatiquement à la taille de l'écran en réorganisant
l'interface utilisateur afin de remplir correctement tout l'espace disponible. Pensez simplement à un changement d'orientation dans
votre téléphone mobile par exemple : l'espace horizontal augmente/diminue et votre interface utilisateur devrait s'adapter en
conséquence.
Les listes sont un élément courant de l'interface utilisateur et en fait ListView est un widget Flutter très populaire. Dans l'image ci
dessus, vous pouvez voir une liste déroulante avec une série d'éléments. Cela a l'air bien tel quel, mais si nous faisions pivoter
l'écran, la situation changerait :
Il y a maintenant beaucoup d'espace libre sur la droite et il n'y a que deux éléments entièrement visibles ; l'utilisateur
doit faire défiler beaucoup plus qu'avant. Dans de tels cas, une bonne interface utilisateur réactive réorganise son
contenu afin de couvrir tout l’espace disponible.
Ce problème n’est pas uniquement lié au cas de rotation de l’écran. Si votre application fonctionne à la fois
sur les téléphones mobiles et les tablettes (ce qui est très probable), il existe de grandes différences de taille sur
les écrans et votre interface utilisateur doit être suffisamment flexible pour être belle dans tous les cas.
Plus généralement, si vous envisagez d'exécuter votre application Flutter sur des appareils mobiles, des ordinateurs de bureau
et sur le Web, il y aura d'énormes différences de taille d'écran. Vous devez vraiment en tenir compte et créer l'interface
utilisateur en conséquence.
Considérant l’exemple cidessus d’une simple liste d’éléments, le code est assez simple pour le moment. Le ListView
est toujours utilisé dans le sens vertical sans tenir compte de l'orientation de l'écran. Comme nous l'avons vu, il ne s'agit
pas du tout d'une utilisation responsive :
Échafaudage
);
}
)
),
Le widget LayoutBuilder donne des informations sur les contraintes du parent telles que la largeur et la
hauteur. Pensez vraiment à utiliser cette classe pour rendre vos applications réactives, car elle peut être
utilisée pour décider comment organiser l'interface utilisateur en fonction de l'espace disponible.
Scaffold( body :
LayoutBuilder( builder :
(contexte BuildContext, tailles BoxConstraints) {
si (sizes.maxWidth < 500) {
return const ListData();
}
renvoie const GridData();
}
)
)
La classe BoxConstraints donne une série d'informations sur la taille du widget parent. Dans ce cas, nous l'utilisons pour
décider ce qui suit : si la largeur est inférieure à 500, une liste est bonne, sinon il est préférable de la réorganiser dans
une grille pour mieux remplir l'espace.
@passer outre
);
}
);
}
}
constGridData ();
@passer outre
Le widget Grid place automatiquement les éléments dans une grille et le nombre de colonnes est déterminé par la valeur
transmise à crossAxisCount. Ce code est dit réactif car lorsque la largeur de l'écran change, grâce à LayoutBuilder,
l'interface utilisateur est réorganisée en conséquence.
Si vous avez ouvert votre application sur une tablette dotée d'un écran très large, vous verrez déjà la grille au lieu
de la liste. LayoutBuilder est idéal pour les rotations d'écran et bien plus encore ; vous pouvez (et devez) l'utiliser
pour adapter l'interface utilisateur aux dimensions de nombreux appareils tels que les téléphones mobiles, les tablettes
et ordinateur de bureau.
La classe MediaQuery est une sorte de version plus puissante de LayoutBuilder car elle est toujours disponible et vous donne plus de contrôle
sur les différents paramètres de l'écran. Cela nécessite juste un contexte :
@passer outre
Avec la taille, vous avez également accès à la hauteur, au rembourrage, aux distances et bien plus encore. Vous pouvez
gérer la valeur nullable renvoyée par of() avec une valeur par défaut (comme nous l'avons fait) de avec une vérification nulle
(une instruction if ). Par exemple, vous souhaiterez peutêtre savoir quelle est l'orientation actuelle de l'appareil :
// Utiliser une vérification nulle plutôt que de fournir une valeur par défaut if ((orientation !=
null) && (orientation == Orientation.portrait)) {...}
Les applications de haute qualité sont réactives et nous vous encourageons donc fortement à ne pas tester votre application
uniquement sur un appareil mobile verrouillé en mode portrait. Faites de nombreux tests avec différentes tailles d'écran en
mode portrait et paysage. En dehors de cela, voici quelques conseils que vous pouvez essayer :
• Si vous devez rendre votre application responsive, évitez d'utiliser MediaQuery.of(context) pour calculer les espaces et
les dimensions. Il contient de nombreuses métadonnées sur l'écran physique mais ne sait rien du widget luimême.
• Utilisez LayoutBuilder pour créer des mises en page réactives car il fournit des dimensions sur le widget contenant et
NON sur l'écran luimême, comme le fait MediaQuery. Cet exemple pourrait donner une meilleure idée de la différence
entre les deux approches.
// 1.
Widget build (contexte BuildContext) { largeur finale =
MediaQuery.of (context)?.size.width ?? 0 ;
return Text("$largeur");
}
// 2.
Widget build(contexte BuildContext) { return
LayoutBuilder( builder :
(contexte, contraintes) { return Text("$
{constraints.maxWidth}");
}
);
}
Pour tirer le meilleur parti de cet exemple, placez cette méthode de construction dans le widget racine de votre arborescence.
Vous verrez que les deux cas 1 et 2 imprimeront la même taille car :
// 1.
Widget build (contexte BuildContext) { largeur
finale = MediaQuery.of (context)?.size.width ?? 0 ; return Padding( padding :
EdgeInsets.all(15),
enfant : Text("$width"),
);
}
// 2.
Widget build(Contexte BuildContext) { return
Padding( padding :
EdgeInsets.all(15), enfant :
LayoutBuilder( builder :
(contexte, contraintes) { return Text("$
{constraints.maxWidth}");
}
)
);
}
Voici la différence importante entre les deux widgets. MediaQuery renvoie toujours la même valeur, car la largeur
de l'écran n'a pas changé, mais LayoutBuilder renvoie une taille différente, 30 unités plus petite qu'auparavant.
– MediaQuery renverra toujours la même valeur car la largeur de l'écran de l'appareil n'a pas
changé. Il ne prend pas en compte le rembourrage.
Vous ne devriez vraiment pas utiliser MediaQuery car il ne s'agit que des « mesures » de l'appareil ; utilisez plutôt
LayoutBuilder qui calcule l'espace restant réel en tenant compte des dimensions des autres widgets qui le
contiennent.
Comme nous l'avons déjà vu, utilisez MediaQuery.orientation si vous avez seulement besoin de savoir si l'appareil est en
mode paysage ou non. Sachez qu'il existe également le widget OrientationBuilder :
OrientationBuilder (
builder : (contexte, orientation) { if (orientation ==
Orientation.portrait) { //travailler en mode portrait } else { //
travailler en mode paysage
}
}
);
Faites attention au fait qu'OrientationBuilder dépend de l'orientation du widget parent, qui n'est pas l'orientation du
périphérique. Par exemple, si votre appareil était en mode portrait et que vous ouvriez le clavier pour remplir un formulaire,
la hauteur pourrait devenir plus petite que la largeur et OrientationBuilder renverrait ainsi le paysage.
1. Utilisez MediaQuery pour obtenir l'orientation actuelle de l'appareil. En pratique c'est le physique
position (horizontale ou verticale) du téléphone mobile ou de la tablette,
2. Utilisez OrientationBuilder pour obtenir l'orientation actuelle en fonction de l'orientation du widget parent. Cela ne
dépend pas de l'orientation de votre appareil physique mais des dimensions du widget contenant (que la hauteur
soit inférieure ou supérieure à la hauteur).
Colonne( enfants : [
const Text("Mon nom"), const
Text("Mon nom"),
Vous voulez que votre nom et votre prénom restent toujours en haut, mais la liste de vos innombrables compétences doit
défiler. La solution cidessus n'est pas réalisable car une colonne se développe pour remplir tout l'espace disponible et un
ListView fait de même. Ils n’ont pas tous deux de hauteur définie. Voici à quoi ressemblent les limites :
Ce sont deux widgets sans valeur spécifique pour la hauteur car ils essaient toujours de remplir tout l'espace disponible et il
n'est donc pas possible de déterminer une valeur à l'avance. Si vous les avez imbriqués
comme ça...
... vous obtiendrez une erreur car les deux essaient de s'étendre pour couvrir tout l'espace mais il n'y a pas de widgets
parents avec une hauteur fixe. La solution est de toujours s'assurer qu'ils se trouvent à l'intérieur d'un widget qui définit
une hauteur finie et il existe plusieurs façons de procéder :
1. Vous pouvez utiliser le widget Étendu qui se développe pour remplir l'espace disponible restant dans un
Colonne ou dans une ligne en donnant un ensemble complet de dimensions.
Colonne( enfants : [
const Text("Mon nom"), const
Text("Mon nom"),
Développé
( enfant : ListView
( enfants : const [
Texte("Compétence 1"),
Texte("Compétence 2"),
]
),
),
]
);
De cette façon, ListView fonctionne correctement car Expanded ne renvoie pas une hauteur infinie : il se développe
pour remplir exactement l'espace restant et calcule une hauteur finie. Ce widget peut également être utilisé avec des
colonnes et des lignes.
Une ligne a le même "problème" mais dans l'autre sens (l'axe horizontal). Chaque
considération faite pour les colonnes est également valable pour les lignes avec la seule
différence que l'orientation est sur l'axe des x plutôt que sur l'axe des y.
Colonne( enfants : [
Texte("Mon nom"),
Texte("Mon nom"),
ListeView(
rétractable : vrai,
enfants : [
Texte("Compétence 1"),
Texte("Compétence 2"),
]
)
]
);
En définissant rétractableWrap: true, la liste occupe uniquement l'espace dont elle a besoin et elle ne
s'étend pas pour remplir tout l'espace disponible. De cette façon, il a une hauteur bien définie car elle est
calculée en fonction des dimensions des enfants.
Cependant, comme Column ne gère pas les débordements avec les barres de défilement, si la liste est trop longue et
que l'écran ne peut pas la contenir entièrement, vous verrez quand même l'exception de débordement d'exécution. Ce
n'est pas vraiment une solution "sûre".
3. Le widget Expanded est généralement le plus pratique à utiliser mais vous pouvez placer la liste dans
n'importe quel type de widget avec une hauteur bien définie comme un Container ou un SizedBox.
SizedBox.fromSize( taille :
const Size(100, 100), enfant :
ListView(...),
);
Vous pouvez également faire en sorte que le conteneur remplisse automatiquement toute la largeur et
forcer sa hauteur à être une valeur fixe. C'est comme un Expanded sur lequel vous pouvez contrôler la
hauteur et/ou la largeur.
Récipient(
),
enfant : ListView(...),
);
Il existe de nombreuses possibilités, notamment l'utilisation combinée d'un LayoutBuilder avec un Container, mais
cela peut devenir trop compliqué. Essayez de rester simple en utilisant Expanded ou un autre widget unique qui
gère automatiquement les tailles.
En général, vous rencontrez ce genre de problèmes de « contrainte infinie » avec les lignes, les
colonnes et les listes car elles sont très fréquemment utilisées. Très souvent, la solution simple est
appelée Expanded mais si vous recherchez une approche plus sophistiquée, pensez à utiliser SizedBoxes ou
Conteneurs.
MaterialApp( thème :
ThemeData( fontFamily : "Times New Roman",
),
)
... la propriété fontFamily par défaut du widget de votre application sera "Times New Roman". C'est très pratique
car, par exemple, n'importe quel widget Texte héritera de cette famille de polices spécifique et vous n'aurez pas à
le faire tout le temps :
)
)
Avec l'utilisation de ThemeData, les modifications sont automatiquement répercutées sur tous les enfants afin que la maintenance soit effectuée.
le financement est beaucoup plus facile. Souhaitezvous utiliser une autre famille de polices telle que « Georgia » ?
Configurez simplement la nouvelle valeur dans ThemeData et automatiquement les modifications seront reflétées ailleurs.
C'est un endroit centralisé dans lequel vous pouvez styliser les widgets :
MaterialApp( theme:
ThemeData( buttonColor:
Colors.red, // Couleur d'un RaisedButton focusColor: Colors.white, // Couleur
lorsqu'un widget est focalisé selectedRowColor: Colors.orange,
PrimaryColor : Colors.green,
accentColor : Colors.red,
),
)
Généralement, un ThemeData doit toujours déclarer un PrimaryColor, qui définit la couleur des widgets d'interface
utilisateur les plus courants (échafaudages, barres d'onglets, focus de champs de texte...), et un PrimaryAccent, qui
définit la couleur des widgets de premier plan (FAB, lueur de défilement de liste. ..). Si vous souhaitez implémenter
un thème sombre ou clair pour votre application, envisagez d'utiliser les constructeurs nommés suivants :
// Ensemble prédéfini de couleurs pour implémenter les thèmes sombres et clairs thème :
ThemeData.dark() thème :
ThemeData.light(),
Vous pouvez obtenir une référence aux propriétés du thème en appelant Theme.of(context) n'importe où dans
l'arborescence des widgets. Il existe également la possibilité de remplacer uniquement un ensemble spécifique de
propriétés pour un thème donné afin de conserver les autres paramètres :
MaterialApp
( thème : ThemeData.dark().copyWith( primaryColor :
Colors.grey
)
)
Il s'agit d'un thème dark() avec la seule différence que PrimaryColor a été modifié en gris (tous les autres paramètres
sont toujours les mêmes). Vous pouvez également décider de remplacer les paramètres de thème pour certaines
parties de l'arborescence plutôt que d'appliquer un thème global comme nous l'avons fait dans l'exemple cidessus.
C'est possible grâce au widget Thème :
//main.dart
MaterialApp(
thème : ThemeData.dark();
)
Dans cet exemple, l'application entière a un thème dark() mais le widget MyFooter et tous ses enfants utiliseront à la place
le thème light(). En d’autres termes, Theme est utilisé pour remplacer le thème actuel par un nouveau pour l’ensemble du
sousarbre. En fait:
Notez que Theme.of() ne renvoie pas de valeur nullable. Dans le chapitre suivant, nous vous montrerons comment changer
facilement le thème de votre application du sombre au clair (et vice versa) à l'aide de HydratedBloc du package flutter_bloc.
11 | Gestion de l'État
Jusqu'à présent, vous savez quel est l'état dans une application Flutter car nous avons traité de manière exhaustive ce
sujet au chapitre 9.2.2 Widgets sans état et avec état. Ce que vous ne savez pas encore, c’est comment gérer correctement
les changements d’état.
Nous allons analyser en détail le fournisseur et flutter_bloc, mais il existe de nombreuses autres
bibliothèques de gestion d'état telles que Redux, MobX ou le modèle Scoped.
Vous pouvez trouver plus de détails dans la documentation officielle 1 .
Savoir gérer correctement l'état d'une application Flutter est fondamental : un code bien structuré est facile à lire et à
maintenir. De plus, vous créerez presque toujours des applications de production avec une interaction de l'utilisateur (ou
de sources externes) et l'état va donc changer plusieurs fois.
Dans ce chapitre, nous allons créer cette application simple :
Très facilement, il ne fait rien d'autre qu'incrémenter et décrémenter le compteur au milieu lorsque vous appuyez
respectivement sur +1 ou 1, qui sont FlatButton. Nous allons implémenter l'application en utilisant 3 stratégies de gestion
d'état différentes :
1https://flutter.dev/docs/development/dataandbackend/statemgmt/options
Vous finirez par avoir vu la même application construite de 3 manières différentes, une pour chaque stratégie, afin
que vous puissiez analyser leurs mécanismes et voir les différences dans leur fonctionnement.
C'est le moyen le plus simple de gérer l'état d'un widget, mais vous devriez vraiment éviter cette approche car elle
mélange la logique de l'interface utilisateur avec la logique métier. Vous verrez également très bientôt qu'une
utilisation correcte de setState(...) nécessite trop de code passepartout.
Veuillez noter que nous n'avons pas dit que vous devriez éviter d'utiliser StatefulWidgets mais que
vous devriez éviter l'utilisation de setState : vous verrez bientôt pourquoi. Comme vous le savez déjà,
un widget avec état est fondamental lorsqu'un widget sans état ne peut pas être créé en raison du
manque d'immuabilité de la classe ellemême.
Avant d'expliquer les raisons pour lesquelles l'utilisation directe de setState est mauvaise, voyons à nouveau comment cela fonctionne dans une
application de compteur traditionnelle.
// 1. la
classe DemoPage étend StatefulWidget { // 2. const
DemoPage();
// 3.
@passer outre
// 5.
void _increment()
{ setState(() => _counter++);
}
void _decrement() {
setState(() => _counter);
}
@passer outre
Il s'agit de la configuration typique d'un widget dont l'état est géré avec setState.
1. Vous devez suivre ce modèle : il est nécessaire d'avoir une classe qui étend StatefulWidget car elle va être
placée dans l'arborescence des widgets. L'autre classe est privée car elle représente et gère l'état du widget.
MatérielApp (
// n'oubliez pas le constructeur const ! domicile : const
DemoPage(),
);
2. Il y a la possibilité de définir un constructeur const pour DemoPage car il ne changera pas avec le temps. Ce
qui va changer, c'est l'état du widget, représenté par _DemoPageState (d'où la classe _DemoPageState ne
peut pas avoir de constructeur constant).
3. La création de l'état persistant du widget qui "survivra" aux reconstructions. Vous allez
travailler beaucoup avec cette classe car elle expose la méthode setState(...).
5. Deux fonctions qui incrémentent et décrémentent le compteur ; ils appellent tous les deux setState pour que
le widget et ses enfants sont reconstruits afin d'actualiser l'interface utilisateur.
La méthode de construction est très simple à comprendre car l'interface utilisateur est minimale, vous obtiendrez
immédiatement ce qui se passe. Notez l'utilisation de const devant Text, lorsque cela est possible, qui "met en
cache" Text et TextStyle.
Ligne
( mainAxisAlignment : MainAxisAlignment.spaceAround,
enfants: [
Bouton Plat (
enfant : const Text("+1", style :
TextStyle( couleur :
Colors.green, fontSize : 25
),
),
onPressed : _increment,
),
Text("$_counter", style :
),
),
Bouton Plat (
enfant : const Text("1", style :
TextStyle( couleur :
Colors.red, fontSize : 25
),
),
onPressed : _decrement,
),
],
)
Comme vous le savez déjà, lorsque setState est appelé, son rappel est exécuté puis le widget est
reconstruit. Puisque l'état persiste, l'incrément de la variable est "mémorisé" et ainsi le widget
Texte affichera la nouvelle valeur mise à jour.
11.1.1 Considérations
Tout d'abord, il n'y a ABSOLUMENT AUCUNE raison de dire que cette approche entraîne des problèmes de performances
car nous avons utilisé un StatefulWidget au lieu d'un StatelessWidget. Le problème est que setState doit être utilisé avec
InheritedWidget, sinon il y aura des reconstructions incontrôlées.
Dans le cas d'un widget sans enfant comme A, lorsque setState est appelé, une reconstruction se produit uniquement
pour A. Dans l'image cidessus, la boîte noire représente un widget reconstruit. Les problèmes de performances
commencent à devenir réels lorsque le widget en cours de reconstruction a un ou plusieurs enfants. Regardez cet exemple :
@passer outre
Lorsque vous appuyez sur le bouton, setState déclenchera toujours une reconstruction pour le widget
actuel et tous ses enfants. Dans l'exemple cidessus, WidgetB et WidgetC sont reconstruits même si
seul WidgetB le devrait, car c'est le seul qui a une dépendance de WidgetA.
Comme vous pouvez le voir sur l'image, les enfants de A sont toujours reconstruits même s'ils n'ont pas de variables en
commun ou tout autre type de dépendance. Les appels à setState() reconstruisent l’intégralité du sousarbre, même si ce
n’est pas vraiment nécessaire. Ce serait mieux si Flutter reconstruisait uniquement les widgets qui ont vraiment besoin
d'être mis à jour, comme ceci :
Si vous utilisiez setState en combinaison avec une classe particulière, appelée InheritedWidget, vous seriez en mesure
d'effectuer des reconstructions optimisées qui ne gaspillent pas de ressources. Lorsqu'ils sont utilisés ensemble, vous
avez la possibilité de reconstruire uniquement les widgets qui ont vraiment besoin d'être mis à jour, en laissant les autres intacts.
Le gros problème est l'utilisation d'un widget sans état combiné à un InheritedWidget :
cela produit beaucoup de passepartout qui sont très difficiles à comprendre et à maintenir. Vous ne voulez
JAMAIS vous en occuper car ce n’est pas nécessaire ; il existe de nombreuses bibliothèques (telles que
supplier ) qui font tout ce travail fastidieux pour vous !
Si vous êtes curieux de comprendre les détails d'InheritedWidget, comme toujours, nous vous recommandons de visiter la
2
documentation officielle qui contient également une vidéo à ce sujet. Nous n'en parlons pas dans le livre car cela ne sert à
rien puisque de nos jours, le fournisseur est le choix par défaut, qui n'est en fait qu'un sucre syntaxique pour InheritedWidget.
1. Les widgets sans état et avec état sont à la fois efficaces et bons, vous n'avez pas besoin de penser que l'un est
moins performant que l'autre. Si vous souhaitez gérer l'état à l'aide d'un StatefulWidget, vous devez vraiment utiliser
setState et InheritedWidget ensemble.
2. Transmettre des données dans l'arborescence et contrôler les reconstructions avec InheritedWidget est compliqué
et produit beaucoup de code passepartout ; ne le fais pas. Préférez l'utilisation d'une bibliothèque telle qu'un
fournisseur qui fait tout pour vous avec moins de code (et c'est aussi beaucoup plus lisible).
3. Avec setState, vous mélangez la logique de l'interface utilisateur et la logique métier. Par exemple, _DemoPageState
est comme un énorme marteau tombant du ciel et détruisant totalement le verre du principe de responsabilité
unique. Cela fait trop de choses :
• il gère la logique de l'interface utilisateur, qui est responsable du dessin des widgets ;
Les applications prêtes pour la production contiennent des milliers de lignes de code écrites dans des centaines de
fichiers. La situation est peutêtre déjà assez compliquée et il est certain qu’ajouter encore plus de complexité en
cassant le SRP n’est pas pratique du tout.
4. L'utilisation de setState est trop « basique » car elle indique simplement à Flutter de reconstruire le widget et tous ses
enfants ; pour un contrôle plus subtil, il serait nécessaire d'utiliser également InheritedWidget.
Hériter de State<T> donne accès à la méthode initState ; il n'est appelé qu'une seule fois au moment de la création de
l'État. Puisque l'état persiste jusqu'à ce que le widget soit supprimé, vous avez la garantie que void initState() ne s'exécutera
qu'une seule fois pendant le cycle de vie de la classe.
@passer outre
void initState() {
2https://api.flutter.dev/flutter/widgets/InheritedWidgetclass.html
super.initState(); // mets le
code ici...
}
@passer outre
Vous devez remplacer initState lorsque le widget doit être configuré avant d'être construit ou s'il est nécessaire d'appeler des
méthodes qui ne doivent être exécutées qu'une seule fois lors de la création de l'état.
En fait, initState() peut être vu comme s'il s'agissait du constructeur d'un widget et dispose() le destructeur.
@passer outre
@passer outre
Il n'est exécuté qu'une seule fois lorsque l'état est détruit et il doit être utilisé lorsqu'il est nécessaire de nettoyer les ressources
utilisées par le widget. Si vous aviez besoin de déclarer une variable qui ne peut pas être immédiatement initialisée et que
vous ne voulez pas qu'elle soit nullable, vous l'utiliseriez tardivement.
@override void
initState() { super.initState();
valeur = "Initezmoi" ;
Si cette valeur ne doit être attribuée qu'une seule fois dans initState, envisagez de la rendre finale tardive.
Avec l'arrivée de NNBD, les variables pourraient également être directement initialisées de cette manière :
Pas besoin du tout d'utiliser initState() mais c'est simplement parce que vous faites une mission : vous ne pouvez
pas le faire lorsqu'il s'agit d'appeler des fonctions, par exemple.
• Effectuez l'initialisation du widget dans la méthode initState afin d'être assuré que la phase ne sera exécutée
qu'une seule fois. En cas d'affectations, envisagez d'utiliser des variables finales tardives et d'initialisation
directement pour réduire la quantité de code passepartout.
• La documentation officielle 3
dit que setState ne devrait mettre à jour que les valeurs, comme attribuer
de nouvelles valeurs aux variables, il ne devrait rien calculer. Par exemple, vous devriez faire ceci...
attendre writeToFile(_counter);
}
... plutôt que d'appeler la fonction dans le programme de mise à jour d'état :
});
}
De plus, si la fonction de rappel est une instance de Future, vous obtiendrez une exception d'exécution. Quoi qu'il en
soit, c'est mauvais de toute façon car l'écriture de données dans un fichier ne devrait pas se produire dans une classe
qui s'occupe de l'interface utilisateur !
3https://api.flutter.dev/flutter/widgets/State/setState.html
• N'appelez pas initState après disposer car cela conduit à un comportement indéfini. StatelessWidget possède
la propriété bool get Mounted qui vous indique si le widget est créé ou supprimé.
Utilisez des widgets avec état sans soucis mais évitez d'utiliser setState pour la gestion de l'état ; préférez utiliser
une bibliothèque comme supplier ou flutter_bloc. Évitez l'utilisation "brute" de setState, car elle reconstruit l'intégralité
du sousarbre, ainsi que la combinaison setState() + InheritedWidget, car elle produit une énorme quantité de code
passepartout.
dépendances :
fournisseur : ^4.3.2
Le SDK Flutter comprend une classe simple appelée ChangeNotifier qui fournit une notification de modification à
ses auditeurs. Concrètement, si vous utilisez cette classe comme mixin, vous avez la possibilité d'envoyer une
"alerte" indiquant que quelque chose a changé aux widgets abonnés.
Le fournisseur utilise ChangeNotifier de Flutter pour créer une classe qui encapsule l'état et,
lorsque quelque chose change, les widgets intéressés sont avertis et reconstruits.
Comme son nom l'indique, il s'agit d'un notificateur qui alerte les auditeurs des changements.
Nous allons toujours créer la même application qui augmente et diminue le compteur mais à la manière "du
fournisseur". Tout d'abord, il y a la nécessité de créer une classe qui s'occupe de la logique métier et fait persister
l'état ; nous créons un fichier nommé counter_model.dart avec ceci
contenu:
incrément vide() {
4https://pub.dev/packages/provider