Flutter: Jak poprawnie używać dziedziczonego widżetu?

103

Jaki jest prawidłowy sposób korzystania z InheritedWidget? Jak dotąd zrozumiałem, że daje to szansę na propagowanie danych w drzewie widżetów. W skrajnym przypadku, jeśli umieścisz jako RootWidget, będzie on dostępny ze wszystkich widżetów w drzewie na wszystkich trasach, co jest w porządku, ponieważ w jakiś sposób muszę udostępnić mój ViewModel / Model dla moich widżetów bez konieczności uciekania się do globali lub singletonów.

ALE InheritedWidget jest niezmienny, więc jak mogę go zaktualizować? A co ważniejsze, w jaki sposób są uruchamiane moje Stateful Widgets, aby odbudować ich poddrzewa?

Niestety dokumentacja jest tutaj bardzo niejasna i po wielu dyskusjach wydaje się, że nikt tak naprawdę nie wie, jaki jest prawidłowy sposób jej użycia.

Dodaję cytat z Briana Egana:

Tak, uważam to za sposób na propagowanie danych w dół drzewa. To, co uważam za mylące, z dokumentacji API:

„Dziedziczone widżety przywoływane w ten sposób spowodują, że konsument będzie przebudowywał, gdy odziedziczony widżet sam zmieni stan”.

Kiedy po raz pierwszy to przeczytałem, pomyślałem:

Mógłbym umieścić niektóre dane w InheritedWidget i zmodyfikować je później. Kiedy ta mutacja nastąpi, odbuduje wszystkie widżety, które odwołują się do mojego InheritedWidget Co znalazłem:

Aby zmutować stan InheritedWidget, musisz opakować go w StatefulWidget. Następnie faktycznie dokonujesz mutacji stanu StatefulWidget i przekazujesz te dane do InheritedWidget, który przekazuje dane wszystkim swoim elementom podrzędnym. Jednak w tym przypadku wydaje się, że odbudowuje całe drzewo pod StatefulWidget, a nie tylko widżety, które odwołują się do InheritedWidget. Czy to jest poprawne? A może w jakiś sposób będzie wiedziało, jak pominąć widżety, które odwołują się do InheritedWidget, jeśli updateShouldNotify zwróci false?

Tomasz
źródło

Odpowiedzi:

108

Problem pochodzi z Twojej wyceny, która jest nieprawidłowa.

Jak powiedziałeś, InheritedWidgets są, podobnie jak inne widżety, niezmienne. Dlatego nie aktualizują się . Są tworzone od nowa.

Rzecz w tym, że: InheritedWidget to po prostu prosty widget, który nie robi nic poza przechowywaniem danych . Nie ma żadnej logiki aktualizacji ani w ogóle. Ale, podobnie jak inne widżety, jest powiązany z plikiem Element. I zgadnij co? Ta rzecz jest zmienna i flutter użyje jej ponownie, gdy tylko będzie to możliwe!

Poprawiony cytat to:

InheritedWidget, gdy odwołuje się w ten sposób, spowoduje, że konsument odbuduje, gdy InheritedWidget skojarzony z InheritedElement ulegnie zmianie.

Jest świetna rozmowa o tym, jak widżety / elementy / renderbox są łączone . Krótko mówiąc, wyglądają tak (po lewej stronie znajduje się typowy widżet, środek to „elementy”, a po prawej „pola renderowania”):

wprowadź opis obrazu tutaj

Rzecz w tym, że: kiedy tworzysz instancję nowego widżetu; flutter porówna go ze starym. Użyj ponownie tego „Elementu”, który wskazuje na RenderBox. I mutować właściwości RenderBox.


OK, ale jak to odpowiada na moje pytanie?

Podczas tworzenia wystąpienia InheritedWidget, a następnie wywoływania context.inheritedWidgetOfExactType(lub MyClass.ofzasadniczo to samo); sugeruje się, że będzie słuchać plików Elementpowiązanych z twoją InheritedWidget. Za każdym razem, gdy Elementotrzyma nowy widżet, wymusi odświeżenie wszystkich widżetów, które wywołały poprzednią metodę.

