Flutter: comment utiliser correctement un widget hérité?

103

Quelle est la bonne façon d'utiliser un InheritedWidget? Jusqu'à présent, j'ai compris que cela vous donne la possibilité de propager des données dans l'arborescence des widgets. À l'extrême, si vous mettez est en tant que RootWidget, il sera accessible à partir de tous les widgets de l'arborescence sur toutes les routes, ce qui est bien parce que d'une manière ou d'une autre, je dois rendre mon ViewModel / Model accessible pour mes widgets sans avoir à recourir aux globaux ou aux singletons.

MAIS InheritedWidget est immuable, alors comment puis-je le mettre à jour? Et plus important encore, comment mes widgets avec état sont-ils déclenchés pour reconstruire leurs sous-arbres?

Malheureusement, la documentation est ici très peu claire et après discussion avec beaucoup de monde, personne ne semble vraiment savoir quelle est la bonne façon de l'utiliser.

J'ajoute une citation de Brian Egan:

Oui, je le vois comme un moyen de propager des données dans l'arborescence. Ce que je trouve déroutant, d'après la documentation de l'API:

"Les widgets hérités, lorsqu'ils sont référencés de cette manière, obligeront le consommateur à se reconstruire lorsque le widget hérité changera d'état."

Quand j'ai lu ceci pour la première fois, j'ai pensé:

Je pourrais insérer des données dans InheritedWidget et les muter plus tard. Lorsque cette mutation se produit, elle reconstruira tous les widgets qui référencent mon InheritedWidget Ce que j'ai trouvé:

Afin de muter l'état d'un InheritedWidget, vous devez l'envelopper dans un StatefulWidget.Vous modifiez ensuite l'état de StatefulWidget et transmettez ces données à InheritedWidget, qui transmet les données à tous ses enfants. Cependant, dans ce cas, il semble reconstruire l'arborescence entière sous le StatefulWidget, pas seulement les widgets qui font référence au InheritedWidget. Est-ce exact? Ou saura-t-il en quelque sorte ignorer les widgets qui font référence à InheritedWidget si updateShouldNotify renvoie false?

Thomas
la source

Réponses:

108

Le problème vient de votre devis, qui est incorrect.

Comme vous l'avez dit, les InheritedWidgets sont, comme les autres widgets, immuables. Par conséquent, ils ne mettent pas à jour . Ils sont créés à nouveau.

Le fait est que InheritedWidget est juste un simple widget qui ne fait rien d'autre que contenir des données . Il n'a aucune logique de mise à jour ou quoi que ce soit. Mais, comme tous les autres widgets, il est associé à un fichier Element. Et devine quoi? Cette chose est mutable et le flutter le réutilisera autant que possible!

Le devis corrigé serait:

InheritedWidget, lorsqu'il est référencé de cette manière, entraînera la reconstruction du consommateur lorsque InheritedWidget associé à un InheritedElement change.

Il y a un bon discours sur la façon dont les widgets / éléments / renderbox sont connectés ensemble . Mais en bref, ils sont comme ça (à gauche est votre widget typique, le milieu est 'éléments' et à droite sont 'boîtes de rendu'):

entrez la description de l'image ici

La chose est: lorsque vous instanciez un nouveau widget; flutter le comparera à l'ancien. Réutilisez c'est "Element", qui pointe vers un RenderBox. Et muter les propriétés RenderBox.


Okey, mais comment cela répond-il à ma question?

Lors de l'instanciation d'un InheritedWidget, puis de l'appel context.inheritedWidgetOfExactType(ou MyClass.ofqui est fondamentalement le même); ce qui est sous-entendu, c'est qu'il écoutera le Elementassocié à votre InheritedWidget. Et chaque fois que cela Elementobtient un nouveau widget, cela forcera l'actualisation de tous les widgets qui ont appelé la méthode précédente.

En bref, lorsque vous remplacez un existant InheritedWidgetpar un neuf; flutter verra que cela a changé. Et avertira les widgets liés d'une modification potentielle.

Si vous avez tout compris, vous devriez déjà avoir deviné la solution:

Enveloppez votre InheritedWidgetintérieur d'un StatefulWidgetqui créera un tout nouveau InheritedWidgetchaque fois que quelque chose a changé!

Le résultat final dans le code réel serait:

class MyInherited extends StatefulWidget {
  static MyInheritedData of(BuildContext context) =>
      context.inheritFromWidgetOfExactType(MyInheritedData) as MyInheritedData;

  const MyInherited({Key key, this.child}) : super(key: key);

