ChatGPT解决这个技术问题 Extra ChatGPT

Flutter: How to correctly use an Inherited Widget?

What is the correct way to use an InheritedWidget? So far I understood that it gives you the chance to propagate data down the Widget tree. In extreme if you put is as RootWidget it will be accessible from all Widgets in the tree on all Routes, which is fine because somehow I have to make my ViewModel/Model accessible for my Widgets without having to resort to globals or Singletons.

BUT InheritedWidget is immutable, so how can I update it? And more important how are my Stateful Widgets triggered to rebuild their subtrees?

Unfortunately the documentation is here very unclear and after discussion with a lot nobody seems really to know what the correct way of using it.

I add a quote from Brian Egan:

Yes, I see it as a way to propagate data down the tree. What I find confusing, from the API docs: "Inherited widgets, when referenced in this way, will cause the consumer to rebuild when the inherited widget itself changes state." When I first read this, I thought: I could stuff some data in the InheritedWidget and mutate it later. When that mutation happens, it will rebuild all the Widgets that reference my InheritedWidget What I found: In order to mutate the State of an InheritedWidget, you need to wrap it in a StatefulWidget You then actually mutate the state of the StatefulWidget and pass this data down to the InheritedWidget, which hands the data down to all of it's children. However, in that case, it seems to rebuild the entire tree underneath the StatefulWidget, not just the Widgets that reference the InheritedWidget. Is that correct? Or will it somehow know how to skip the Widgets that reference the InheritedWidget if updateShouldNotify returns false?

Great question! Thanks for asking.

S
Shubhamhackz

The problem comes from your quote, which is incorrect.

As you said, InheritedWidgets are, like other widgets, immutable. Therefore they don't update. They are created anew.

The thing is: InheritedWidget is just a simple widget that does nothing but holding data. It doesn't have any logic of update or whatsoever. But, like any other widgets, it's associated with an Element. And guess what? This thing is mutable and flutter will reuse it whenever possible!

The corrected quote would be :

InheritedWidget, when referenced in this way, will cause the consumer to rebuild when InheritedWidget associated to an InheritedElement changes.

There's a great talk about how widgets/elements/renderbox are pluged together. But in short, they are like this (left is your typical widget, middle is 'elements', and right are 'render boxes') :

https://i.stack.imgur.com/ZhPdS.png

The thing is: When you instantiate a new widget; flutter will compare it to the old one. Reuse it's "Element", which points to a RenderBox. And mutate the RenderBox properties.

Okey, but how does this answer my question ?

When instantiating an InheritedWidget, and then calling context.inheritedWidgetOfExactType (or MyClass.of which is basically the same) ; what's implied is that it will listen to the Element associated with your InheritedWidget. And whenever that Element gets a new widget, it will force the refresh of any widgets that called the previous method.

In short, when you replace an existing InheritedWidget with a brand new one; flutter will see that it changed. And will notify the bound widgets of a potential modification.

If you understood everything, you should have already guessed the solution :

Wrap your InheritedWidget inside a StatefulWidget that will create a brand new InheritedWidget whenever something changed!

The end result in the actual code would be :

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

But wouldn't creating a new InheritedWidget rebuild the whole tree ?

No, it won't necessarily. As your new InheritedWidget can potentially have the exact same child as before. And by exact, I mean the same instance. Widgets who have the same instance they had before don't rebuild.

And in the most situation (Having an inheritedWidget at the root of your app), the inherited widget is constant. So no unneeded rebuild.


But wouldn't creating a new InheritedWidget rebuild the whole tree? Why then the need for Listeners?
For your first comment, I added a third part to my answer. As for being tedious : I disagree. A code snippet can generate this fairly easily. And accessing the data is as simple as calling MyInherited.of(context).
Not sure if you're interested, but got the Sample updated with this technique: github.com/brianegan/flutter_architecture_samples/tree/master/… A bit less duplication now for sure! If you have any other suggestions for that implementation, would love a code review if you ever have a few moments to spare :) Still trying to figure out the best way to share this logic cross platform (Flutter and Web) and ensure it's testable (especially the async stuff).
Since the updateShouldNotify test is always referring to the same MyInheritedState instance, won't it always return false? Certainly the build method of MyInheritedState is creating new _MyInherited instances, but the data field always references this no? I'm having issues... Works if I just hard code true.
@cdock Yeah my bad. Don't remember why I did that as it obviously won't work. Fixed by editing to true, thanks.
T
TheIT

