Flattern: Wie verwende ich ein geerbtes Widget richtig?


101

Was ist der richtige Weg, um ein InheritedWidget zu verwenden? Bisher habe ich verstanden, dass Sie damit die Möglichkeit haben, Daten über den Widget-Baum zu verbreiten. Wenn Sie als RootWidget angeben, ist es im Extremfall von allen Widgets im Baum auf allen Routen zugänglich, was in Ordnung ist, da ich mein ViewModel / Modell für meine Widgets irgendwie zugänglich machen muss, ohne auf Globals oder Singletons zurückgreifen zu müssen.

ABER InheritedWidget ist unveränderlich. Wie kann ich es aktualisieren? Und was noch wichtiger ist, wie werden meine Stateful Widgets ausgelöst, um ihre Teilbäume neu zu erstellen?

Leider ist die Dokumentation hier sehr unklar und nach vielen Diskussionen scheint niemand wirklich zu wissen, wie man sie richtig einsetzt.

Ich füge ein Zitat von Brian Egan hinzu:

Ja, ich sehe es als eine Möglichkeit, Daten im Baum zu verbreiten. Was ich aus den API-Dokumenten verwirrend finde:

"Wenn geerbte Widgets auf diese Weise referenziert werden, wird der Verbraucher neu erstellt, wenn das geerbte Widget selbst den Status ändert."

Als ich das zum ersten Mal las, dachte ich:

Ich könnte einige Daten in das InheritedWidget einfügen und später mutieren. Wenn diese Mutation auftritt, werden alle Widgets neu erstellt, die auf mein InheritedWidget verweisen. Was ich gefunden habe:

Um den Status eines InheritedWidget zu mutieren, müssen Sie ihn in ein StatefulWidget einschließen. Anschließend mutieren Sie den Status des StatefulWidget tatsächlich und geben diese Daten an das InheritedWidget weiter, das die Daten an alle untergeordneten Elemente weitergibt. In diesem Fall scheint jedoch der gesamte Baum unter dem StatefulWidget neu erstellt zu werden, nicht nur die Widgets, die auf das InheritedWidget verweisen. Ist das korrekt? Oder wird es irgendwie wissen, wie man die Widgets überspringt, die auf das InheritedWidget verweisen, wenn updateShouldNotify false zurückgibt?

Antworten:


107

Das Problem ergibt sich aus Ihrem Angebot, das falsch ist.

Wie Sie sagten, sind InheritedWidgets wie andere Widgets unveränderlich. Daher werden sie nicht aktualisiert . Sie werden neu geschaffen.

Die Sache ist: InheritedWidget ist nur ein einfaches Widget, das nur Daten enthält . Es gibt keine Logik der Aktualisierung oder was auch immer. Aber wie bei allen anderen Widgets ist es mit einem verbunden Element. Und rate was? Dieses Ding ist veränderlich und wird durch Flattern wiederverwendet, wann immer es möglich ist!

Das korrigierte Zitat wäre:

Wenn InheritedWidget auf diese Weise referenziert wird, wird der Consumer neu erstellt, wenn sich InheritedWidget, das einem InheritedElement zugeordnet ist, ändert.

Es wird viel darüber gesprochen, wie Widgets / Elemente / Renderbox zusammengefügt werden . Kurz gesagt, sie sind wie folgt (links ist Ihr typisches Widget, Mitte ist "Elemente" und rechts sind "Renderboxen"):

Geben Sie hier die Bildbeschreibung ein

Die Sache ist: Wenn Sie ein neues Widget instanziieren; Flattern wird es mit dem alten vergleichen. Verwenden Sie das "Element" wieder, das auf eine RenderBox verweist. Und mutieren Sie die RenderBox-Eigenschaften.


Okey, aber wie beantwortet das meine Frage?

Wenn Sie ein InheritedWidget instanziieren und dann aufrufen context.inheritedWidgetOfExactType(oder MyClass.ofwas im Grunde dasselbe ist); was impliziert ist, dass es auf die Elementmit Ihnen verbundenen hören wird InheritedWidget. Und wenn dadurch Elementein neues Widget erstellt wird, wird die Aktualisierung aller Widgets erzwungen, die die vorherige Methode aufgerufen haben.

Kurz gesagt, wenn Sie eine vorhandene InheritedWidgetdurch eine brandneue ersetzen ; Flattern wird sehen, dass es sich geändert hat. Und benachrichtigt die gebundenen Widgets über eine mögliche Änderung.

