Cet article couvre les notions importantes de Widget, State, Context et InheritedWidget dans les applications Flutter. Une attention particulière est portée sur le InheritedWidget qui est l’un des widgets les plus importants et le moins documenté.

Difficulté: Débutant

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)

Cet article est également disponible (en Anglais) sur 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:

@override
Widget build(BuildContext){
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              'You have pushed the button this many times:',
            ),
            new Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: new Icon(Icons.add),
      ),
    );
}

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

state diagram basic

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

state diagram basic context

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

class MyStatelessWidget extends StatelessWidget {

	MyStatelessWidget({
		Key key,
		this.parameter,
	}): super(key:key);

	final parameter;

	@override
	Widget build(BuildContext context){
		return new ...
	}
}

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.

class MyStatefulWidget extends StatefulWidget {

	MyStatefulWidget({
		Key key,
		this.parameter,
	}): super(key: key);
	
	final parameter;
	
	@override
	_MyStatefulWidgetState createState() => new _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {

	@override
	void initState(){
		super.initState();
		
		// Additional initialization of the State
	}
	
	@override
	void didChangeDependencies(){
		super.didChangeDependencies();
		
		// Additional code
	}
	
	@override
	void dispose(){
		// Additional disposal code
		
		super.dispose();
	}
	
	@override
	Widget build(BuildContext context){
		return new ...
	}
}

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

state diagram

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

class MyStatefulWidget extends StatefulWidget {
    MyStatefulWidget({
		Key key,
		this.color,
	}): super(key: key);
	
	final Color color;

	@override
	_MyStatefulWidgetState createState() => new _MyStatefulWidgetState();
}

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

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
    ...
	@override
	Widget build(BuildContext context){
	    ...
	}
}

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

    GlobalKey myKey = new GlobalKey();
    ...
    @override
    Widget build(BuildContext context){
        return new MyWidget(
            key: myKey
        );
    }

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.

    ...
    GlobalKey<MyStatefulWidgetState> myWidgetStateKey = new GlobalKey<MyStatefulWidgetState>();
    ...
    @override
    Widget build(BuildContext context){
        return new MyStatefulWidget(
            key: myWidgetStateKey,
            color: Colors.blue,
        );
    }

Une fois identifié, un Widget parent peut accéder au State de son enfant via:

myWidgetStateKey.currentState

Considérons un exemple basique qui montre un SnackBar lorsque l’utilisateur appuie sur un bouton.

Comme le SnackBar 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 SnackBar.

    class _MyScreenState extends State<MyScreen> {
        /// the unique identity of the Scaffold
        final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();

        @override
        Widget build(BuildContext context){
            return new Scaffold(
                key: _scaffoldKey,
                appBar: new AppBar(
                    title: new Text('My Screen'),
                ),
                body: new Center(
                    new RaiseButton(
                        child: new Text('Hit me'),
                        onPressed: (){
                            _scaffoldKey.currentState.showSnackBar(
                                new SnackBar(
                                    content: new Text('This is the Snackbar...'),
                                )
                            );
                        }
                    ),
                ),
            );
        }
    }

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.

state child get state

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:

class MyExposingWidget extends StatefulWidget {

   MyExposingWidgetState myState;
	
   @override
   MyExposingWidgetState createState(){
      myState = new MyExposingWidgetState();
      return myState;
   }
}

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:

class MyExposingWidgetState extends State<MyExposingWidget>{
   Color _color;
	
   Color get color => _color;
   ...
}

le “Widget intéressé par l’obtention du State” (en bleu) doit obtenir une référence du State

class MyChildWidget extends StatelessWidget {
   @override
   Widget build(BuildContext context){
      final MyExposingWidget widget = context.ancestorWidgetOfExactType(MyExposingWidget);
      final MyExposingWidgetState state = widget?.myState;
		
      return new Container(
         color: state == null ? Colors.blue : state.color,
      );
   }
}

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:

class MyInheritedWidget extends InheritedWidget {
   MyInheritedWidget({
      Key key,
      @required Widget child,
      this.data,
   }): super(key: key, child: child);
	
   final data;
	
   static MyInheritedWidget of(BuildContext context) {
      return context.inheritFromWidgetOfExactType(MyInheritedWidget);
   }

   @override
   bool updateShouldNotify(MyInheritedWidget oldWidget) => data != oldWidget.data;
}

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:

class MyParentWidget... {
   ...
   @override
   Widget build(BuildContext context){
      return new MyInheritedWidget(
         data: counter,
         child: new Row(
            children: <Widget>[
               ...
            ],
         ),
      );
   }
}

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:

class MyChildWidget... {
   ...
	
   @override
   Widget build(BuildContext context){
      final MyInheritedWidget inheritedWidget = MyInheritedWidget.of(context);
		
      ///
      /// From this moment, the widget can use the data, exposed by the MyInheritedWidget
      /// by calling:  inheritedWidget.data
      ///
      return new Container(
         color: inheritedWidget.data.color,
      );
   }
}

Comment faire interagir des Widgets entre eux?

Considérez le diagramme suivant qui montre une structure arborescente de widgets.

inheritedwidget tree

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:

class Item {
   String reference;

   Item(this.reference);
}

class _MyInherited extends InheritedWidget {
  _MyInherited({
    Key key,
    @required Widget child,
    @required this.data,
  }) : super(key: key, child: child);

  final MyInheritedWidgetState data;

  @override
  bool updateShouldNotify(_MyInherited oldWidget) {
    return true;
  }
}

class MyInheritedWidget extends StatefulWidget {
  MyInheritedWidget({
    Key key,
    this.child,
  }): super(key: key);

  final Widget child;

  @override
  MyInheritedWidgetState createState() => new MyInheritedWidgetState();

  static MyInheritedWidgetState of(BuildContext context){
    return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
  }
}

class MyInheritedWidgetState extends State<MyInheritedWidget>{
  /// List of Items
  List<Item> _items = <Item>[];

  /// Getter (number of items)
  int get itemsCount => _items.length;

  /// Helper method to add an Item
  void addItem(String reference){
    setState((){
      _items.add(new Item(reference));
    });
  }

  @override
  Widget build(BuildContext context){
    return new _MyInherited(
      data: this,
      child: widget.child,
    );
  }
}

class MyTree extends StatefulWidget {
  @override
  _MyTreeState createState() => new _MyTreeState();
}

class _MyTreeState extends State<MyTree> {
  @override
  Widget build(BuildContext context) {
    return new MyInheritedWidget(
      child: new Scaffold(
        appBar: new AppBar(
          title: new Text('Title'),
        ),
        body: new Column(
          children: <Widget>[
            new WidgetA(),
            new Container(
              child: new Row(
                children: <Widget>[
                  new Icon(Icons.shopping_cart),
                  new WidgetB(),
                  new WidgetC(),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class WidgetA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final MyInheritedWidgetState state = MyInheritedWidget.of(context);
    return new Container(
      child: new RaisedButton(
        child: new Text('Add Item'),
        onPressed: () {
          state.addItem('new item');
        },
      ),
    );
  }
}

class WidgetB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final MyInheritedWidgetState state = MyInheritedWidget.of(context);
    return new Text('${state.itemsCount}');
  }
}

class WidgetC extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Text('I am Widget C');
  }
}

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

  static MyInheritedWidgetState of(BuildContext context) {
	return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
  }

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 RaisedButton du ‘widget A’ est cliqué, le flux des opérations est le suivant (version simplifiée):

  1. Un appel est effectué à la méthode addItem de MyInheritedWidgetState
  2. La méthode MyInheritedWidgetState.addItem ajoute un nouvel élément à la collection List
  3. setState() est invoqué pour reconstruire le MyInheritedWidget
  4. Une nouvelle instance de _MyInherited est créée avec le nouveau contenu de la collection List
  5. _MyInherited enregistre le nouveau State qui est passé en argument (data)
  6. En tant que InheritedWidget, il vérifie s’il est nécessaire de notifier les widgets inscrits (la réponse est toujours positive)
  7. Il parcourt la liste complète des widgets inscrits (ici Widget A et Widget B) et leur demande de reconstruire
  8. 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.inheritFromWidgetOfExactType() 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:

  static MyInheritedWidgetState of([BuildContext context, bool rebuild = true]){
    return (rebuild ? context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited
                    : context.ancestorWidgetOfExactType(_MyInherited) as _MyInherited).data;
  }

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

class WidgetA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final MyInheritedWidgetState state = MyInheritedWidget.of(context, false);
    return new Container(
      child: new RaisedButton(
        child: new Text('Add Item'),
        onPressed: () {
          state.addItem('new item');
        },
      ),
    );
  }
}

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.