Krótko mówiąc, kiedy wymieniasz istniejący InheritedWidgetna zupełnie nowy; flutter zobaczy, że się zmieniło. I powiadomi powiązane widżety o potencjalnej modyfikacji.

Jeśli wszystko zrozumiałeś, powinieneś już odgadnąć rozwiązanie:

Owiń swoje InheritedWidgetwnętrze StatefulWidget, które stworzy zupełnie nowe, InheritedWidgetgdy coś się zmieni!

Końcowy wynik w rzeczywistym kodzie wyglądałby tak:

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

Ale czy utworzenie nowego InheritedWidget nie odbudowałoby całego drzewa?

Nie, niekoniecznie. Ponieważ Twój nowy InheritedWidget może potencjalnie mieć dokładnie to samo dziecko, co wcześniej. Dokładnie mam na myśli ten sam przypadek. Widżety, które mają tę samą instancję, co wcześniej, nie są odbudowywane.

W większości sytuacji (posiadanie odziedziczonego widżetu w katalogu głównym aplikacji) dziedziczony widżet jest stały . Więc nie ma niepotrzebnej przebudowy.

Rémi Rousselet
źródło
1
Ale czy utworzenie nowego InheritedWidget nie odbudowałoby całego drzewa? Skąd zatem potrzeba słuchaczy?
Thomas
1
Jako pierwszy komentarz dodałem trzecią część do mojej odpowiedzi. A jeśli chodzi o nudę: nie zgadzam się. Fragment kodu może to dość łatwo wygenerować. Dostęp do danych jest tak prosty, jak dzwonienie MyInherited.of(context).
Rémi Rousselet
3
Nie jestem pewien, czy jesteś zainteresowany, ale zaktualizowałem Sample za pomocą tej techniki: github.com/brianegan/flutter_architecture_samples/tree/master/ ... Na pewno teraz trochę mniej duplikatów! Jeśli masz jakieś inne sugestie dotyczące tej implementacji, z chęcią przejrzyj kod, jeśli będziesz mieć kilka chwil do stracenia :) Wciąż próbuję znaleźć najlepszy sposób na udostępnienie tej logiki między platformami (Flutter i Web) i upewnienie się, że można ją przetestować ( zwłaszcza rzeczy asynchroniczne).
brianegan
4
Ponieważ updateShouldNotifytest zawsze odnosi się do tej samej MyInheritedStateinstancji, czy nie zawsze zwróci false? Z pewnością buildmetoda MyInheritedStatepolega na tworzeniu nowych _MyInheritedinstancji, ale datapole zawsze odwołuje się do thisnie? Mam problemy ... Działa, jeśli tylko napiszę kod true.
cdock
2
@cdock Yeah my bad. Nie pamiętam, dlaczego to zrobiłem, ponieważ to oczywiście nie zadziała. Naprawione przez edycję do true, dzięki.
Rémi Rousselet
20

TL; DR

Nie używaj ciężkich obliczeń w metodzie updateShouldNotify i używaj const zamiast new podczas tworzenia widgetu


Przede wszystkim powinniśmy zrozumieć, czym są obiekty Widget, Element i Render.

  1. Obiekty renderowane są faktycznie renderowane na ekranie. Są zmienne , zawierają logikę malowania i układu. Drzewo renderowania jest bardzo podobne do modelu obiektów dokumentu (DOM) w Internecie i możesz spojrzeć na renderowany obiekt jako węzeł DOM w tym drzewie
  2. Widget - to opis tego, co powinno być renderowane. Są niezmienne i tanie. Jeśli więc Widget odpowiada na pytanie „Co?” (Podejście deklaratywne), wówczas obiekt Render odpowiada na pytanie „Jak?” (Podejście rozkazujące). Analogią z internetu jest „wirtualny DOM”.
  3. Element / BuildContext - jest proxy między obiektami Widget i Render . Zawiera informacje o pozycji widżetu w drzewie * oraz o tym, jak zaktualizować obiekt Render w przypadku zmiany odpowiedniego widżetu.

Teraz jesteśmy gotowi do zanurzenia się w InheritedWidget i metodzie BuildContext inheritFromWidgetOfExactType .