Wenn Sie alles verstanden haben, sollten Sie die Lösung bereits erraten haben:

Wickeln Sie Ihr InheritedWidgetInneres in ein StatefulWidget, das ein brandneues InheritedWidgetProdukt schafft, wenn sich etwas ändert!

Das Endergebnis im eigentlichen Code wäre:

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

Aber würde das Erstellen eines neuen InheritedWidget nicht den gesamten Baum neu erstellen?

Nein, das wird nicht unbedingt so sein. Da Ihr neues InheritedWidget möglicherweise genau dasselbe Kind wie zuvor haben kann. Und mit genau meine ich die gleiche Instanz. Widgets mit derselben Instanz wie zuvor werden nicht neu erstellt.

In den meisten Fällen (mit einem geerbten Widget im Stammverzeichnis Ihrer App) ist das geerbte Widget konstant . Also kein unnötiger Umbau.


1
Aber würde das Erstellen eines neuen InheritedWidget nicht den gesamten Baum neu erstellen? Warum dann die Notwendigkeit für Zuhörer?
Thomas

1
Für Ihren ersten Kommentar habe ich meiner Antwort einen dritten Teil hinzugefügt. Was die Langeweile angeht: Ich bin anderer Meinung. Ein Code-Snippet kann dies ziemlich einfach generieren. Der Zugriff auf die Daten ist so einfach wie das Aufrufen MyInherited.of(context).
Rémi Rousselet

3
Ich bin mir nicht sicher, ob Sie interessiert sind, habe aber das Beispiel mit dieser Technik aktualisiert: github.com/brianegan/flutter_architecture_samples/tree/master/… Jetzt sicher ein bisschen weniger Duplizierung! Wenn Sie weitere Vorschläge für diese Implementierung haben, würden Sie sich über eine Codeüberprüfung freuen, wenn Sie jemals ein paar Momente Zeit haben :) Versuchen Sie immer noch, den besten Weg zu finden, um diese logikübergreifende Plattform (Flutter und Web) zu teilen und sicherzustellen, dass sie testbar ist ( vor allem das asynchrone Zeug).
Brianegan

4
Da sich der updateShouldNotifyTest immer auf dieselbe MyInheritedStateInstanz bezieht , wird er nicht immer zurückgegeben false? Sicherlich erstellt die buildMethode MyInheritedStateneue _MyInheritedInstanzen, aber das dataFeld verweist immer auf thisNein? Ich habe Probleme ... Funktioniert, wenn ich nur harten Code habe true.
CDock

2
@cdock Ja mein schlechtes. Ich erinnere mich nicht, warum ich das getan habe, da es offensichtlich nicht funktioniert. Durch Bearbeiten auf true behoben, danke.
Rémi Rousselet

20

TL; DR

Verwenden Sie keine umfangreichen Berechnungen innerhalb der updateShouldNotify- Methode und verwenden Sie beim Erstellen eines Widgets const anstelle von new


Zunächst sollten wir verstehen, was ein Widget-, Element- und Render-Objekt ist.

  1. Renderobjekte sind das, was tatsächlich auf dem Bildschirm gerendert wird. Sie sind veränderlich , enthalten die Mal- und Layoutlogik. Der Renderbaum ist dem Document Object Model (DOM) im Web sehr ähnlich, und Sie können ein Renderobjekt in diesem Baum als DOM-Knoten betrachten
  2. Widget - beschreibt, was gerendert werden soll. Sie sind unveränderlich und billig. Wenn also ein Widget die Frage "Was?" (Deklarativer Ansatz) beantwortet, beantwortet ein Renderobjekt die Frage "Wie?" (Imperativer Ansatz). Eine Analogie aus dem Web ist ein "virtuelles DOM".
  3. Element / BuildContext - ist ein Proxy zwischen Widget- und Render- Objekten. Es enthält Informationen zur Position eines Widgets im Baum * und zum Aktualisieren des Renderobjekts, wenn ein entsprechendes Widget geändert wird.

Jetzt können wir uns mit InheritedWidget und der BuildContext-Methode inheritFromWidgetOfExactType befassen .

