Vous êtes sur la page 1sur 150

Machine Translated by Google

Chapitre 6. Génériques et collections

6.3 Bonnes pratiques


Vous avez vu que le langage propose trois catégories importantes de conteneurs et qu'en général vous devez les utiliser
avec leurs implémentations par défaut. Une carte "par défaut", par exemple, est généralement final myMap = <int, int>{};
mais si vous avez besoin de garder les clés triées, optez pour un SplayTreeMap.
D'après la documentation officielle 5:

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

dernier exemple = List<int>(); // Mauvais exemple final


= <int>[]; // Bien

• 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). Reportez­vous à la documentation pour une liste complète des
méthodes utilitaires.

6.3.1 opérateur== et hashCode


Vous savez déjà depuis la version 5.3 "La classe Object" comment remplacer correctement l'opérateur d'égalité ET la
propriété hashCode. Les ensembles et les cartes utilisent beaucoup de comparaisons et du code de hachage d'un objet
donné, vous voulez donc vraiment effectuer un bon remplacement.

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/effective­dart/usage#collections
6https://api.dart.dev/stable/2.7.0/dart­core/dart­core­library.html

Référence complète Flutter 150


Machine Translated by Google

Chapitre 6. Génériques et collections

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'est­ce 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);

opérateur booléen == (Objet autre) {...}

int obtenir hashCode


{ const prime = 31 ; var
résultat = 1 ;

résultat = premier * résultat + a.hashCode ; résultat =


premier * résultat + b.hashCode ; return prime *
résultat + c.hashCode ;
}
}

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.

class Test extends Equatable { final int a;


final int b ;

7https://pub.dev/packages/equatable

Référence complète Flutter 151


Machine Translated by Google

Chapitre 6. Génériques et collections

Chaîne finale c ;
Test(ce.a, ceci.b, ceci.c);

@passer outre

List<Object> get props => [a, b, c];


}

Il vous suffit de sous­classer 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

List<Object> get props => [a, b, c];


}

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/effective­dart/design#avoid­defining­custom­equality­for­mutable­classes

Référence complète Flutter 152


Machine Translated by Google

Chapitre 6. Génériques et collections

6.3.2 Méthodes de transformation

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

// Renvoie une nouvelle liste de nombres pairs final


List<String> other = list .where((int value) =>
value % 2 == 0) // 1. .map((int value) => value.toString( )) //
2. .toList(); // 3.

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.

1. La méthode Where() parcourt toute la collection et renvoie une expression booléenne.


Ici, nous analysons chaque élément de la liste, représenté par une valeur int , et nous le supprimons au cas où
ce ne serait pas pair. Cette méthode est un « filtre » qui ajoute des valeurs uniquement si l'expression booléenne
renvoie vrai.

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 lui­mê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.

Référence complète Flutter 153


Machine Translated by Google

Chapitre 6. Génériques et collections

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 ci­dessus
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.

– map() : transforme l'élément d'un type en un autre.

– skip() : ignore les n premiers éléments de la collection source.

– followBy() : concatène ce conteneur avec un autre, passé en paramètre.

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

– each() : renvoie un booléen indiquant si chaque élément de la collection satisfait aux


état donné.

– 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:

liste finale = <int>[1, 2, 3, 4, 5] ; somme finale =


list.reduce((int a, int b) => a + b);

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

Référence complète Flutter 154


Machine Translated by Google

Chapitre 6. Génériques et collections

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

liste finale = <int>[1, 2, 3, 4, 5] ; somme finale


= list.fold(0, (int a, int b) => a + b);

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 :

liste finale = <int>[1, 2, 3, 4, 5] ;

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.

liste finale = ['bonjour', 'Dart', '!'];

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 :

liste finale = ['bonjour', 'Dart', '!'];

// 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 :

final withReduce = list.reduce(someCallback);

Référence complète Flutter 155


Machine Translated by Google

Chapitre 6. Génériques et collections

final withFold = list.skip(1).fold(list.first, someCallback);

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.

Référence complète Flutter 156


Machine Translated by Google

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 :

• opérations de base de données ;

• 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ère­plan.

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, entre­temps, 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.

int processData (int param1, double, param2) { var valeur = 0 ;

Référence complète Flutter 157


Machine Translated by Google

Chapitre 7. Programmation asynchrone

for(var i = 0; i < param1; ++i) { for (var j = 0; j <


param1*param2; j++) { // beaucoup de travail ici...

}
}

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

print("Bienvenue à... Dart!");


}

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 elle­même semble gelée.

7.2 Contrats à terme

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 :

Future<int> processData(int param1, double, param2) {


valeur var = 0 ;

for(var i = 0; i < param1; ++i) { for (var j = 0; j <


param1*param2; j++) { // beaucoup de travail ici...

}
}

Référence complète Flutter 158


Machine Translated by Google

Chapitre 7. Programmation asynchrone

res finale = httpGetRequest(valeur); return


Future<int>.value(res);
}

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.

Puisque la fonction renvoie désormais un Future<T>, nous devons la traiter différemment :

// Les types sont explicites par souci de simplicité void main()