Jako przykład, który polecam, rozważmy następujący przykład z dokumentacji Fluttera na temat 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 - po prostu widżet, który implementuje w naszym przypadku jedną ważną metodę - updateShouldNotify . updateShouldNotify - funkcja, która przyjmuje jeden parametr oldWidget i zwraca wartość logiczną: true lub false.

Jak każdy widżet, InheritedWidget ma odpowiedni obiekt Element. To jest InheritedElement . InheritedElement wywołuje updateShouldNotify na widgecie za każdym razem, gdy tworzymy nowy widget (wywołanie setState na przodku). Gdy updateShouldNotify zwraca wartość true, InheritedElement wykonuje iterację przez zależności (?) I wywołuje metodę didChangeDependencies .

Gdzie InheritedElement pobiera zależności ? Tutaj powinniśmy spojrzeć na metodę inheritFromWidgetOfExactType .

inheritFromWidgetOfExactType - ta metoda zdefiniowana w BuildContext i każdy element implementuje interfejs BuildContext (Element == BuildContext). Więc każdy element ma tę metodę.

Spójrzmy na kod inheritFromWidgetOfExactType:

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

Tutaj próbujemy znaleźć przodka w _inheritedWidgets zmapowanych według typu. Jeśli przodek zostanie znaleziony, wywołujemy inheritFromElement .

Kod inheritFromElement :

  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }
  1. Dodajemy przodka jako zależność bieżącego elementu (_dependencies.add (ancestor))
  2. Dodajemy bieżący element do zależności przodka (ancestor.updateDependencies (this, aspekt))
  3. Zwracamy widżet przodka w wyniku inheritFromWidgetOfExactType (return ancestor.widget)

Więc teraz wiemy, skąd InheritedElement pobiera swoje zależności.

Spójrzmy teraz na metodę didChangeDependencies . Każdy element ma tę metodę:

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

Jak widzimy ta metoda po prostu zaznacza element jako brudny i ten element należy odbudować w następnej klatce. Odbudować metody środki połączenia build na coresponding widget elementu.

Ale co z „Przebudowaniem całego poddrzewa, gdy odbuduję InheritedWidget?”. Tutaj powinniśmy pamiętać, że widżety są niezmienne i jeśli utworzysz nowy widget, Flutter odbuduje poddrzewo. Jak możemy to naprawić?

  1. Buforuj widżety ręcznie (ręcznie)
  2. Użyj const, ponieważ const tworzy jedyne wystąpienie wartości / klasy
maksimr
źródło
1
wielkie wyjaśnienie maksimr. Najbardziej wprawia mnie w zakłopotanie to, że jeśli całe poddrzewo i tak zostało odbudowane po zastąpieniu inheritedWidget, jaki jest cel updateShouldNotify ()?
Panda World
3

Z dokumentów :

[BuildContext.inheritFromWidgetOfExactType] uzyskuje najbliższy widżet danego typu, który musi być typem konkretnej podklasy InheritedWidget, i rejestruje ten kontekst kompilacji z tym widżetem w taki sposób, że gdy ten widget się zmieni (lub zostanie wprowadzony nowy widget tego typu, lub widżet zniknie), ten kontekst kompilacji zostanie przebudowany, aby mógł uzyskać nowe wartości z tego widgetu.

Jest to zwykle wywoływane niejawnie z metod statycznych of (), np. Theme.of.

Jak zauważył OP, InheritedWidgetinstancja się nie zmienia ... ale można ją zastąpić nową instancją w tym samym miejscu w drzewie widżetów. W takim przypadku możliwe jest, że zarejestrowane widgety będą wymagały odbudowy. InheritedWidget.updateShouldNotifyMetoda czyni tę determinację. (Zobacz: dokumentacja )

Jak więc można wymienić instancję? InheritedWidgetPrzykład może być zawarty przez StatefulWidget, które mogą zastąpić stary wystąpienie nową np.

kkurian
źródło
-1

InheritedWidget zarządza scentralizowanymi danymi aplikacji i przekazuje je dziecku, tak jak możemy przechowywać tutaj liczbę koszyków, jak wyjaśniono tutaj :

Sunil
źródło