Als Beispiel empfehle ich, dieses Beispiel aus Flatters Dokumentation über InheritedWidget zu betrachten:

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 - nur ein Widget, das in unserem Fall eine wichtige Methode implementiert - updateShouldNotify . updateShouldNotify - eine Funktion, die einen Parameter oldWidget akzeptiert und einen booleschen Wert zurückgibt: true oder false.

Wie jedes Widget verfügt auch InheritedWidget über ein entsprechendes Element-Objekt. Es ist InheritedElement . InheritedElement ruft updateShouldNotify jedes Mal im Widget auf, wenn wir ein neues Widget erstellen (rufen Sie setState für einen Vorfahren auf). Wenn updateShouldNotify true zurückgibt , iteriert InheritedElement durch Abhängigkeiten (?) Und ruft die Methode didChangeDependencies auf.

Woher bekommt InheritedElement Abhängigkeiten ? Hier sollten wir uns die Methode inheritFromWidgetOfExactType ansehen .

inheritFromWidgetOfExactType - Diese in BuildContext definierte Methode und jedes Element implementiert die BuildContext-Schnittstelle (Element == BuildContext). Jedes Element hat also diese Methode.

Schauen wir uns den Code von inheritFromWidgetOfExactType an:

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

Hier versuchen wir, einen Vorfahren in _inheritedWidgets zu finden, der nach Typ zugeordnet ist. Wenn der Vorfahr gefunden wird, rufen wir inheritFromElement auf .

Der Code für inheritFromElement :

  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }
  1. Wir fügen Vorfahren als Abhängigkeit des aktuellen Elements hinzu (_dependencies.add (Vorfahren))
  2. Wir fügen den Abhängigkeiten der Vorfahren ein aktuelles Element hinzu (ancestor.updateDependencies (dies, Aspekt)).
  3. Wir geben das Widget des Vorfahren als Ergebnis von inheritFromWidgetOfExactType zurück (return ancestor.widget)

Jetzt wissen wir also, woher InheritedElement seine Abhängigkeiten bezieht.

Schauen wir uns nun die didChangeDependencies- Methode an. Jedes Element hat diese Methode:

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

Wie wir sehen können, markiert diese Methode nur ein Element als fehlerhaft und dieses Element sollte im nächsten Frame neu erstellt werden. Neu erstellen bedeutet , dass die Aufrufmethode auf dem entsprechenden Widget-Element basiert.

Aber was ist mit "Ganze Teilbäume werden neu erstellt, wenn ich InheritedWidget neu erstelle?". Hier sollten wir uns daran erinnern, dass Widgets unveränderlich sind und wenn Sie ein neues Widget erstellen, wird Flutter den Unterbaum neu erstellen. Wie können wir das beheben?

  1. Cache-Widgets von Hand (manuell)
  2. Verwenden Sie const, weil const die einzige Instanz von value / class erstellt

1
tolle Erklärung maksimr. Die Sache, die mich am meisten verwirrt, ist, dass, wenn der gesamte Unterbaum ohnehin neu erstellt wurde, als erbenWidget ersetzt wurde, was der Sinn von updateShouldNotify () ist.
Panda World

3

Aus den Dokumenten :

[BuildContext.inheritFromWidgetOfExactType] ruft das nächste Widget des angegebenen Typs ab, das der Typ einer konkreten InheritedWidget-Unterklasse sein muss, und registriert diesen Build-Kontext bei diesem Widget, sodass bei Änderung dieses Widgets (oder bei Einführung eines neuen Widgets dieses Typs) oder das Widget verschwindet), dieser Build-Kontext wird neu erstellt, damit er neue Werte von diesem Widget erhalten kann.

Dies wird normalerweise implizit von () statischen Methoden aufgerufen, z. B. Theme.of.

Wie das OP feststellte, InheritedWidgetändert sich eine Instanz nicht ... aber sie kann durch eine neue Instanz an derselben Stelle im Widget-Baum ersetzt werden. In diesem Fall müssen die registrierten Widgets möglicherweise neu erstellt werden. Die InheritedWidget.updateShouldNotifyMethode macht diese Bestimmung. (Siehe: Dokumente )

Wie könnte eine Instanz ersetzt werden? Eine InheritedWidgetInstanz kann in a enthalten sein StatefulWidget, wodurch eine alte Instanz durch eine neue Instanz ersetzt werden kann.


-1

InheritedWidget verwaltet die Daten des App zentralisiert und an das Kind weitergeben, wie wir hier Warenkorb Zahl speichern können wie erläutert hier :

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.