TL;DR

Don't use heavy computation inside updateShouldNotify method and use const instead of new when creating a widget

First of all, we should understand what is a Widget, Element and Render objects.

Render objects are what is actually rendered on the screen. They are mutable, contain the painting and layout logic. The Render tree is very similar to the Document Object Model(DOM) in the web and you can look at a render object as a DOM node in this tree Widget - is a description of what should be rendered. They are immutable and cheap. So if a Widget answers the question "What?"(Declarative approach) then a Render object answer the question "How?"(Imperative approach). An analogy from the web is a "Virtual DOM". Element/BuildContext - is a proxy between Widget and Render objects. It contains information about the position of a widget in the tree* and how to update the Render object when a corresponding widget is changed.

Now we are ready to dive into InheritedWidget and BuildContext's method inheritFromWidgetOfExactType.

As an example I recommend we consider this example from Flutter's documentation about 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 - just a widget which implements in our case one important method - updateShouldNotify. updateShouldNotify - a function which accepts one parameter oldWidget and returns a boolean value: true or false.

Like any widget, InheritedWidget has a corresponding Element object. It is InheritedElement. InheritedElement call updateShouldNotify on the widget every time we build a new widget(call setState on an ancestor). When updateShouldNotify returns true InheritedElement iterates through dependencies(?) and call method didChangeDependencies on it.

Where InheritedElement gets dependencies? Here we should look at inheritFromWidgetOfExactType method.

inheritFromWidgetOfExactType - This method defined in BuildContext and every Element implements BuildContext interface (Element == BuildContext). So every Element has this method.

Lets look at the code of inheritFromWidgetOfExactType:

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

Here we try to find an ancestor in _inheritedWidgets mapped by type. If the ancestor is found, we then call inheritFromElement.

The code for inheritFromElement:

  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }

We add ancestor as a dependency of the current element (_dependencies.add(ancestor)) We add current element to ancestor's dependencies (ancestor.updateDependencies(this, aspect)) We return ancestor's widget as result of inheritFromWidgetOfExactType (return ancestor.widget)

So now we know where InheritedElement gets its dependencies.

Now lets look at didChangeDependencies method. Every Element has this method:

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

As we can see this method just marks an element as dirty and this element should be rebuilt on next frame. Rebuild means call method build on the coresponding widget element.

But what about "Whole sub-tree rebuilds when I rebuild InheritedWidget?". Here we should remember that Widgets are immutable and if you create new widget Flutter will rebuild the sub-tree. How can we fix it?

Cache widgets by hands(manually) Use const because const create the only one instance of value/class


great explanation maksimr. The thing confuse me the most is that if the whole sub tree rebuilt anyway when inheritedWidget got replaced, what is the point of updateShouldNotify()?
So here the Inherited widget can update its listener if value change and that's exactly provider widget do so what's difference between them . .correct me if i'm wrong
Where does the clean up happen? i.e. remove all the dependencies from the HashSet's when the widget is removed?
D
DarkNeuron

From the docs:

[BuildContext.dependOnInheritedWidgetOfExactType] obtains the nearest widget of the given type, which must be the type of a concrete InheritedWidget subclass, and registers this build context with that widget such that when that widget changes (or a new widget of that type is introduced, or the widget goes away), this build context is rebuilt so that it can obtain new values from that widget. This is typically called implicitly from of() static methods, e.g. Theme.of.

As the OP noted, an InheritedWidget instance does not change... but it can be replaced with a new instance at the same location in the widget tree. When that happens it is possible that the registered widgets need to be rebuilt. The InheritedWidget.updateShouldNotify method makes this determination. (See: docs)

So how might an instance be replaced? An InheritedWidget instance may be contained by a StatefulWidget, which may replace an old instance with a new instance.


S
Sunil

InheritedWidget manages centralized data of the app and pass it to the child, Like we can store here cart count as explained here:


关注公众号,不定期副业成功案例分享
Follow WeChat

Success story sharing

Want to stay one step ahead of the latest teleworks?

Subscribe Now