  final Widget child;

  @override
  _MyInheritedState createState() => _MyInheritedState();
}

class _MyInheritedState extends State<MyInherited> {
  String myField;

  void onMyFieldChange(String newValue) {
    setState(() {
      myField = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MyInheritedData(
      myField: myField,
      onMyFieldChange: onMyFieldChange,
      child: widget.child,
    );
  }
}

class MyInheritedData extends InheritedWidget {
  final String myField;
  final ValueChanged<String> onMyFieldChange;

  MyInheritedData({
    Key key,
    this.myField,
    this.onMyFieldChange,
    Widget child,
  }) : super(key: key, child: child);

  static MyInheritedData of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<MyInheritedData>();
  }

  @override
  bool updateShouldNotify(MyInheritedData oldWidget) {
    return oldWidget.myField != myField ||
        oldWidget.onMyFieldChange != onMyFieldChange;
  }
}

Mais la création d'un nouveau InheritedWidget ne reconstruirait-elle pas l'arborescence entière?

Non, ce ne sera pas nécessairement le cas. Comme votre nouveau InheritedWidget peut potentiellement avoir exactement le même enfant qu'avant. Et par exact, je veux dire la même instance. Les widgets qui ont la même instance qu'avant ne se reconstruisent pas.

Et dans la plupart des cas (ayant un inheritedWidget à la racine de votre application), le widget hérité est constant . Donc pas de reconstruction inutile.

Rémi Rousselet
la source
1
Mais la création d'un nouveau InheritedWidget ne reconstruirait-elle pas l'arborescence entière? Pourquoi alors le besoin d'auditeurs?
Thomas
1
Pour votre premier commentaire, j'ai ajouté une troisième partie à ma réponse. Quant à être fastidieux: je ne suis pas d'accord. Un extrait de code peut générer cela assez facilement. Et accéder aux données est aussi simple que d'appeler MyInherited.of(context).
Rémi Rousselet
3
Je ne sais pas si cela vous intéresse, mais vous avez mis à jour le Sample avec cette technique: github.com/brianegan/flutter_architecture_samples/tree/master / ... Un peu moins de duplication maintenant, c'est sûr! Si vous avez d'autres suggestions pour cette implémentation, j'aimerais une révision du code si vous avez quelques instants à perdre :) J'essaie toujours de trouver le meilleur moyen de partager cette logique multiplateforme (Flutter et Web) et de vous assurer qu'elle est testable ( en particulier le truc asynchrone).
brianegan
4
Puisque le updateShouldNotifytest fait toujours référence à la même MyInheritedStateinstance, ne revient-il pas toujours false? Certes, la buildméthode MyInheritedStateconsiste à créer de nouvelles _MyInheritedinstances, mais le datachamp fait toujours référence à thisnon? J'ai des problèmes ... Fonctionne si je code juste en dur true.
cdock
2
@cdock Ouais mon mauvais. Je ne me souviens pas pourquoi j'ai fait cela car cela ne fonctionnera évidemment pas. Corrigé en éditant sur true, merci.
Rémi Rousselet
20

TL; DR

N'utilisez pas de calculs lourds dans la méthode updateShouldNotify et utilisez const au lieu de new lors de la création d'un widget


Tout d'abord, nous devons comprendre ce qu'est un objet Widget, Element et Render.

  1. Les objets de rendu sont ce qui est réellement rendu à l'écran. Ils sont mutables , contiennent la logique de peinture et de mise en page. L'arborescence de rendu est très similaire au modèle d'objet de document (DOM) sur le Web et vous pouvez regarder un objet de rendu comme un nœud DOM dans cette arborescence
  2. Widget - est une description de ce qui doit être rendu. Ils sont immuables et bon marché. Donc, si un Widget répond à la question "Quoi?" (Approche déclarative), alors un objet Render répond à la question "Comment?" (Approche impérative). Une analogie du Web est un "DOM virtuel".
  3. Element / BuildContext - est un proxy entre les objets Widget et Render . Il contient des informations sur la position d'un widget dans l'arborescence * et comment mettre à jour l'objet Render lorsqu'un widget correspondant est modifié.

Nous sommes maintenant prêts à plonger dans InheritedWidget et la méthode de BuildContext inheritFromWidgetOfExactType .

À titre d'exemple, je recommande de considérer cet exemple de la documentation de Flutter sur InheritedWidget:

class FrogColor extends InheritedWidget {
  const FrogColor({
    Key key,
    @required this.color,
    @required Widget child,
  })  : assert(color != null),
        assert(child != null),
        super(key: key, child: child);