{ Future<int> val
= processData(1, 2.5); val.then((result) => print(result));

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ère­plan et être informé de l'achèvement via then().

// Les types sont explicites par souci de simplicité void main()


{ Future<int> val
= processData(1, 2.5); val.then((résultat) =>
print(résultat))
.catchError((e) => print(e.message));
}

Les méthodes peuvent être enchaînées ; la capture des exceptions potentielles levées lors de l'exécution en
arrière­plan 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 :

val.then((résultat) => une autreFonction1(résultat))


.then((une autre) => une autreFonction2(une autre)) .then((fin)
=> une autreFonction3(fin))

Référence complète Flutter 159


Machine Translated by Google

Chapitre 7. Programmation asynchrone

.catchError((e) => print(e.message));

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<int> one = exampleOne();


Future<int> deux = exempleTwo();
Future<int> trois = exempleThree();

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

final future = Future<int>.delayed(const Duration(seconds: 1), ()=> 1);

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

final future = Future<double>.error("Échec");

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.

Référence complète Flutter 160


Machine Translated by Google

Chapitre 7. Programmation asynchrone

• Future<T>.value()

final future = Future<String>.value("Flutter Complete Reference");

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

final future = Future<void>.sync(() => print("Appelé immédiatement"));

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.

1. Version non future (code synchrone).

int processData(int param1, double, param2) { // prend 4 ou 5


secondes pour s'exécuter...
}

void main()
{ données finales = processData(31, 2.5); print("
résultat fonctionnel = $données");

print("L'avenir est radieux");


}

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

Référence complète Flutter 161


Machine Translated by Google

Chapitre 7. Programmation asynchrone

processData() bloque le flux d'exécution lors de l'exécution des calculs.

2. Future version (code asynchrone).

Future<int> processData(int param1, double param2) {


// fonction qui prend 4 ou 5 secondes à s'exécuter...
}

void main()
{ processus final = processData(1, 2.5);
process.then((data) => print("result = $data"));

print("L'avenir est radieux");


}

Le résultat est maintenant différent :

L'avenir est brillant


résultat = 10 ; // <­­ imprimé après 4 ou 5 secondes

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

processus final = processData(1, 2.5); process.then((data)


{ print("result = $data");
print("L'avenir est radieux");

});

... alors la console aurait imprimé ...

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.

Référence complète Flutter 162


Machine Translated by Google

Chapitre 7. Programmation asynchrone

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évenez­moi 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.

7.2.2 asynchrone et attente


L'utilisation de async et wait rend le code moins verbeux et donc plus facile à comprendre.
C'est juste du sucre syntaxique pour éviter l'utilisation de then() pour écrire des rappels :

• Utilisation alors.

void main()
{ processus final = processData(1, 2.5);
process.then((data) => print("result = $data"));
}

• Utilisation d'async et d'attente.

void main() async { données


finales = wait processData(1, 2.5); print("résultat =
$données")
}

Les extraits ci­dessus 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 mot­clé async avant le corps.

3. Vous êtes autorisé à appeler wait uniquement sur un Future<T>.

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ère­plan et se poursuit une fois le calcul terminé (ce qui est exactement ce que fait then()).
Pour être clair, écrire...

processData(1, 2.5).then((data) => print("result = $data"));

Référence complète Flutter 163


Machine Translated by Google

Chapitre 7. Programmation asynchrone

... est le même que ...

données finales = attendre processData (1, 2.5);


print("résultat = $données");

... car les lignes après le mot­clé 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() async { données


finales = wait processData(1, 2.5); print("résultat =
$données"); print("L'avenir est
radieux");
}

... est équivalent à ...

void main()
{ processus final = processData(1, 2.5);
process.then((data) { print("result
= $data"); print("L'avenir est
radieux");
});
}

...mais absolument PAS équivalent à...

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

Référence complète Flutter 164


Machine Translated by Google

Chapitre 7. Programmation asynchrone

... est simplifié avec l'utilisation de async et wait :

void main() async { try


{ résultat
final = wait processData(1, 2.5); imprimer(résultat); } sur
Exception catch (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.

7.2.3 Bonnes pratiques


1
La première chose indiquée par les futurs officiels". les directives d'utilisation sont "préférer async/wait plutôt que d'utiliser raw.
Étant donné que le code asynchrone peut être difficile à lire et à déboguer, vous devriez préférer async et wait à une chaîne de
then() et catchError().

Future<String> example() async { try { final String


data =
wait httpGetRequest(); chaîne finale autre = attendre
anotherRequest(data); retourner un autre ; } sur Quelque chose catch (e)
{ print(e.message);
renvoyer « échec » ;

}
}

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 :

Future<String> exemple() { return


httpGetRequest().then((data) {

1https://dart.dev/guides/langue/effective­dart/usage#asynchrony

Référence complète Flutter 165


Machine Translated by Google

Chapitre 7. Programmation asynchrone

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 passe­partout, 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.

// Utilisez le constructeur nommé Future<int>


example() => Future<int>.value(3);

// Utilisez async et le compilateur encapsule la valeur dans un Future


Future<int> exemple() async => 3;

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.

Référence complète Flutter 166


Machine Translated by Google

Chapitre 7. Programmation asynchrone

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.

• Générateur. Crée de nouvelles données et les envoie via le flux.

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

Référence complète Flutter 167


Machine Translated by Google

Chapitre 7. Programmation asynchrone

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.

7.3.1 Flux et générateurs


A titre d'exemple basique, nous allons créer un générateur asynchrone produisant 100 nombres aléatoires, un
par seconde. Afin d'indiquer au compilateur que cette fonction est un générateur, elle doit être marquée avec le
modificateur async* .

Stream<int> randomNumbers() async* { // 1.


final aléatoire = Random();

for(var i = 0; i < 100; ++i) { // 2. wait Future.delayed(Duration(seconds:


1)); // 3. rendement random.nextInt(50) + 1; // 4.

}
} // 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.

2. La boucle génère 100 nombres aléatoires.

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 mot­clé 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.

// contient la fonction 'sleep' import 'dart:io';

2Voir l'annexe A.3 pour en savoir plus sur la Durée

Référence complète Flutter 168


Machine Translated by Google

Chapitre 7. Programmation asynchrone

Itérable<int> randomNumbers() sync* {


final aléatoire = Random();

for(var i = 0; i < 100; ++i)


{ sleep(Duration(seconds:1)); donne
random.nextInt(50) + 1 ;
}
}

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 :

Stream<int> randomNumbers() async* {


final aléatoire = Random();

for(var i = 0; i < 100; ++i) { wait


Future.delayed(Duration(seconds: 1)); donne
random.nextInt(50) + 1 ; donne
random.nextInt(50) + 1 ; donne
random.nextInt(50) + 1 ;
}
}

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 » :

Référence complète Flutter 169


Machine Translated by Google

Chapitre 7. Programmation asynchrone

• Stream<T>.périodique()

final aléatoire = Random();

flux final = Stream<int>.periodic( const


Duration(seconds : 2), (count) =>
random.nextInt(10)

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

flux final = Stream<String>.value("Bonjour");

Crée un nouveau flux qui émet un seul événement avant de terminer.

• Stream<T>.erreur()

Future<void> quelque chose (Stream<int> source) async {

essayez { wait for ( événement final dans la source) { ... }


} sur SomeException catch (e) {
print("Une erreur s'est produite : $e");
}
}

// Passe l'objet d'erreur quelque


chose(Stream<int>.error("Whoops"));

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

flux final = Stream<double>.fromIterable(const <double>[


1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9
]);

Crée un nouveau flux à abonnement unique qui émet uniquement les valeurs de la liste.

• Stream<T>.fromFuture()

Référence complète Flutter 170


Machine Translated by Google

Chapitre 7. Programmation asynchrone

flux final = Stream<double>.fromFuture( Future<double>.value(15.10)

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

flux final = Stream<double>.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 ;

• skip(int count) : ignore le premier événement count sur le flux ;

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.

Stream<int> randomNumbers() async* { // voir le


code ci­dessus...
}

void main() async { flux // 1. //


final = randomNumbers(); 2.

wait for ( valeur var dans le flux) { // 3.


imprimer(valeur);
}

print("Flux asynchrone!"); // 4.

Référence complète Flutter 171


Machine Translated by Google

Chapitre 7. Programmation asynchrone

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 à celui­ci. 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 .

4. La chaîne est imprimée à la fin de la boucle afin qu'elle apparaisse à la fin.

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 :

Iterable<int> randomNumbers() sync* { // voir le


code ci­dessus...
}

void main() { flux


final = randomNumbers();

pour ( valeur var dans le flux) {


imprimer(valeur);
}

print("Synchroniser le flux !");


}

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 :

void main() { flux


final = randomNumbers();

essayez { wait for ( valeur var dans le flux) {


imprimer(valeur);

Référence complète Flutter 172


Machine Translated by Google

Chapitre 7. Programmation asynchrone

}
} 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é :

Stream<int> counterStream([int maxCount = 10000]) async* { final delay = const


Duration(secondes : 1); nombre de variables = 0 ;

while (true) { if
(count == maxCount) {
casser;
}
wait Future.delayed(délai); rendement +
+compte ;
}
}

void main() async { wait


for(var c in counterStream) { print(c);

}
}

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 :

1. Utilisation du flux : https://dart.dev/tutorials/langage/streams

2. Générateurs : https://dart.dev/articles/libraries/creating­streams

3. Documents en streaming : https://api.dart.dev/stable/2.9.2/dart­async/Stream­class.html

Dans Flutter, l' objet StreamBuilder<T> est utilisé pour s'abonner à un flux et nous allons utiliser

Référence complète Flutter 173


Machine Translated by Google

Chapitre 7. Programmation asynchrone

cela assez souvent, surtout dans la troisième partie du livre.

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

Renvoie un Stream<T> Renvoie un Itérable<T>

Marquer la fonction avec async* Fonction Mark avec synchronisation*

Peut utiliser attendre Impossible d'utiliser l'attente

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, devrais­je 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
Flut­ter, à 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

Référence complète Flutter 174


Machine Translated by Google

Chapitre 7. Programmation asynchrone

utile pour diviser la logique d'un Stream<T> en plusieurs morceaux. C'est le cas où le rendement* est requis :

Stream<int> numberGenerator(bool pair) async* { if (pair) {

rendement
0 ; rendement* evenNumbersUpToTen();
rendement
0 ; } else
{ rendement
­1 ; rendement* oddNumbersUpToTen();
rendement ­1 ;
}
}

Stream<int> evenNumbersUpToTen() async* { ... }


Stream<int> oddNumbersUpToTen() async* { ... }

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

• Une fois que evenNumbersUpToTen est terminé, numberGenerator reprend et exécute le


prochaine déclaration de rendement .

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.

7.3.4 Utilisation d'un contrôleur


Les exemples que nous avons montrés jusqu’à présent ne sont en réalité pas très utiles. En particulier, la création d'un flux
"sur place" n'a pas beaucoup de cas d'utilisation en dehors des exemples et des démos. Vous connaissez déjà cette
configuration simple :

Référence complète Flutter 175


Machine Translated by Google

Chapitre 7. Programmation asynchrone

Stream<String> someStream() async* { ... }

void main() async { final


stream = someStream();
}

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 Flut­ter, 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 {

/// Le nombre aléatoire maximum à générer final int maxValue;


statique final _random =
Random();

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

}

/// Une référence au flux de nombres aléatoires Stream<int>


get stream => _controller.stream;

// d'autres méthodes à venir...


}

Référence complète Flutter 176


Machine Translated by Google

Chapitre 7. Programmation asynchrone

3. La classe Timer arrive


Remarquez comment nous avons utilisé des triples barres obliques (/// ) pour
documenter le code du package dart:async : il s'agit d'un compte à rebours qui peut être configuré pour se
déclencher une ou plusieurs fois. Il compte à rebours à partir de la durée donnée jusqu'à 0, puis déclenche le rappel. Il a
deux constructeurs :

• Timer (Durée durée, void callback ())


Exécute une fois le rappel après la durée donnée.

• Timer.periodic (Durée, rappel annulé (Timer timer))


Le rappel est invoqué à plusieurs reprises avec des intervalles de durée.

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

• onResume : ce callback est appelé à la reprise du flux (reprise de l'abonnement).

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

void _runStream (minuterie) { _currentCount+



_controller.add(_random.nextInt(maxValue));

3En savoir plus sur la documentation du code dans 23.1.2

Référence complète Flutter 177


Machine Translated by Google

Chapitre 7. Programmation asynchrone

if (_currentCount == maxValue) { _stopTimer();

}
}

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() :

// Nouvelle valeur ajoutée au flux. Les auditeurs seront informés


_controller.add(_random.nextInt(maxValue));

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

void main() async { final


stream = RandomStream().stream; wait
Future.delayed(const Duration(seconds: 2));

// Le minuteur à l'intérieur de notre 'RandomStream' est démarré final


souscription = stream.listen((int random) {

Référence complète Flutter 178


Machine Translated by Google

Chapitre 7. Programmation asynchrone

imprimer (aléatoire);
});

wait Future.delayed(const Duration(millisecondes: 3200)); abonnement.cancel();

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ère­plan ;

• il n'existe pas d'équivalent, par exemple, aux types thread­safe 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 :

Référence complète Flutter 179


Machine Translated by Google

Chapitre 7. Programmation asynchrone

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ère­plan" 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 :

Référence complète Flutter 180


Machine Translated by Google

Chapitre 7. Programmation asynchrone

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 est­il 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>).

// ceci s'appelle d'abord var json


= myModel.readFromDisk();

// et ceci est appelé après le résultat final ci­dessus


= calculateIntegerValue();

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 :

Référence complète Flutter 181


Machine Translated by Google

Chapitre 7. Programmation asynchrone

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.

void printName() async { final int id


= generateId(); nom de chaîne final =
attendre HttpModel.getRequest(id);

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 :

Référence complète Flutter 182


Machine Translated by Google

Chapitre 7. Programmation asynchrone

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ère­plan. 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

Référence complète Flutter 183


Machine Translated by Google

Chapitre 7. Programmation asynchrone

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.

7.4.1 Isolats multiples et Flutter


Une seule application Dart peut contenir plusieurs isolats ; vous pouvez les créer en utilisant Isolate.spawn() de la
bibliothèque "dart:isolate" . Les isolats ont leur propre boucle d'événements et leur propre zone de mémoire, il n'y a
aucune dépendance ni composant partagé. La seule façon dont ils disposent pour communiquer est via des messages.

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

Référence complète Flutter 184


Machine Translated by Google

Chapitre 7. Programmation asynchrone

60 images par seconde. C'est le cas d'un nouvel isolat :

// Tâche très lourde en calculs int


sumOfPrimes(int limit) {...}

// Fonction à appeler dans Flutter


Future<int> heavyCalculations() {
return calculate<int, int>(sumOfPrimes, 50000);
}

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, enveloppez­les
simplement dans une classe de modèle et transmettez­la 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);
}

// Utiliser le modèle comme paramètre int


sumOfPrimes (données PrimeParams) {
limite finale = data.limit; final un
autre = data.un autre ;
...
}

// Fonction à appeler dans Flutter Future<int>


heavyCalculations() { final params =
PrimeParams(50000, 10.5); return calculate<PrimeParams,
int>(sumOfPrimes, params);
}

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.

Référence complète Flutter 185


Machine Translated by Google

8 | Principes de codage avec Dart

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/flutter­clean­architecture­tdd

• 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/flutter­firebase­ddd­course

• 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/effective­dart

– https://dart.dev/tutorials

Référence complète Flutter 186


Machine Translated by Google

Chapitre 8. Principes de codage avec Dart

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.

8.1 Principes SOLIDES


Le terme SOLID devrait en fait s'écrire SOLID car c'est un acronyme pour 5 principes de conception, un pour chaque lettre,
qui aident le programmeur à écrire du code maintenable et flexible.

8.1.1 Principe de responsabilité unique


De manière très intuitive, ce principe (abrégé en SRP) stipule qu'une classe ne doit avoir qu'une seule responsabilité afin
qu'elle puisse changer pour une seule raison et pas plus. En d’autres termes, vous devez créer des classes traitant d’une
seule tâche afin qu’elles soient plus faciles à maintenir et plus difficiles à rompre.

Formes de classe {
Liste<String> cache = Liste<>();

// Calculs double
SquareArea(double l) { /* ... */ } double circleArea(double r) { /* ...
*/ } double triangleArea(double b, double h) { /* ... */ }

// Peindre à l'écran void


paintSquare(Canvas c) { /* ... * / } void paintCircle(Canvas c) { /
* ... */ } void paintTriangle(Canvas c) { /* ... */ }

// GET requêtes String


wikiArticle (String figure) { /* ... */ } void _cacheElements (String text) { /
* ... */ }
}

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?

Référence complète Flutter 187


Machine Translated by Google

Chapitre 8. Principes de codage avec Dart

// Calculs et classe abstraite logique


Shape { double zone ();

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

8.1.2 Principe ouvert fermé


Le principe ouvert et fermé stipule que dans une bonne architecture, vous devriez pouvoir ajouter de nouveaux
comportements sans modifier le code source existant. Ce concept est notoirement décrit par la phrase « les entités
logicielles doivent être ouvertes aux extensions mais fermées aux modifications ». Regardez cet exemple :

class Rectangle { double


largeur finale ; double
hauteur finale ;
Rectangle(this.width, this.height);
}

classe Cercle
{ double rayon final ;
Rectangle(ce.radius);

double obtenir PI => 3,1415 ;


}

Référence complète Flutter 188


Machine Translated by Google

Chapitre 8. Principes de codage avec Dart

class AreaCalculator { double


calculate ( forme de l'objet) { if (la forme est
rectangle) { // Renvoie intelligent
r.width * r.height; }
else { final c = forme en cercle ;
retourner
c.radius * c.radius * c.PI;

}
}
}

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 Rectangle {...} classe


Cercle {...} classe Triangle
{...} classe Losange {...}
classe Trapèze {...}

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

Référence complète Flutter 189


Machine Translated by Google

Chapitre 8. Principes de codage avec Dart

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 !

// Utilisez­le comme classe abstraite


d'interface Area {
double computingArea();
}

// Chaque classe calcule l'aire par elle­même. classe Rectangle


implémente Area {} classe Circle implémente
Area {} classe Triangle implémente Area {}
classe Rhombus implémente Area {} classe
Trapezoid implémente Area {}

class AreaCalculator { double


calculate (forme de la zone) { return
shape.computeArea ();
}
}

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

L’essentiel de ce principe est le suivant : dépendez d’abstractions et non d’implémentations.


Grâce aux classes abstraites vous travaillez avec des abstractions et non avec des implémentations
concrètes : votre code ne s'appuie pas sur des entités "prédéfinies".

8.1.3 Principe de substitution de Liskov


Le principe de substitution de Liskov stipule que les sous­classes doivent être remplaçables par des superclasses
sans altérer l'exactitude logique du programme. En termes pratiques, cela signifie qu'un sous­type doit garantir
les "conditions d'utilisation" de son supertype ainsi que quelque chose de plus qu'il souhaite ajouter. Regardez
cet exemple :

Référence complète Flutter 190


Machine Translated by Google

Chapitre 8. Principes de codage avec Dart

classe Rectangle { double


largeur ; double
hauteur;
Rectangle(this.width, this.height);
}

la classe Carré étend le Rectangle {


Carré (double longueur) : super (longueur, longueur) ;
}

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 sous­classe 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.

8.1.4 Principe de séparation des interfaces


Ce principe stipule qu'il n'est pas nécessaire de forcer un client à mettre en œuvre un comportement dont il n'a pas besoin. Il
en ressort que vous devez créer de petites interfaces avec un minimum de méthodes.
En général, il est préférable d'avoir 8 interfaces avec 1 méthode plutôt que 1 interface avec 8 méthodes.

Référence complète Flutter 191


Machine Translated by Google

Chapitre 8. Principes de codage avec Dart

// Classe abstraite
d' interface Worker {
travail nul ();
annuler le sommeil ();
}

class Human implémente Worker { void


work() => print("Je fais beaucoup de travail"); void sleep()
=> print("J'ai besoin de 10 heures par nuit...");
}

class Robot implémente Worker { void


work() => print("Je travaille toujours"); void sleep()
{} // ??
}

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

classe Humain implémente Worker, Sleeper {


void work() => print("Je fais beaucoup de travail"); void
sleep() => print("J'ai besoin de 10 heures par nuit...");
}

class Robot implémente Worker { void


work() => print("Je travaille toujours");
}

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.

Référence complète Flutter 192


Machine Translated by Google

Chapitre 8. Principes de codage avec Dart

8.1.5 Principe d'inversion de dépendance

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.

// Utilisez­le comme classe


abstraite d'interface EncryptionAlgorithm {
Chiffrement de chaîne (); // <­­ abstraction
}

la classe AlgoAES implémente EncryptionAlgorithm {} la classe


AlgoRSA implémente EncryptionAlgorithm {} la classe AlgoSHA
implémente EncryptionAlgorithm {}

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 :

class FileManager { void


secureFile (EncryptionAlgorithm algo) { algo.encrypt();

}
}

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:

final fm = Gestionnaire de fichiers(...);

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 sous­type de EncryptionAlgorithm. Dans cet exemple, nous respectons ensemble les 5 principes
SOLID.

Référence complète Flutter 193


Machine Translated by Google

Chapitre 8. Principes de codage avec Dart

8.2 Injection de dépendances

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 PaymentValidator { date


finale ; Numéro de
carte de chaîne final ; const
PaymentValidator(this.date, this.cardNumber);

// Utilise le circuit de paiement MasterCard void


validatePayment(int montant) { ... }
}

classe PaymentProcessor {
_validateur final tardif ;
Processeur de paiement (chaîne numéro de carte) {
_validator = PaymentValidator(DateTime.now(), cardNumber);
}

Date get expiryDate => _validator.date ; annuler le


paiement ( montant int) =>
_validator.validatePayment(montant);
}

Vérificateur de classe abstraite {


PaiementValidator mastercardCheck();
}
la classe CheckerOne étend Checker { /*... code ... */ } la classe CheckerTwo
étend Checker { /*... code ... */ }

Checker et PaymentProcessor ont tous deux une forte dépendance à PaymentValidator car il est essentiel à la
compilation. Bien entendu, les sous­classes héritent également de la dépendance.

Référence complète Flutter 194


Machine Translated by Google

Chapitre 8. Principes de codage avec Dart

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 e­mail, 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 PaymentValidator { chaîne


finale _email ; const
PaymentValidator(this._email);

void validatePayment(int montant) { ... }


}

class PaymentProcessor
{ PaymentValidator final tardif _validator ;

PaymentProcessor (String email) : _validator


= PaymentValidator (email);

annuler le paiement ( montant int) =>


_validator.validatePayment(montant);
}

Référence complète Flutter 195


Machine Translated by Google

Chapitre 8. Principes de codage avec Dart

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 ci­dessus cassent une autre partie du code : la classe abstraite Checker dépend
également de PaymentValidator, il est donc nécessaire de corriger le code.

Vérificateur de classe abstraite {


// auparavant, il s'appelait 'mastercardCheck()'
PaymentValidator payPalCheck();
}

Ce changement a des conséquences sur toute sous­classe 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 sous­classe 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.

8.2.1 Injection du constructeur


En utilisant l'injection de dépendances et les abstractions plutôt que les implémentations, les problèmes ci­dessus
disparaissent. Les dépendances transmises de l’extérieur créent un couplage faible qui est plus sûr qu’un
couplage fort car il repose sur des abstractions.

classe abstraite PaymentValidator { const


PaymentValidator(); void
validatePayment(int montant);
}

la classe MasterCard implémente PaymentValidator {


// Définir la date, le numéro de carte et le constructeur const
MasterCard(); void
validatePayment(int montant) {...}
}

classe PayPal implémente PaymentValidator {


// Définir un email et le constructeur

Référence complète Flutter 196


Machine Translated by Google

Chapitre 8. Principes de codage avec Dart

constPayPal (); void


validatePayment(int montant) {...}
}

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.

class PaymentProcessor { final


PaymentValidator _validator ; const
PaymentProcessor(this._validator);

annuler le paiement ( montant int) =>


_validator.validatePayment(montant);
}

// Et puis on peut utiliser librement PayPal ou MasterCard void main() {

final p1 = const PaymentProcessor(MasterCard()); final p2 = const


PaymentProcessor(PayPal());
}

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 sous­type de PaymentValidator.

la classe Visa implémente PaymentValidator { const


Visa(); void
validatePayment(int montant) {...}
}

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

Référence complète Flutter 197


Machine Translated by Google

Chapitre 8. Principes de codage avec Dart

pourrait créer une classe "simulée" pour les tests unitaires, tout comme un type de validateur classique :

la classe TestValidator implémente PaymentValidator {


const TestValidator(); void
validatePayment(int montant) {...}
}

La dernière chose que nous devons refactoriser est la classe Checker car elle doit renvoyer une abstraction plutôt
qu'une implémentation.

Vérificateur de classe abstraite {


PaymentValidator paymentCheck();
}

la classe CheckerOne étend Checker {...} la classe


CheckerTwo étend Checker {...}

Puisque PaymentValidator est abstrait, toute classe de la hiérarchie hérite d’une dépendance faible qui est sûre.

8.2.2 Méthode d'injection


L'injection de constructeur est utilisée lorsque votre classe a vraiment besoin d'une dépendance externe pour fonctionner. Lorsque
vous disposez d’une dépendance « facultative » qui n’est pas strictement requise par votre classe, vous pouvez utiliser l’injection
de méthode.

classe abstraite CheckProcessor { const


CheckProcessor(); bool estActive();

la classe MastercardCheck implémente CheckProcessor {


finale MasterCardApi _api ; const
MastercardCheck(this._api);

bool isActive() async => wait _api.isOnline();


}

la classe PaypalCheck implémente CheckProcessor {


PaypalApi final _api ;

Référence complète Flutter 198


Machine Translated by Google

Chapitre 8. Principes de codage avec Dart

const PaypalCheck(this._api);

bool isActive() async => wait _api.available();


}

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.

class PaymentProcessor { final


PaymentValidator _validator ; const
PaymentProcessor(this._validator);

annuler le paiement ( montant int) => ...

bool isProcessorActive (vérification CheckProcessor) =>


return check.isActive();
}

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

paiement final = PaymentProcessor (processeur); final isOnline =


payment.isProcessorActive(checker);
}

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.

Référence complète Flutter 199


Machine Translated by Google
Machine Translated by Google

Partie II

Le cadre Flutter

"Les programmes doivent être écrits pour que les gens puissent les lire, et seulement

accessoirement pour que les machines puissent les exécuter."


Abelson et Sussman

201
Machine Translated by Google
Machine Translated by Google

9 | Bases du Flutter

9.1 Structure et outils


Android Studio (AS), avec le plugin officiel, est l'IDE de premier choix de Google qui offre un
expérience de développement très agréable. Alternativement, les applications Flutter peuvent également être créées à
l'aide de Vi­sual Studio Code (VS Code), d'Emacs ou de tout autre éditeur de texte avec l'outil de ligne de commande 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/get­started/install

Référence complète Flutter 203


Machine Translated by Google

Chapitre 9. Bases de Flutter

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 ci­dessus sont automatiquement gérés
par l'EDI (ou le compilateur), vous ne devriez donc pas vous en soucier.

Référence complète Flutter 204


Machine Translated by Google

Chapitre 9. Bases de Flutter

9.1.1 Structure des dossiers


Avant de commencer votre parcours de codage, ce serait une bonne idée d'avoir une solide expérience en structure et
organisation de dossiers. La documentation officielle de Flutter ne donne aucune directive à ce sujet puisque vous êtes libre
de faire ce que vous préférez. Nous avons quelques suggestions pour vous :

• 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 sous­dossiers.

Référence complète Flutter 205


Machine Translated by Google

Chapitre 9. Bases de Flutter

• 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/

Nous aborderons les tests en profondeur au chapitre 16.

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.

9.1.2 Le fichier pubspec.yaml


Ce dossier est très important et il mérite d’être correctement décrit. Il vous donne le contrôle sur : les dépendances utilisées
par Flutter, les ressources/actifs de votre application et le système de versionnage pour les binaires de production.

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 points­virgules 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

Référence complète Flutter 206


Machine Translated by Google

Chapitre 9. Bases de Flutter

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 ci­dessus, 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.

• usages­matériaux­conception. 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 utilisations­matériaux­conception : 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/

Référence complète Flutter 207


Machine Translated by Google

Chapitre 9. Bases de Flutter

• 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 ;

2. recherchez « http » ou tout autre mot­clé significatif ;

3. choisissez un package dans la liste et cliquez sur l'onglet Installation ;

4. copiez/collez la chaîne d'installation donnée, dans notre cas "http: ^0.12.2"

• 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 importez­les directement dans votre application.

battement:
actifs:

­ images/

Référence complète Flutter 208


Machine Translated by Google

Chapitre 9. Bases de Flutter

polices :

­ famille : Roboto
polices :

­ élément : fonts/Roboto­Regular.ttf ­ élément :


fonts/Roboto­Italic.ttf
style : italique ­
famille : RobotoMono
polices :

­ actif : fonts/Righteous­Regular.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 placez­y 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 mettez­le 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.

2. Avant de publier l'application, accédez à https://fonts.google.com/, télécharger les fichiers de polices


dont vous avez besoin et déplacez­les vers le dossier font/ de votre projet Flutter.

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

Référence complète Flutter 209


Machine Translated by Google

Chapitre 9. Bases de Flutter

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

9.1.3 Rechargement à chaud

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

Référence complète Flutter 210


Machine Translated by Google

Chapitre 9. Bases de Flutter

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 changez la définition d'une classe en énumération et vice versa,

• 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 Vir­tual 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.

9.1.4 Règles de linter

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.

Référence complète Flutter 211


Machine Translated by Google

Chapitre 9. Bases de Flutter

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 :

• erreur : provoque l'échec de l'analyse statique ;

• 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 ;

• ignorer : ignore la règle donnée.

4https://dart­lang.github.io/linter/lints/

Référence complète Flutter 212


Machine Translated by Google

Chapitre 9. Bases de Flutter

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.

9.1.5 Secousses des arbres et constantes


L'utilisation de l'importation "package:flutter/foundation.dart" peut être très utile lors du développement et du
débogage d'applications Flutter. Il expose trois valeurs booléennes constantes que le développeur peut utiliser
pour exécuter une série d'instructions en fonction du mode de construction :

• 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/analyzer­library.html

Référence complète Flutter 213


Machine Translated by Google

Chapitre 9. Bases de Flutter

String get name { if


(kDebugMode) { return
"Démo" ; } else
{ return
_real();
}
}

Le bouton Exécuter d'Android Studio et VS Code crée l'application en mode débogage afin que le code ci­dessus 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 :

Mode débogage Mode profil Mode de libération

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 :

const estBon = vrai ;

si (est bon) {
print("Bien!"); } else

{ print("Mauvais!");
}

Le compilateur supprimera la branche else car elle est considérée comme du code mort.

Référence complète Flutter 214


Machine Translated by Google

Chapitre 9. Bases de Flutter

9.2 Widgets et état


Dans Flutter, tout ce qui apparaît à l'écran est appelé « widget » car, techniquement parlant, c'est un descendant
de la classe Widget. Lorsque vous créez des interfaces utilisateur dans Flutter, vous créez une composition de
widgets en les imbriquant les uns dans les autres.

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 elle­même. Tout ce qui apparaît à l'écran ou interagit avec
celui­ci 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());
}

la classe MyApp étend StatelessWidget {


@passer outre

Construction du widget (contexte BuildContext) {...}


}

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 elle­mê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

Référence complète Flutter 215


Machine Translated by Google

Chapitre 9. Bases de Flutter

classe appelée MyApp la racine de l'arborescence. Ajoutons plus de contenu pour voir comment l'arborescence des widgets se
développe.

void main() => runApp(const MyApp());

la classe MyApp étend StatelessWidget { const


MyApp(); // Bonne idée!

@passer outre

Widget build (contexte BuildContext) { return


Column ( enfants :
<Widget>[
Texte("Bonjour"),
Texte("Flutter),
]

}
}

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 :

Référence complète Flutter 216


Machine Translated by Google

Chapitre 9. Bases de Flutter

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.

L'interpolation de classes, imbriquées avec des constructeurs nommés, embrasse le


appelé conception déclarative d’interface utilisateur qui est le style de codage typique de Flutter.

Référence complète Flutter 217


Machine Translated by Google

Chapitre 9. Bases de Flutter

9.2.1 Widgets de base


Flutter propose une quantité incalculable de widgets qui peuvent être trouvés à la fois dans la bibliothèque
principale ou en ligne sur https://pub.dev. Nous allons tout de suite lister les plus importants mais vous en
découvrirez bien d'autres en lisant le livre.

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 lui­mê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/TextStyle­class.html

Référence complète Flutter 218


Machine Translated by Google

Chapitre 9. Bases de Flutter

),

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.

Ligne( mainAxisAlignment : MainAxisAlignment.center, enfants : const


[ Text("Bonjour"),
Text("Flutter!"),
Text("!!"),

],
),

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.

• centre. Place les éléments au centre de la ligne.

• commencer. Place les éléments au début de la ligne.

Référence complète Flutter 219


Machine Translated by Google

Chapitre 9. Bases de Flutter

• fin. Place les éléments à la fin de la ligne.

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

Référence complète Flutter 220


Machine Translated by Google

Chapitre 9. Bases de Flutter

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 au­dessus 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.

Référence complète Flutter 221


Machine Translated by Google

Chapitre 9. Bases de Flutter

9.2.1.4 Vue Liste

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.

ListView( enfants : const


[ Text("Bonjour"),
Text("Flutter!"), Text("!!),

],
),

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.

ListView( scrollDirection : Axis.horizontal,


),

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 ci­dessus. 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]}"),
},
),

La documentation officielle de Flutter 7


suggère d'utiliser le builder(...) nommé constructor lorsque la
source de données est une longue liste car elle gère efficacement les enfants. Alors, plutôt que manuellement

7https://flutter.dev/docs/cookbook/lists/long­lists

Référence complète Flutter 222


Machine Translated by Google

Chapitre 9. Bases de Flutter

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.

Widget build (contexte BuildContext) => Conteneur


( hauteur : 80,
largeur : 260,
couleur :
Colors.blueGrey, alignement :
Alignment.center, transformation :
Matrix4.rotationZ(­0.25), enfant : const
Text( "Conteneurs !",
style: TextStyle
( couleur: Colors.white,

Taille de la police : 25

)
)

Référence complète Flutter 223


Machine Translated by Google

Chapitre 9. Bases de Flutter

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 :

Conteneur( enfant : const Center(...),


largeur : 100,
hauteur : 100,
décoration : const BoxDecoration( forme :
BoxShape.circle, boxShadow :
[ BoxShadow(

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ère­plan. La classe BoxShadow est très similaire à la propriété CSS box­shadow , 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

Référence complète Flutter 224


Machine Translated by Google

Chapitre 9. Bases de Flutter

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ère­plan unie avec la propriété color :. Assurez­vous de consulter la documentation officielle sur BoxDecoration
8
pour voir comment vous pouvez entièrement personnaliser un conteneur.

9.2.1.6 Empilement et positionnement

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/BoxDecoration­class.html

Référence complète Flutter 225


Machine Translated by Google

Chapitre 9. Bases de Flutter

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.

9.2.2 Widgets sans état et avec état


Une classe devient un widget Flutter lorsqu'elle sous­classe StatelessWidget ou StatefulWidget et remplace la construction
du widget(...); méthode abstraite. C'est tout : la tâche principale d'un widget est de disposer d'autres widgets sur
l'arborescence à l'aide de la méthode build().

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:

Référence complète Flutter 226


Machine Translated by Google

Chapitre 9. Bases de Flutter

• l'utilisateur a appuyé sur un bouton et quelque chose dans l'interface utilisateur doit donc changer ;

• l'appareil a subi une rotation et l'interface utilisateur doit être repeinte ;

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

la classe MyName étend StatelessWidget {


// Notez le constructeur de constante const
MyName();

@passer outre

Construction du widget (contexte BuildContext)


{ return Row (
mainAxisAlignment : MainAxisAlignment.spaceAround, enfants :
const [ Icon(Icons.person),
Text("Développeur Flutter"),

]

}
}

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

class MyName extends StatelessWidget { nom de


chaîne final ; const
MyName({ // utilisez
l'annotation '@required' si votre version de Dart ne prend pas en charge //nnbd requirejs
this.name

Référence complète Flutter 227


Machine Translated by Google

Chapitre 9. Bases de Flutter

});

@passer outre

Widget build (contexte BuildContext) => const Text (nom);


}

Par convention, les widgets Flutter ont nommé des paramètres facultatifs dans le constructeur ; au cas où ils seraient
requis, utilisez le mot­clé 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 mot­clé 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.

la classe Counter étend StatefulWidget {


// N'oubliez pas le constructeur constant ! const Compteur();

@passer outre

_CounterState createState() => _CounterState();


}

// 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

Construction du widget (contexte BuildContext) { return


Column (

Référence complète Flutter 228


Machine Translated by Google

Chapitre 9. Bases de Flutter

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 lui­mê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 passe­partout pour vous.

3. Les sous­classes de State<T> accèdent à la méthode setState(...) qui reconstruit le


widget (c'est comme un outil de rafraîchissement).

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.

Référence complète Flutter 229


Machine Translated by Google

Chapitre 9. Bases de Flutter

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.

la classe _CounterState étend State<Counter> {


int _counter = 0 ;

@passer outre

Widget build (contexte BuildContext) { return Column(...);

}
}

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

la classe _CounterState étend State<Counter> {


@passer outre

Construction du widget (contexte BuildContext) { int


_counter = 0 ;

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 à celles­ci en utilisant simplement le getter de widget. Encore une fois, essayez d'utiliser const
autant que possible.

class WidgetDemo extends StatefulWidget { final int id ; const


WidgetDemo(this.id);

@passer outre

_WidgetDemoState createState() => _WidgetDemoState();


}

Référence complète Flutter 230


Machine Translated by Google

Chapitre 9. Bases de Flutter

class _WidgetDemoState extends State<WidgetDemo> { @override

Widget build(Contexte BuildContext) { return Text("L'


identifiant donné est ${widget.id}");
}
}

Dans l'exemple ci­dessus, 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.

9.2.2.1 Bonnes pratiques

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 passe­partout 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 .

class PersonWidget extends StatelessWidget { nom de chaîne final ;

Référence complète Flutter 231


Machine Translated by Google

Chapitre 9. Bases de Flutter

Âge final de la chaîne ;


constPersonWidget ({
requis this.name, requis
this.age
});

@override

Widget build (contexte BuildContext) { ... }


}

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.

le compteur de classe étend StatefulWidget { const


Counter();

@passer outre

_CounterState createState() => _CounterState();


}

la classe _CounterState étend State<Counter> {


int _counter = 0 ;

@passer outre

Construction du widget (contexte BuildContext) { ... }


}

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.

la classe AuthorsWidget étend StatelessWidget {


const AuteursWidget();

Référence complète Flutter 232


Machine Translated by Google

Chapitre 9. Bases de Flutter

@passer outre

Construction du widget (contexte BuildContext)


{ return Row (
enfants: [
Texte("Alberto Miola"),
Texte("Félix Angélov"),
Text("Rémi Rousselet"),
Texte("Matej Rešetár"),
]

}
}

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 :

final itemKey = ValueKey<String>("item­id­0025");

// puis sur la méthode build...


Texte(
itemText,
clé : itemKey,
)

Utilisez un ValueKey lorsque vous avez un objet représenté par une valeur unique et constante.

Référence complète Flutter 233


Machine Translated by Google

Chapitre 9. Bases de Flutter

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é de classe abstraite


{ const factory Key(String value) = ValueKey<String>;
}

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

liste finale = [Tâche

( propriétaire : const OwnerData(...), date :


"...", durée :
const Durée(...),
)
]

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 lui­même est unique.

• Clé Unique. Cette clé n'est égale qu'à elle­mê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.

Référence complète Flutter 234


Machine Translated by Google

Chapitre 9. Bases de Flutter

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.

// La disposition des onglets est traitée au chapitre 21


TabBarView(
contrôleur : tabController, enfants :

[ ListView.builder( clé : const


PageStorageKey<String>(('list1'), itemBuilder : (contexte, index)
{...},
),
ListView.builder( clé :
const PageStorageKey<String>(('list2'),

Référence complète Flutter 235


Machine Translated by Google

Chapitre 9. Bases de Flutter

itemBuilder : (contexte, index) {...},


),
]
)

Un PageStorageKey<T> (sous­classe de ValueKey<T>) est utilisé pour mémoriser la position de défilement


d'une liste lorsque la page d'un onglet est modifiée. Si vous n'avez pas utilisé de PageStorageKey, la
position de défilement des listes sera remise à 0 à chaque changement d'onglet (la position n'est pas
mémorisée par défaut).

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.

9.3 Reconstructions et optimisation


Le framework parcourt très souvent l’arborescence des widgets. La méthode build() est appelée, bien sûr, la première fois que
l'interface utilisateur est rendue. Elle sera appelée plus d'une fois au cours de la durée de vie de votre application, mais vous ne
pouvez pas prédire combien de fois car de nombreux facteurs peuvent déclencher une reconstruction :

• appeler setState,

• faire pivoter l'écran de l'appareil,

• en attendant le résultat d'un futur,

• écouter les événements de flux entrants.

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.

Référence complète Flutter 236


Machine Translated by Google

Chapitre 9. Bases de Flutter

9.3.1 constructeur const

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 :

la classe ExempleWidget étend StatelessWidget {


const ExempleWidget();

@passer outre

Construction du widget (contexte BuildContext) {...}


}

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 :

ListView( enfants : const [


ExempleWidget(),
ExempleWidget(),
ExempleWidget(),
ExempleWidget(),
]

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 sous­arbre 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.

la classe ExempleWidget étend StatelessWidget {


// Pas de constructeur constant

Référence complète Flutter 237


Machine Translated by Google

Chapitre 9. Bases de Flutter

@passer outre

Construction du widget (contexte BuildContext) {.}


}

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 :

L'exemple de classe étend StatefulWidget { const


Exemple();

@passer outre

_ExampleState createState() => _ExampleState();


}

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 sous­arbres et gagner beaucoup de temps de calcul !

version constante version non constante

ListView ListView
( enfants : const

[ ExempleWidget(), ( enfants : [ExempleWidget(),


ExempleWidget(), ExempleWidget(),
ExempleWidget(), // ... + 7 ExempleWidget(), // ... + 7
autres entrées autres entrées
] ]
); );

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.

9.3.2 Préférer la composition des widgets aux fonctions


Il est de notoriété publique que la duplication de code est mauvaise et vous créerez donc très souvent des widgets
réutilisables. Par exemple, de nombreuses applications ont un « pied de page » qui comprend des icônes et un peu de
texte sur les droits d'auteur.

Référence complète Flutter 238


Machine Translated by Google

Chapitre 9. Bases de Flutter

la classe FooterWidget étend StatelessWidget {


const FooterWidget();

@passer outre

Construction du widget (contexte BuildContext)


{ return Column (
mainAxisSize : MainAxisSize.min,
enfants : [ Ligne

( mainAxisAlignment : MainAxisAlignment.spaceAround, enfants : const


[ Icône (Icons.email),
Icône (Icons.tablet_mac),

]
),
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 :

• il existe un constructeur constant car la classe n'a pas de variables mutables ;

• 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 :

Widget footerWidget (contexte BuildContext) =>


Colonne(
mainAxisSize : MainAxisSize.min, enfants :
[Ligne

( mainAxisAlignment : MainAxisAlignment.center,

Référence complète Flutter 239


Machine Translated by Google

Chapitre 9. Bases de Flutter

enfants : const [Icône


(Icons.email), Icône
(Icons.tablet_mac), Icône
(Icons.tune)
]
),

const Text("Développé par X"),


]

}

C'est une fonction renvoyant le widget Colonne avec ses enfants : il ne faut absolument JAMAIS préférer les fonctions aux
widgets car :

• Les fonctions n'ont bien sûr pas de constructeurs const .

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

Référence complète Flutter 240


Machine Translated by Google

Chapitre 9. Bases de Flutter

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.

Référence complète Flutter 241


Machine Translated by Google

Chapitre 9. Bases de Flutter

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

Référence complète Flutter 242


Machine Translated by Google

Chapitre 9. Bases de Flutter

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 :

1. Mode HTML. Flutter utilise HTML, CSS, JavaScript et Canvas.

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.

9.4.1 Élément et RenderObject

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.

la classe MonWidget étend StatelessWidget { const


MonWidget();

@passer outre

Widget build (contexte BuildContext) { return


Container ( décoration :
BoxDecoration (), enfant : SomeText (

texte : "Bonjour"

),

9https://www.w3.org/TR/wasm­core­1/
10Voir « The Layer Cake » de Frederik Schweiger sur Medium

Référence complète Flutter 243


Machine Translated by Google

Chapitre 9. Bases de Flutter

}
}

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.

11Plus d'informations à ce sujet au chapitre 16

Référence complète Flutter 244


Machine Translated by Google

Chapitre 9. Bases de Flutter

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.

la classe abstraite ComponentElement étend Element { ... }

Référence complète Flutter 245


Machine Translated by Google

Chapitre 9. Bases de Flutter

– RenderObjectElement. Un élément qui participe à la peinture, à la mise en page et aux tests de frappe
étapes.

la classe abstraite RenderObjectElement étend Element { ... }

L'arborescence des éléments est essentiellement une série de ComponentElement ou de RenderObjectElement,


en fonction du widget auquel ils font référence. Dans notre exemple, un Container est un ComponentElement
car il peut héberger d’autres widgets à l’intérieur.

• Arborescence des widgets. Il est composé de classes étendant StatelessWidget ou StatefulWidget.


Ils sont utilisés par le développeur pour créer l'interface utilisateur et ne coûtent pas cher à créer (beaucoup
moins qu'un RenderObject).

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 lui­même est immuable mais l'état qu'il renvoie
peut muter.

L'exemple de classe étend StatefulWidget { const


Exemple();

@passer outre

_ExampleState createState() => _ExampleState();


}

class _ExampleState extends State<Example> {


@passer outre

Construction du widget (contexte BuildContext) { ... }


}

Le widget lui­mê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é).

Référence complète Flutter 246


Machine Translated by Google

Chapitre 9. Bases de Flutter

Pour chaque reconstruction, Flutter parcourt l'intégralité de l'arborescence à la recherche de


modifications sur les widgets. Si le type du widget changeait, il serait alors supprimé et remplacé
avec son élément et son RenderObject associés. Les 3 sous­arbres seraient également recréés. Si
le widget était du même type et que seules quelques propriétés étaient modifiées, l'élément resterait
intact et le RenderObject serait mis à jour (et non recréé). Voyons un exemple :

Widget build (contexte BuildContext) { return


Container ( décoration :
BoxDecoration (), enfant : SomeText (

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.

Widget build (contexte BuildContext) { return


Container ( décoration :
BoxDecoration (), enfant : SomeText (

texte : « Bonjour tout le monde ! »

),

}

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.

Référence complète Flutter 247


Machine Translated by Google

Chapitre 9. Bases de Flutter

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.

Widget build(Contexte BuildContext) { return


Container( decoration:
BoxDecoration(), child: Text("Bonjour
tout le monde!"),

}

En parcourant l'arborescence, le framework remarque à nouveau le changement grâce à l'arborescence des


éléments. En particulier, cette fois, le type du widget est complètement différent, il est donc nécessaire de
reconstruire l'intégralité des sous­arbres (widgets, éléments et rendus).

Référence complète Flutter 248


Machine Translated by Google

Chapitre 9. Bases 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.

9.4.2 Interface de fonction étrangère


Grâce à la bibliothèque dart:ffi , également connue sous le nom de Foreign Function Interface, votre code Dart peut se
lier directement aux API natives écrites en C. FFI est très rapide car aucune sérialisation n'est requise pour transmettre
les données puisque les appels sont effectués vers des bibliothèques liées dynamiquement ou statiquement. . Voici un
exemple :

// demo.h
void print_demo() {};

Référence complète Flutter 249


Machine Translated by Google

Chapitre 9. Bases de Flutter

// démo.c
#include <stdio.h>
#include "démo.h"

void print_demo() { printf("


Démo Dart FFI!");
}

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 :

importer "dart:ffi" en tant que FFI ;

// Signature de la fonction en C typedef


print_demo_c = FFI.Void Function(); // Signature de la fonction
dans Dart typedef PrintDemo = void Function();

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.

importer "dart:ffi" en tant que FFI ;

typedef print_demo_c = FFI.Void Function(); typedef


PrintDemo = void Function();

void main() { //
Ouvrir le chemin final de la
bibliothèque = "demo_lib.dll"; // Sous Windows final lib =
FFI.DynamicLibrary.open(path);

// Crée un "lien" de C vers Dart

Référence complète Flutter 250


Machine Translated by Google

Chapitre 9. Bases de Flutter

Démo finale de PrintDemo = lib


.lookup<FFI.NativeFunction<print_demo_c>>('print_demo') .asFunction();

// Appelez la fonction démo();

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, assurez­vous que votre compilateur
exporte correctement vers la DLL les fonctions que Dart doit utiliser.

int somme(int a, int b) { return a +



}

Le code ci­dessus 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);

Consultez les chaînes de documentation 12


pour quelques bons exemples sur la façon d'interagir avec les structures,
officielle et les bases de données SQLite.

9.4.3 Canaux de méthode


Disponibles uniquement pour les appareils mobiles et les ordinateurs de bureau, les canaux de méthode permettent à Dart d'appeler le code
spécifique à la plate­forme de votre application d'hébergement. Les données sont sérialisées depuis Dart puis désérialisées en Java, Kotlin,
Swift ou Objective­C. Regardez comme c'est simple :

const canal = MethodChannel("personne"); nom final = wait


canal.invokeMethod<String>("getPersonName"); imprimer(nom); // 'name' est une chaîne Dart
normale

A titre d'exemple, disons que le code ci­dessus 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/dart­ffi/dart­ffi­library.html

Référence complète Flutter 251


Machine Translated by Google

Chapitre 9. Bases de Flutter

une partie aussi mais c'est très simple à comprendre :

// Initialisation val canal


= MethodChannel(flutterView, "person") canal.setMethodCallHandler
{appel, résultat ­>
quand (appel.méthode) {
"getPersonName" ­> result.success(getPersonName()) else ­>
result.notImplemented()
}
}

// Cette fonction est définie dans un endroit amusant


getPersonName() : String {
retourner "Alberto"
}

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

canal const = MethodChannel("random"); final random


= wait canal.invokeMethod<int>("getRandom", 60);

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

// Initialisation let chl =


FlutterMethodChannel(nom : "random", binaireMessenger : flutterView) chl.setMethodCallHandler { (appel :
FlutterMethodCall, résultat :
FlutterResult) ­> Vide dans le commutateur (call.method) {

case "getRandom": result(getRandom(call.arguments as! Int)) par défaut:


result(FlutterMethodNotImplemented)
}
}

// Cette fonction est définie quelque part

Référence complète Flutter 252


Machine Translated by Google

Chapitre 9. Bases de Flutter

func getRandom(valeur : Int) ­> Int {


return Int.random(in: 0...value);
}

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ère­plan

Référence complète Flutter 253


Machine Translated by Google

Chapitre 9. Bases de Flutter

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 sous­jacent é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 ;

4. une réponse est renvoyée au client (Flutter) qui traite le résultat.

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.

Référence complète Flutter 254


Machine Translated by Google

10 | Créer des interfaces utilisateur dans Flutter

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 :

Référence complète Flutter 255


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

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 ci­dessus :

• 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 :

Widget build (contexte BuildContext) { return


MaterialApp ( accueil :
Scaffold (
barre d'application : barre d'application (

titre : const Text("Flutter"), actions : const


[ Padding( padding :

EdgeInsets.only(right : 20), enfant : Icon(Icons.info),

] ), tiroir : const Drawer(), corps :


const Center( enfant :
Text("Wow nice book"), ),

floatActionButton : FloatingActionButton( onPressed: () {},


enfant : const
Icon(Icons.add), ) , ), );

}
}

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/guidelines­overview/

Référence complète Flutter 256


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

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"),
)
)

Référence complète Flutter 257


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

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.

Référence complète Flutter 258


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

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

tiroir 'classique' de gauche à droite : Drawer( child:


ListView( ListTile( leader:
const Icon(Icons.people),
title: const

Text("Item 1"), onTap: () {},

),
ListTile( en

tête : const Icon(Icons.train), titre : const Text("Item


2"), onTap: () {},

)
)
),
// 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 float­ingActionButtonLocation, vous pouvez décider de la
position du widget :

Échafaudage

( floatingActionButton : FloatingActionButton ( enfant : const


Icon(Icons.add), backgroundColor : Colors.red,
onPressed : () {...},

),
)

Cet extrait ajoute un FAB à la position par défaut de l'écran (en bas à droite) :

Référence complète Flutter 259


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

Voulez­vous l’avoir au centre plutôt qu’à droite ? Il existe de nombreux postes disponibles que vous pouvez utiliser :

floatActionButton : FloatingActionButton ( enfant : const Icon


(Icons.add), backgroundColor : Colors.red,
onPressed : () {},

),
floatActionButtonLocation :
FloatingActionButtonLocation.centerFloat,

Un échafaudage ne peut avoir qu’un seul FAB.

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.

10.1.2 Widgets de matériaux

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/Scaffold­class.html

Référence complète Flutter 260


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

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,

Référence complète Flutter 261


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

enfants: [

FlatButton( onPressed : () {},


enfant : const Text("Non")
),

RaisedButton( onPressed : () {},


enfant : const Text("OUI")
),
],

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

10.1.2.2 Boîtes de dialogue

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(

enfant : const Text("Oui"), onPressed :


() {},
),
FlatButton( enfant :
const Text("Sûr"), onPressed : () {},

Référence complète Flutter 262


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

]

}

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.

C'est juste une question d'utiliser RoundedRectangleBorder et Flutter s'occupera de tout

Référence complète Flutter 263


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

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

type final = attendre showDialog<String>(


contexte : contexte,
constructeur : (contexte BuildContext) {
return SimpleDialog( titre :
const Text("Saveur de gâteau ?"), enfants : [

Référence complète Flutter 264


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

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.

Référence complète Flutter 265


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

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

Référence complète Flutter 266


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

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.

Widget build (contexte BuildContext) { return


CupertinoApp ( accueil :
const CupertinoPageScaffold (
navigationBar : CupertinoNavigationBar ( milieu :
Texte ("Cupertino App"),
),
enfant : Centre(
enfant : Texte("Thème Cupertino!"),
),
),

}

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.

Référence complète Flutter 267


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

10.2.1 Échafaudage de pages de Cupertino

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/CupertinoTabBar­class.html

Référence complète Flutter 268


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

tabBar : CupertinoTabBar( onTap :


(index) {...}, activeColor :
Colors.blue, items : const

[ BottomNavigationBarItem(
icône : Icône (Icons.home),
titre : Texte ("Accueil"),
),
BottomNavigationBarItem(
icône : Icône(Icons.email), titre :
Texte("E­mail"),
),
],
),
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 :

renvoie const PageOneWidget();


cas 1:
défaut:
return const PageTwoWidget();
}
},
),
),

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"

Référence complète Flutter 269


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

page.

10.2.2 Widgets de Cupertino


Flutter fournit une très grande collection de widgets qui suivent les directives de conception iOS. Au moment de la rédaction de
ce livre, la bibliothèque de Cupertino contient moins de widgets que de matériel, mais l'équipe Flutter a déclaré dans sa feuille
4
de route que la collection allait s'agrandir au fil du temps.

• Boîte de dialogue Alerte Cupertino.

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

Référence complète Flutter 270


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

],

}
),

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ère­plan 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(

Référence complète Flutter 271


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

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. Assurez­vous de consulter le catalogue de
5
Cupertino dans la documentation officielle de Flutter.

10.3 Dispositions des bâtiments

10.3.1 Prise en charge de la plateforme

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 plates­formes 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.

10.3.1.1 Système d'exploitation unique

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

Référence complète Flutter 272


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

• un menu coulissant depuis la droite/gauche de l'écran (un Tiroir) ;

• probablement un FAB ou une mise en page avec des onglets glissants.

Un échafaudage vous aide à créer facilement une mise en page avec les caractéristiques ci­dessus, 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.

Référence complète Flutter 273


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

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.

10.3.1.2 Plusieurs systèmes d'exploitation

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 :

// Contient 'TargetPlatform' import 'dart:io'


show Platform ;

vide main() {

Référence complète Flutter 274


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

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 Objective­C/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.

importer 'dart:io' show Platform ;

void main() => runApp(RestaurantApp());

la classe RestaurantApp étend StatelessWidget {


const RestaurantApp();

// Selon la plateforme, renvoie un logo différent String _logoName() { if


(Platform.isIOS) { return
"Bienvenue utilisateur iOS !";

}
return "Bienvenue utilisateur Android !";
}

Construction du widget (contexte BuildContext) {...}


}

Dans cet exemple, la même base de code unique fonctionne pour Android et iOS. Certains paramètres

Référence complète Flutter 275


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

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 !

10.3.2 Interfaces utilisateur réactives

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 :

Référence complète Flutter 276


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

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.

10.3.2.1 Générateur de mises en page

Considérant l’exemple ci­dessus 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

( corps : ListView.builder ( itemCount :


50, itemBuilder :
(contexte, identifiant) {
return ListTile ( en tête :
const Icon (Icons.add_box), titre : Text ("Item $id"),


}

Référence complète Flutter 277


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

)
),

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.

la classe ListData étend StatelessWidget { const ListData();

@passer outre

Widget build (contexte BuildContext) { return


ListView.builder (
itemCount : 100,
itemBuilder : (contexte, identifiant) {
return ListTile ( en tête :
const Icon (Icons.add_box), titre : Text ("Item $id"),


}

}
}

la classe GridData étend StatelessWidget {

Référence complète Flutter 278


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

constGridData ();

@passer outre

Widget build (contexte BuildContext) { return


GridView.count ( crossAxisCount :
2, enfants : List.generate
(100, (index) {
Centre de retour (
enfant : ListTile ( en
tête : const Icon (Icons.add_box), titre : Text ("Item
$ index"),


}

}
}

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

Référence complète Flutter 279


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

et ordinateur de bureau.

10.3.2.2 Requête multimédia

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

Widget build(BuildContext context) { // Nous utilisons


'double.nan' mais cela aurait pu être n'importe quelle autre valeur final width =
MediaQuery.of(context)?.size.width ?? double.nan; return Text("$largeur");

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 :

orientation finale = MediaQuery.of(context)?.orientation;

// Utiliser une vérification nulle plutôt que de fournir une valeur par défaut if ((orientation !=
null) && (orientation == Orientation.portrait)) {...}

10.3.2.3 Bonnes pratiques

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 lui­mê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 lui­mê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 ;

Référence complète Flutter 280


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

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 :

– MediaQuery renvoie la largeur de l'écran

– LayoutBuilder renvoie la largeur du widget parent mais, étant la racine, il faut


toute la taille de l'écran.

Essayez maintenant d'exécuter cet exemple qui est un peu différent :

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

Référence complète Flutter 281


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter


}

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.

– LayoutBuilder prend en compte le fait qu’il y ait un padding et le retour


la dimension est screenSize ­ paddingAmount, qui est l'espace disponible.

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

Référence complète Flutter 282


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

10.3.3 Défilement et contraintes


Les ListViews et les Columns sont très populaires, mais vous devez faire attention à la façon dont ils traitent le contenu sur
l'axe vertical. Ce code semble fonctionner comme prévu mais il lancera un runtime
exception car la hauteur de la colonne est infinie.

Colonne( enfants : [
const Text("Mon nom"), const
Text("Mon nom"),

ListView( enfants : const [


Texte("Compétence 1"), Texte("Compétence 2"),
]
)
]

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 ci­dessus 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

Référence complète Flutter 283


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

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"),
]
),

Référence complète Flutter 284


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

),
]

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.

2. Il existe un attribut intéressant de ListView qui modifie le comportement du widget afin


qu'il a une hauteur fixe et que le problème de la limite inférieure disparaît.

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.

Référence complète Flutter 285


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

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(

Référence complète Flutter 286


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

contraintes : const BoxConstraints.expand ( hauteur : 200

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

10.3.4 Utiliser des thèmes


Si vous souhaitez partager des styles de police, des couleurs et d'autres paramètres d'apparence de l'interface utilisateur
dans une application, utilisez la classe ThemeData, dans le cas d'une MaterialApp, ou CupertinoThemeData, pour le widget
CupertinoApp. Par exemple, si vous avez utilisé ce type de configuration...

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 :

const Text("Quelque chose",


style : TextStyle(
// Inutile car "Times New Roman" est déjà hérité grâce // à 'ThemeData' fontFamily : "Times
New Roman",

)
)

Avec l'utilisation de ThemeData, les modifications sont automatiquement répercutées sur tous les enfants afin que la maintenance soit effectuée.

Référence complète Flutter 287


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

le financement est beaucoup plus facile. Souhaitez­vous 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 ci­dessus.
C'est possible grâce au widget Thème :

//main.dart
MaterialApp(

Référence complète Flutter 288


Machine Translated by Google

Chapitre 10. Création d'interfaces utilisateur dans Flutter

thème : ThemeData.dark();
)

// build du widget light_footer.dart


(contexte BuildContext) {
return Theme
( données : ThemeData.light(),
enfant : const MyFooter(),

}

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
sous­arbre. En fait:

• appeler Theme.of(context) dans MyFooter renvoie une référence au thème clair ;

• appeler Theme.of(context) en dehors de MyFooter renvoie une référence au thème sombre.

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.

Référence complète Flutter 289


Machine Translated by Google

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/data­and­backend/state­mgmt/options

Référence complète Flutter 290


Machine Translated by Google

Chapitre 11. Gestion de l'État

1. Mise à jour de l'interface utilisateur à l'aide de setState,

2. Passer l'état dans l'arborescence des widgets, à l'aide du widget Provider,

3. Alternatives à setState(), implémentées à l'aide du widget BlocBuilder.

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.

11.1 Mise à jour de l'interface utilisateur

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 passe­partout.

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 elle­mê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

_DemoPageState createState() => _DemoPageState();


}

la classe _DemoPageState étend State<DemoPage> {


// 4.
int _counter = 0;

// 5.

Référence complète Flutter 291


Machine Translated by Google

Chapitre 11. Gestion de l'État

void _increment()
{ setState(() => _counter++);
}

void _decrement() {
setState(() => _counter­­);
}

@passer outre

Construction du widget (contexte BuildContext) {...}

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

4. Le compteur qui sera affiché dans un widget Texte.

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,

Référence complète Flutter 292


Machine Translated by Google

Chapitre 11. Gestion de l'État

enfants: [
Bouton Plat (
enfant : const Text("+1", style :

TextStyle( couleur :
Colors.green, fontSize : 25

),
),
onPressed : _increment,
),
Text("$_counter", style :

const TextStyle( fontSize : 30,

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

Référence complète Flutter 293


Machine Translated by Google

Chapitre 11. Gestion de l'État

Dans le cas d'un widget sans enfant comme A, lorsque setState est appelé, une reconstruction se produit uniquement
pour A. Dans l'image ci­dessus, 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 :

class _WidgetAState extends State<WidgetA> { int _value = 0;

@passer outre

Widget build (contexte BuildContext) { return Column


( enfants : [

const WidgetB("$_value"), const


WidgetC(),
Bouton surélevé (
enfant : const Text("Mise à jour"),
onPressed : () => setState(() { _value +=
10 ;
}),
),
]

}
}

Lorsque vous appuyez sur le bouton, setState déclenchera toujours une reconstruction pour le widget
actuel et tous ses enfants. Dans l'exemple ci­dessus, WidgetB et WidgetC sont reconstruits même si
seul WidgetB le devrait, car c'est le seul qui a une dépendance de WidgetA.

Référence complète Flutter 294


Machine Translated by Google

Chapitre 11. Gestion de l'État

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 sous­arbre, 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 passe­partout 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 !

Référence complète Flutter 295


Machine Translated by Google

Chapitre 11. Gestion de l'État

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 passe­partout ; 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 ;

• il gère la logique métier, qui s'occupe de _counter ;

• il gère l'état de l'application, qui est géré par setState

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.

la classe _DemoPageState étend State<DemoPage> {

@passer outre

void initState() {

2https://api.flutter.dev/flutter/widgets/InheritedWidget­class.html

Référence complète Flutter 296


Machine Translated by Google

Chapitre 11. Gestion de l'État

super.initState(); // mets le
code ici...
}

@passer outre

Construction du widget (contexte BuildContext) {...}

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.

la classe _DemoPageState étend State<DemoPage> {

@passer outre

void dispose() { // votre


code ici... super.dispose();

@passer outre

Construction du widget (contexte BuildContext) {}

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.

valeur de chaîne tardive ;

@override void

initState() { super.initState();

valeur = "Initez­moi" ;

Si cette valeur ne doit être attribuée qu'une seule fois dans initState, envisagez de la rendre finale tardive.

Référence complète Flutter 297


Machine Translated by Google

Chapitre 11. Gestion de l'État

Avec l'arrivée de NNBD, les variables pourraient également être directement initialisées de cette manière :

valeur de chaîne tardive = "Init me" ;

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.

11.1.2 Bonnes pratiques


Nous tenons à souligner à nouveau le fait que l'utilisation de widgets avec état est tout à fait acceptable : le problème
réside dans l'utilisation de setState sans InheritedWidget associé. En plus de donner trop de responsabilités au
widget, cela ne vous donne pas de contrôle sur les reconstructions des widgets enfants, ce qui peut constituer un
gros problème de performances. A part ça :

• 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 passe­partout.

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

void _increment(int value) async { setState(()


{ _counter +=
valeur ;
});

attendre writeToFile(_counter);
}

... plutôt que d'appeler la fonction dans le programme de mise à jour d'état :

void _increment(int value) { setState(()


async { _counter += valeur;
wait writeToFile(_counter);

});
}

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

Référence complète Flutter 298


Machine Translated by Google

Chapitre 11. Gestion de l'État

• 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 sous­arbre, ainsi que la combinaison setState() + InheritedWidget, car elle produit une énorme quantité de code
passe­partout.

11.2 Passage de l'état avec le fournisseur


4
Le référentiel package a été créé par Rémi Rousselet et il est disponible dans le site officiel
de packages du fournisseur. Assurez­vous de l'installer correctement en ouvrant le pubspec et en ajoutant la
dépendance.

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:

// Le mixin est nécessaire car il contient la classe 'notifyListeners()' CounterModel avec


ChangeNotifier { int _counter = 0;

incrément vide() {

4https://pub.dev/packages/provider

Référence complète Flutter 299

Vous aimerez peut-être aussi