Avant-propos
Les notions de Widget, State et Context dans le monde de Flutter sont parmi les concepts les plus importants que chaque développeur Flutter doit comprendre pleinement.
Cependant, la documentation est vaste et ces concepts ne sont pas toujours clairement expliqués.
Je vais expliquer ces notions avec mes propres mots et raccourcis, sachant que cela risque de choquer certains puristes, mais le véritable objectif de cet article est de tenter de clarifier les sujets suivants:
- différence entre les widgets Stateful et Stateless
- Qu'est-ce qu'un contexte (context)
- Qu'est-ce qu'un État (State) et comment l'utiliser?
- relation entre un contexte et son objet d'état
- InheritedWidget et la façon de propager l'information dans un arbre de Widgets
- notion de reconstruction (rebuild)
This article is also available on Medium - Flutter Community.
Partie 1: Concepts
Notion de Widget
Dans le monde de Flutter, presque tout est un Widget.
Pensez à un Widget comme un composant visuel (ou un composant qui interagit avec l'aspect visuel d'une application).
Lorsque vous avez besoin de construire quelque chose qui soit directement ou indirectement en relation avec l'aspect visuel ou interaction, vous utilisez des Widgets.
Notion d'arborescence de Widgets
Les Widgets sont organisés sous forme d'arborescence (= tree).
Un widget qui contient d'autres widgets est appelé Widget parent (ou conteneur de Widgets). Les widgets contenus dans un Widget parent sont appelés Widgets enfants.
Illustrons ceci avec l'application de base qui est automatiquement générée par Flutter. Voici le code simplifié, limité à la méthode build:
1
2
3 Widget build(BuildContext){
4 return Scaffold(
5 appBar: AppBar(
6 title: Text(widget.title),
7 ),
8 body: Center(
9 child: Column(
10 mainAxisAlignment: MainAxisAlignment.center,
11 children: <Widget>[
12 Text(
13 'You have pushed the button this many times:',
14 ),
15 Text(
16 '$_counter',
17 style: Theme.of(context).textTheme.display1,
18 ),
19 ],
20 ),
21 ),
22 floatingActionButton: FloatingActionButton(
23 onPressed: _incrementCounter,
24 tooltip: 'Increment',
25 child: const Icon(Icons.add),
26 ),
27 );
28 }
29
Si nous considérons maintenant cet exemple de base, nous obtenons la structure arborescente de Widgets suivante (limitée à la liste des Widgets présents dans le code):
Notion de Context
Une autre notion importante est le Context.
Un context n'est rien d'autre qu'une référence à l'emplacement d'un Widget dans l'arborescence de tous les Widgets qui sont construits.
Un context n'est lié qu'à UN SEUL widget.
Si un widget 'A' contient des widgets enfants, le context de widget 'A' deviendra le contexte parent de tous les enfants directs.
En lisant ceci, il est clair que les contextes sont liés et eux aussi, composent une arborescence de contextes (relation parent-enfants).
Si nous essayons maintenant d'illustrer la notion de Context dans le diagramme précédent, nous obtenons (toujours comme une vue très simplifiée) où chaque couleur représente un context (à l'exception de MyApp, qui est différent):
Context Visibility (Enoncé simplifié):
Quelque chose n'est visible que dans son propre contexte ou dans celui de ses parents.
De cette phrase nous pouvons déduire que d'un contexte enfant, il est facilement possible de trouver un Widget ancêtre (= parent).
Comme exemple, considérant le Scaffold > Center > Column > Text: context.findAncestorWidgetOfExactType(Scaffold) => renvoie le premier Scaffold qu'il trouve en remontant dans l'arborescence, à partir du contexte du Text.
A partir d'un contexte parent, il est également possible de trouver un widget descendant (= child) mais il est déconseillé de le faire (nous en discuterons plus tard).
Types de Widgets
Il existe 2 types de Widgets:
Stateless Widget
Certains de ces composants visuels ne dépendent pas d'autre chose que de leurs propres informations de configuration, qui sont fournies au moment de la construction (= build) par son parent direct.
En d'autres termes, ces Widgets n'auront à se préoccuper d'aucune variation, une fois créés.
Ces Widgets sont appelés Stateless Widgets.
Les exemples typiques de tels Widgets pourraient être Text, Row, Column, Container ... où pendant la construction, nous leur passons simplement quelques paramètres.
Les Paramètres peuvent être quelque chose comme une décoration, des dimensions, ou même d'autres widgets. Ce n'est pas important. La seule chose qui importe est que cette configuration, une fois appliquée, ne changera pas avant le prochain processus de construction (= build).
Un 'stateless widget' ne peut être rendu qu'une seule fois lorsque le widget est chargé / construit, ce qui signifie que nous ne pouvons pas le modifier en fonction des événements ou des actions de l'utilisateur
Cycle de vie d'un Stateless Widget
Voici une structure typique du code associé à un Stateless Widget.
Comme vous pouvez le voir, nous pouvons passer quelques paramètres supplémentaires à son constructeur. Cependant, gardez à l'esprit que ces paramètres NE changeront pas (mutera) à un stade ultérieur et ne seront utilisés que tels quels.
1
2 class MyStatelessWidget extends StatelessWidget {
3
4 MyStatelessWidget({
5 super.key,
6 required this.parameter,
7 });
8
9 final parameter;
10
11
12 Widget build(BuildContext context){
13 return ...
14 }
15 }
16
Même s'il existe une autre méthode pouvant être surchargée (= override) (createElement), cette dernière ne l'est que très rarement. La seule qui doivent être surchargée est build.
Le cycle de vie d'un Stateless widget est très simple:
- initialisation
- rendu/construction via build()
Stateful Widget
Certains autres Widgets géreront certaines données internes qui changeront pendant la durée de vie du Widget. Ces données deviennent donc dynamiques.
L'ensemble des données contenues et gérées par ce Widget et qui peuvent varier pendant la durée de vie de ce Widget s'appelle un State.
Ces Widgets sont appelés Stateful Widgets.
Un exemple d'un tel widget peut être une liste de cases à cocher que l'utilisateur peut sélectionner ou un bouton qui est désactivé en fonction d'une condition.
Notion de State
Un State définit la partie "comportementale" d'une instance d'un StatefulWidget.
Il contient les informations qui interagissent / interfèrent avec ce Widget en termes de:
- comportement
- rendu/layout
Toute modification appliquée à un State force le widget à se reconstruire (via build()).
Relation entre un State et un Context
Pour les Stateful widgets, un State est associé à un Context.
Cette association est permanente et l'objet State ne changera jamais son context.
Même si le context du widget peut être déplacé dans l'arborescence, le State restera associé à ce context.
Lorsqu'un State est associé à un Context, le State est considéré comme mounted.
HYPER IMPORTANT:
Comme un objet State est associé à un Context, cela signifie que l'objet State n'est PAS (directement) accessible par un autre context! (Nous en discuterons plus loin dans quelques instants).
Cycle de vie d'un Stateful Widget
Maintenant que les concepts de base ont été abordés, il est temps d'aller un peu plus loin ...
Voici une structure typique du code associé à un Stateful Widget.
Comme l'objectif principal de cet article est d'expliquer la notion de State en termes de données "variables", j'ometterai intentionnellement toute explication liée à certaines méthodes surchargeables (= override) d'un Stateful Widget, qui ne se rapportent pas spécifiquement à ce thème. Ces méthodes substituables sont didUpdateWidget, deactivate, reassemble. Celles-ci seront discutées dans un prochain article.
1
2 class MyStatefulWidget extends StatefulWidget {
3
4 MyStatefulWidget({
5 super.key,
6 required this.parameter,
7 });
8
9 final parameter;
10
11
12 _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
13 }
14
15 class _MyStatefulWidgetState extends State<MyStatefulWidget> {
16
17
18 void initState(){
19 super.initState();
20
21 // Additional initialization of the State
22 }
23
24
25 void didChangeDependencies(){
26 super.didChangeDependencies();
27
28 // Additional code
29 }
30
31
32 void dispose(){
33 // Additional disposal code
34
35 super.dispose();
36 }
37
38
39 Widget build(BuildContext context){
40 return ...
41 }
42 }
43
Le schéma suivant montre (en version simplifiée) la séquence d'actions / appels liés à la création d'un Widget Stateful.
Sur le côté droit du diagramme, vous remarquerez le statut interne de l'objet State pendant le flux. En outre, vous verrez également le moment où le Context est associé au State, et devient ainsi disponible (mounted).
Explications avec quelques détails supplémentaires:
initState()
La méthode initState() est la toute première méthode (après le constructeur) à être appelée une fois que l'objet State a été créé. Cette méthode doit être surchargée lorsque vous devez effectuer des initialisations supplémentaires. Les initialisations typiques sont liées aux animations, aux contrôleurs ...
Si vous surchargez cette méthode, vous devez appeler la méthode super.initState() normalement en premier.
Dans cette méthode, un Context est disponible mais vous ne pouvez pas l'utiliser réellement car le framework n'a pas encore complètement associé le State avec ce Context.
Une fois la méthode initState() terminée, l'objet State est maintenant initialisé et le Context disponible.
Cette méthode ne sera plus invoquée durant le reste de la durée de vie de cet objet State.
didChangeDependencies()
La méthode didChangeDependencies() est la deuxième méthode à être appelée.
A ce stade, comme le Context est disponible, vous pouvez l'utiliser.
Cette méthode est généralement surchargée si votre Widget est lié à un InheritedWidget et/ou si vous avez besoin d'initialiser certains listeners (basés sur le Context).
Notez que si votre widget est lié à un InheritedWidget, cette méthode sera appelée chaque fois que ce Widget sera reconstruit (à cause du InheritedWidget).
Si vous surchargez cette méthode, vous devez invoquer super.didChangeDependencies() en premier.
build()
La méthode build(BuildContext context) est appelée après didChangeDependencies() (et didUpdateWidget).
C'est l'endroit où vous construisez votre widget (et potentiellement n'importe quel sous-arbre de Widgets).
Cette méthode sera appelée chaque fois que votre objet State change (ou lorsqu'un InheritedWidget doit notifier des widgets "enregistrés") !!
Pour forcer une reconstruction, vous pouvez appeler la méthode setState(() {...}).
dispose()
La méthode dispose() est appelée lorsque le widget est supprimé définitivement.
Surchargez cette méthode si vous devez effectuer un nettoyage (par exemple, les listeners), puis appelez super.dispose() juste après.
Stateless ou Stateful Widget?
C'est une question que beaucoup de développeurs doivent se poser: mon Widget doit-il être Stateless ou Stateful?
Pour répondre à cette question, demandez-vous:
Dans la vie de mon widget, dois-je considérer une variable qui va changer et qui, une fois changée, forcera le widget à être reconstruit?
Si la réponse à cette question est oui, alors vous avez besoin d'un Widget Stateful, autrement, vous avez besoin d'un Widget Stateless.
Quelques exemples:
un widget pour afficher une liste de cases à cocher. Pour afficher les cases à cocher, vous devez considérer un tableau d'éléments. Chaque élément est un objet avec un titre et un statut. Si vous cliquez sur une case à cocher, l'item.status correspondant change de valeur;
Dans ce cas, vous devez utiliser un widget Stateful pour mémoriser l'état des éléments afin de pouvoir redessiner les cases à cocher.
un écran avec un formulaire. L'écran permet à l'utilisateur de remplir les widgets du formulaire et d'envoyer le formulaire au serveur.
Dans ce cas, sauf si vous devez valider le formulaire ou effectuer une autre action avant de l'envoyer, un widget Stateless peut suffire.
Un Widget Stateful est composé de 2 parties
Vous vous rappelez de la structure d'un Widget Stateful ? Il y a 2 parties:
La partie principale du Widget
1
2 class MyStatefulWidget extends StatefulWidget {
3 const MyStatefulWidget({
4 super.key,
5 required this.color,
6 });
7
8 final Color color;
9
10
11 _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
12 }
13
La première partie "MyStatefulWidget" est normalement la partie publique du Widget. Vous instanciez cette partie lorsque vous souhaitez l'ajouter à une arborescence de widgets.
Cette partie ne varie pas pendant la durée de vie du widget mais peut accepter des paramètres pouvant être utilisés par son instance State correspondante.
Notez que toute variable, définie au niveau de cette première partie du Widget, ne changera normalement PAS au cours de sa durée de vie.
La partie State
1
2class _MyStatefulWidgetState extends State<MyStatefulWidget> {
3 ...
4
5 Widget build(BuildContext context){
6 ...
7 }
8}
9
La deuxième partie "_MyStatefulWidgetState" est la partie qui varie pendant la durée de vie du widget et force cette instance spécifique du widget à être reconstruit à chaque fois qu'une modification est appliquée. Le caractère '_' au début du nom de la classe rend cette classe private et non accessible en dehors du fichier .dart.
Si vous devez faire référence à cette classe en dehors du fichier .dart, n'utilisez pas le préfixe '_'.
La classe _MyStatefulWidgetState peut accéder à n'importe quelle variable stockée dans MyStatefulWidget, en utilisant 'widget.{Nom de la variable}'.
Dans cet exemple: widget.color
Identité unique d'un Widget - Key
En Flutter, chaque widget est identifié de manière unique. Cette identité unique est définie par le framework au moment de la construction du Widget.
Cette identité unique correspond au paramètre facultatif Key. S'il est omis, Flutter générera une clé unique pour vous.
Dans certaines circonstances, vous devrez peut-être forcer cette key, afin que vous puissiez accéder à un widget par sa key.
Pour ce faire, vous pouvez utiliser l'une des classes suivantes: GlobalKey<T>, LocalKey, UniqueKey ou ObjectKey.
La classe GlobalKey garantit que la clé est unique sur l'ensemble de l'application.
Pour forcer une identité unique d'un widget:
1
2GlobalKey myKey = GlobalKey();
3...
4
5Widget build(BuildContext context){
6 return MyWidget(
7 key: myKey
8 );
9 }
10
Partie 2: Comment accéder au State?
Comme expliqué précédemment, un State est lié à UN Context et un Context est lié à une instance d'un Widget.
1. Le Widget lui-même
En théorie, le seul qui soit capable d'accéder à un State est le State lui-même.
Dans ce cas, il n'y a pas de difficulté. La classe Widget State accède à l'une de ses variables.
2. Un Widget enfant direct (= child Widget)
Parfois, un widget parent peut avoir besoin d'accéder au State de l'un de ses enfants directs pour effectuer des tâches spécifiques.
Dans ce cas, pour accéder au State des enfants directs, vous devez les connaître.
Le moyen le plus simple d'appeler quelqu'un est via son nom. Dans Flutter, chaque Widget a une identité unique, qui est déterminée au moment de construction (= build) par le framework.
Comme indiqué précédemment, vous pouvez forcer l'identité d'un widget en utilisant le paramètre key.
1
2...
3GlobalKey<MyStatefulWidgetState> myWidgetStateKey = GlobalKey<MyStatefulWidgetState>();
4...
5
6Widget build(BuildContext context){
7 return MyStatefulWidget(
8 key: myWidgetStateKey,
9 color: Colors.blue,
10 );
11 }
12
Une fois identifié, un Widget parent peut accéder au State de son enfant via:
myWidgetStateKey.currentState
Considérons un exemple basique qui ouvre un Drawer lorsque l'utilisateur appuie sur un bouton.
Comme le Drawer est un Widget enfant du Scaffold, il n'est pas directement accessible à aucun autre enfant faisant partie du body du Scaffold (souvenez-vous de la notion de contexte et sa structure hiérarchique / arborescente). Par conséquent, la seule façon d'y accéder est via le ScaffoldState, qui expose une méthode publique pour afficher le Drawer.
1
2class _MyScreenState extends State<MyScreen> {
3 /// the unique identity of the Scaffold
4 final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
5
6
7 Widget build(BuildContext context){
8 return Scaffold(
9 key: _scaffoldKey,
10 appBar: AppBar(
11 title: Text('My Screen'),
12 ),
13 drawer: Drawer(),
14 body: Center(
15 RaiseButton(
16 child: Text('Hit me'),
17 onPressed: (){
18 _scaffoldKey.currentState?.openDrawer();
19 }
20 ),
21 ),
22 );
23 }
24}
25
3. Un Widget parent
Supposons que vous ayez un widget qui appartient à un sous-arbre d'un autre widget, comme indiqué dans le diagramme suivant.
3 conditions doivent être remplies pour que cela soit possible:
le "Widget with State" (en rouge) doit exposer son State
Afin d'exposer (= rendre publique) son State, le Widget doit le mémoriser au moment de sa création, comme suit:
1
2class MyExposingWidget extends StatefulWidget {
3
4 late MyExposingWidgetState myState;
5
6
7 MyExposingWidgetState createState(){
8 myState = MyExposingWidgetState();
9 return myState;
10 }
11}
12
le "Widget State" doit exposer des getters/setters
Afin de permettre à un "étranger" de définir/obtenir une propriété du State, le Widget State doit en autoriser l'accès, via:
- un propriété publique (déconseillé)
- getter / setter
Exemple:
1
2class MyExposingWidgetState extends State<MyExposingWidget>{
3 Color _color;
4
5 Color get color => _color;
6 ...
7}
8
le "Widget intéressé par l'obtention du State" (en bleu) doit obtenir une référence du State
1
2class MyChildWidget extends StatelessWidget {
3
4 Widget build(BuildContext context){
5 final MyExposingWidget? widget = context.findAncestorWidgetOfExactType(MyExposingWidget);
6 final MyExposingWidgetState? state = widget?.myState;
7
8 return Container(
9 color: state == null ? Colors.blue : state.color,
10 );
11 }
12}
13
Cette solution est facile à implémenter, mais comment le widget enfant sait-il quand il a besoin de se reconstruire?
Avec cette solution, il ne le sait pas. Il faudra attendre une reconstruction de l'arborescence dans laquelle il se trouve pour actualiser son contenu, ce qui n'est pas très pratique.
La section suivante aborde la notion de Inherited Widget qui fournit une solution à ce problème.
InheritedWidget
En bref et avec des mots simples, le InheritedWidget permet de propager efficacement (et de partager) des informations dans un arbre de widgets.
Le InheritedWidget est un widget spécial, que vous mettez dans l'arborescence en tant que parent d'un autre sous-arbre. Tous les widgets faisant partie de ce sous-arbre pourront interagir avec les données qui sont exposées par ce InheritedWidget.
Les bases
Pour l'expliquer, considérons le morceau de code suivant:
1
2class MyInheritedWidget extends InheritedWidget {
3 MyInheritedWidget({
4 Key? key,
5 required Widget child,
6 this.data,
7 }): super(key: key, child: child);
8
9 final data;
10
11 static MyInheritedWidget? of(BuildContext context) {
12 return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
13 }
14
15
16 bool updateShouldNotify(MyInheritedWidget oldWidget) => data != oldWidget.data;
17}
18
Ce code définit un Widget, nommé "MyInheritedWidget", dont l'objectif est de "partager" des données et les rendre accessibles à tous les widgets, faisant partie de sa sous-arborescence.
Comme mentionné précédemment, un InheritedWidget doit être positionné en haut d'un arbre de widgets afin de pouvoir propager/partager certaines données; ceci explique le required Widget child qui est transmis au constructeur de base du InheritedWidget.
La méthode MyInheritedWidget of(BuildContext context), permet à tous les widgets enfants d'obtenir l'instance du MyInheritedWidget le plus proche qui contient le contexte de l'enfant (voir plus loin).
Enfin, la méthode surchargée updateShouldNotify est utilisée pour indiquer au InheritedWidget si les notifications devront être passées à tous les widgets enfants (enregistrés / inscrits) si une modification est appliquée au data (voir plus loin).
Par conséquent, nous devons le mettre au niveau d'un nœud d'arbre comme suit:
1
2class MyParentWidget... {
3 ...
4
5 Widget build(BuildContext context){
6 return MyInheritedWidget(
7 data: counter,
8 child: Row(
9 children: <Widget>[
10 ...
11 ],
12 ),
13 );
14 }
15}
16
Comment un enfant accède-t-il aux données du InheritedWidget?
Au moment de construire un enfant, ce dernier obtiendra une référence du InheritedWidget, comme suit:
1
2class MyChildWidget... {
3 ...
4
5
6 Widget build(BuildContext context){
7 final MyInheritedWidget? inheritedWidget = MyInheritedWidget.of(context);
8
9 ///
10 /// From this moment, the widget can use the data, exposed by the MyInheritedWidget
11 /// by calling: inheritedWidget.data
12 ///
13 return Container(
14 color: inheritedWidget?.data.color,
15 );
16 }
17}
18
Comment faire interagir des Widgets entre eux?
Considérez le diagramme suivant qui montre une structure arborescente de widgets.
Pour illustrer un type d'interaction, supposons ce qui suit:
- 'Widget A' est un bouton qui ajoute un article au panier;
- 'Widget B' est un texte qui affiche le nombre d'éléments dans le panier;
- 'Widget C' est à côté de Widget B et est un texte avec n'importe quel texte à l'intérieur;
- Nous voulons que le 'Widget B' affiche automatiquement le bon nombre d'articles dans le panier, dès que le 'Widget A' est pressé, mais nous ne voulons pas que 'Widget C' soit reconstruit
Le InheritedWidget est le Widget idéal pour réaliser cela!
Exemple par le code
Commençons par écrire le code et les explications suivront:
1
2class Item {
3 Item(this.reference);
4
5 String reference;
6 }
7
8 class _MyInherited extends InheritedWidget {
9 const _MyInherited({
10 Key? key,
11 required Widget child,
12 required this.data,
13 }) : super(key: key, child: child);
14
15 final MyInheritedWidgetState data;
16
17
18 bool updateShouldNotify(_MyInherited oldWidget) => true;
19 }
20
21 class MyInheritedWidget extends StatefulWidget {
22 const MyInheritedWidget({
23 super.key,
24 required this.child,
25 });
26
27 final Widget child;
28
29
30 MyInheritedWidgetState createState() => MyInheritedWidgetState();
31
32 static MyInheritedWidgetState? of(BuildContext context) {
33 return (context.dependOnInheritedWidgetOfExactType<_MyInherited>())
34 ?.data;
35 }
36 }
37
38 class MyInheritedWidgetState extends State<MyInheritedWidget> {
39 // List of Items
40 final List<Item> _items = <Item>[];
41
42 // Getter (number of items)
43 int get itemsCount => _items.length;
44
45 // Helper method to add an Item
46 void addItem(String reference) {
47 if (mounted) {
48 setState(() {
49 _items.add(Item(reference));
50 });
51 }
52 }
53
54
55 Widget build(BuildContext context) {
56 return _MyInherited(
57 data: this,
58 child: widget.child,
59 );
60 }
61 }
62
63 class MyTree extends StatefulWidget {
64 const MyTree({super.key});
65
66
67 _MyTreeState createState() => _MyTreeState();
68 }
69
70 class _MyTreeState extends State<MyTree> {
71
72 Widget build(BuildContext context) {
73 return MyInheritedWidget(
74 child: Scaffold(
75 appBar: AppBar(
76 title: const Text('Title'),
77 ),
78 body: Column(
79 children: <Widget>[
80 const WidgetA(),
81 Row(
82 children: const <Widget>[
83 Icon(Icons.shopping_cart),
84 WidgetB(),
85 WidgetC(),
86 ],
87 ),
88 ],
89 ),
90 ),
91 );
92 }
93 }
94
95 class WidgetA extends StatelessWidget {
96 const WidgetA({super.key});
97
98
99 Widget build(BuildContext context) {
100 final MyInheritedWidgetState? state = MyInheritedWidget.of(context);
101 return ElevatedButton(
102 child: const Text('Add Item'),
103 onPressed: () {
104 state?.addItem('new item');
105 },
106 );
107 }
108 }
109
110 class WidgetB extends StatelessWidget {
111 const WidgetB({super.key});
112
113
114 Widget build(BuildContext context) {
115 final MyInheritedWidgetState? state = MyInheritedWidget.of(context);
116 return Text('${state?.itemsCount}');
117 }
118 }
119
120 class WidgetC extends StatelessWidget {
121 const WidgetC({super.key});
122
123
124 Widget build(BuildContext context) {
125 return const Text('I am Widget C');
126 }
127 }
128
Explications
Dans cet exemple très basique,
- _MyInherited est un InheritedWidget qui est recréé à chaque fois que nous ajoutons un élément via un clic sur le bouton de 'Widget A'
- MyInheritedWidget est un widget avec un State qui contient la liste des éléments. Ce State est accessible via le static MyInheritedWidgetState of(BuildContext context)
- MyInheritedWidgetState expose un getter (itemsCount) et une méthode (addItem) afin qu'ils soient utilisables par les widgets qui font partie de l'arbre de widgets child
- Chaque fois que nous ajoutons un élément au State, le MyInheritedWidgetState est reconstruit
- La classe MyTree construit simplement un arbre de widgets, ayant le MyInheritedWidget comme parent de l'arbre
- WidgetA est un simple ElevatedButton qui, lorsqu'il est pressé, appelle la méthode addItem du MyInheritedWidget le plus proche
- WidgetB est un simple Text qui affiche le nombre d'éléments, présents au niveau du plus proche MyInheritedWidget
Comment tout cela fonctionne-il?
Enregistrement d'un widget pour des notifications ultérieures
Lorsqu'un widget enfant appelle le MyInheritedWidget.of(context), il appelle la méthode suivante de MyInheritedWidget en lui transmettant son propre context.
1
2static MyInheritedWidgetState? of(BuildContext context) {
3 return (context.dependOnInheritedWidgetOfExactType<_MyInherited>())
4 ?.data;
5 }
6
En interne, ce simple appel à la méthode statique effectue 2 choses:
- le widget appelant est automatiquement ajouté à la liste des "subscribers" qui seront reconstruits lorsqu'une modification est appliquée au InheritedWidget (ici _MyInherited)
- le data référencé au niveau du _MyInherited widget (MyInheritedWidgetState) est retourné au widget "appelant"
Flux
Puisque 'Widget A' et 'Widget B' ont souscrit auprès du InheritedWidget, si une modification est appliquée à _MyInherited, lorsque le ElevatedButton du 'widget A' est cliqué, le flux des opérations est le suivant (version simplifiée):
- Un appel est effectué à la méthode addItem de MyInheritedWidgetState
- La méthode MyInheritedWidgetState.addItem ajoute un nouvel élément à la collection List<Item>
- setState() est invoqué pour reconstruire le MyInheritedWidget
- Une nouvelle instance de _MyInherited est créée avec le nouveau contenu de la collection List<Item>
- _MyInherited enregistre le nouveau State qui est passé en argument (data)
- En tant que InheritedWidget, il vérifie s'il est nécessaire de notifier les widgets inscrits (la réponse est toujours positive)
- Il parcourt la liste complète des widgets inscrits (ici Widget A et Widget B) et leur demande de reconstruire
- Comme 'Wiget C' n'est pas inscrit, il n'est pas reconstruit.
Voilà, ça marche!
Cependant, 'Widget A' et 'Widget B' sont tous les deux reconstruits alors qu'il est inutile de reconstruire 'Wiget A' puisque rien n'a changé à son niveau.
Comment empêcher cela de se produire?
Empêcher la reconstruction de certains Widgets tout en continuant d'accéder au InheritedWidget
La raison pour laquelle 'Widget A' a également été reconstruit provient de la façon dont il accède à MyInheritedWidgetState.
Comme nous l'avons vu précédemment, le fait d'invoquer la méthode context.dependOnInheritedWidgetOfExactType() inscrit automatiquement le widget à la liste des ceux devant être reconstruits.
La solution pour empêcher cet inscription automatique tout en permettant au 'Widget A' d'accéder au MyInheritedWidgetState est de changer la méthode statique de MyInheritedWidget comme suit:
1
2static MyInheritedWidgetState of(BuildContext context, [bool rebuild = true]){
3 return (rebuild
4 ? context.dependOnInheritedWidgetOfExactType<_MyInherited>()
5 : context.findAncestorWidgetOfExactType<_MyInherited>())
6 ?.data;
7}
8
En ajoutant un paramètre booléen supplémentaire...
- Si le paramètre rebuild est vrai (par défaut), nous utilisons l'approche normale (et le Widget sera inscrit)
- Si le paramètre rebuild est faux, nous avons toujours accès aux données mais sans utiliser l'implémentation interne du InheritedWidget (donc pas d'inscription)
Donc, pour compléter la solution, nous avons aussi besoin de mettre légèrement à jour le code de 'Widget A' comme suit (nous ajoutons le paramètre 'false' supplémentaire):
1
2class WidgetA extends StatelessWidget {
3
4 Widget build(BuildContext context) {
5 final MyInheritedWidgetState? state = MyInheritedWidget.of(context, false);
6 return Container(
7 child: ElevatedButton(
8 child: cont Text('Add Item'),
9 onPressed: () {
10 state?.addItem('new item');
11 },
12 ),
13 );
14 }
15}
16
Voilà, Widget A n'est plus reconstruit quand on appuie dessus.
Note spéciale pour les Routes, Dialogs...
Les Context des Routes, Dialogs sont liés à l'Application.
Cela signifie que si un 'Screen A' demande d'afficher un autre 'Screen B' (par exemple en tant que popup), il n'y a aucune façon aisée de pouvoir lier les 2 Context des 2 écrans.
La seule façon pour 'Screen B' de connaître le context de 'Screen A' est de l'obtenir en tant que paramètre lorsque 'Screen A' fait appel à Navigator.of(context).push(....).
Liens intéressants
Conclusions
Il y a encore tant à dire sur ces sujets... surtout sur InheritedWidget.
Dans un prochain article, je présenterai la notion de Notifiers / Listeners qui est aussi très intéressante à utiliser dans le contexte du State et de la manière de transmettre des données.
Alors, restez à l'écoute et heureux codage.
Didier,