  final Color color;

  static FrogColor of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(FrogColor);
  }

  @override
  bool updateShouldNotify(FrogColor old) {
    return color != old.color;
  }
}

InheritedWidget - juste un widget qui implémente dans notre cas une méthode importante - updateShouldNotify . updateShouldNotify - une fonction qui accepte un paramètre oldWidget et renvoie une valeur booléenne: true ou false.

Comme tout widget, InheritedWidget a un objet Element correspondant. Il s'agit de InheritedElement . InheritedElement appelle updateShouldNotify sur le widget chaque fois que nous construisons un nouveau widget (appelez setState sur un ancêtre). Lorsque updateShouldNotify retourne true, InheritedElement itère dans les dépendances (?) Et appelle la méthode didChangeDependencies dessus.

Où InheritedElement obtient les dépendances ? Ici, nous devrions examiner la méthode inheritFromWidgetOfExactType .

inheritFromWidgetOfExactType - Cette méthode définie dans BuildContext et chaque élément implémente l'interface BuildContext (Element == BuildContext). Donc, chaque élément a cette méthode.

Regardons le code de inheritFromWidgetOfExactType:

final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
if (ancestor != null) {
  assert(ancestor is InheritedElement);
  return inheritFromElement(ancestor, aspect: aspect);
}

Ici, nous essayons de trouver un ancêtre dans _inheritedWidgets mappé par type. Si l'ancêtre est trouvé, nous appelons alors inheritFromElement .

Le code pour inheritFromElement :

  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }
  1. Nous ajoutons l'ancêtre en tant que dépendance de l'élément courant (_dependencies.add (ancestor))
  2. Nous ajoutons l'élément courant aux dépendances de l'ancêtre (ancestor.updateDependencies (this, aspect))
  3. Nous retournons le widget de l'ancêtre comme résultat de inheritFromWidgetOfExactType (return ancestor.widget)

Nous savons maintenant où InheritedElement obtient ses dépendances.

Regardons maintenant la méthode didChangeDependencies . Chaque élément a cette méthode:

  void didChangeDependencies() {
    assert(_active); // otherwise markNeedsBuild is a no-op
    assert(_debugCheckOwnerBuildTargetExists('didChangeDependencies'));
    markNeedsBuild();
  }

Comme nous pouvons le voir, cette méthode marque simplement un élément comme sale et cet élément doit être reconstruit à l'image suivante. Reconstruire signifie appeler la méthode construite sur l'élément de widget correspondant.

Mais qu'en est-il de "La sous-arborescence entière se reconstruit quand je reconstruis InheritedWidget?". Ici, nous devons nous rappeler que les widgets sont immuables et que si vous créez un nouveau widget, Flutter reconstruira le sous-arbre. Comment pouvons-nous y remédier?

  1. Cachez les widgets à la main (manuellement)
  2. Utilisez const car const crée la seule instance de valeur / classe
maksimr
la source
1
grande explication maksimr. Ce qui me déroute le plus, c'est que si tout le sous-arbre est de toute façon reconstruit lors du remplacement de inheritedWidget, quel est l'intérêt de updateShouldNotify ()?
Panda World
3

À partir de la documentation :

[BuildContext.inheritFromWidgetOfExactType] obtient le widget le plus proche du type donné, qui doit être le type d'une sous-classe InheritedWidget concrète, et enregistre ce contexte de construction avec ce widget de sorte que lorsque ce widget change (ou qu'un nouveau widget de ce type est introduit, ou le widget disparaît), ce contexte de construction est reconstruit afin qu'il puisse obtenir de nouvelles valeurs à partir de ce widget.

Ceci est généralement appelé implicitement à partir de méthodes statiques of (), par exemple Theme.of.

Comme l'a noté l'OP, une InheritedWidgetinstance ne change pas ... mais elle peut être remplacée par une nouvelle instance au même emplacement dans l'arborescence des widgets. Lorsque cela se produit, il est possible que les widgets enregistrés doivent être reconstruits. La InheritedWidget.updateShouldNotifyméthode effectue cette détermination. (Voir: docs )

Alors, comment une instance pourrait-elle être remplacée? Une InheritedWidgetinstance peut être contenue par a StatefulWidget, qui peut remplacer une ancienne instance par une nouvelle instance.

kkurian
la source
-1

InheritedWidget gère les données centralisées de l'application et les transmet à l'enfant, comme nous pouvons stocker ici le nombre de cartons comme expliqué ici :

Sunil
la source