Flutter: How to correctly use an Inherited Widget?
Asked Answered
R

4

184

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?

Remonstrance answered 26/3, 2018 at 12:47 Comment(0)
E
173

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

enter image description here

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.

Erikerika answered 26/3, 2018 at 13:20 Comment(25)
But wouldn't creating a new InheritedWidget rebuild the whole tree? Why then the need for Listeners?Remonstrance
Ok you could modify the state of Statefull Widgets down the tree I guess. But still it seems pretty tedious to propagate statechanges this way.Remonstrance
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).Bonacci
To give it the same Instance as child I would have to save the child before recreating the InheritedWidgetRemonstrance
>And in most situation (Having an inheritedWidget at the root of your app), the inherited widget is constant. So no unneeded rebuild < But how do you then change it's values? IRemonstrance
@RémiRousselet Thanks for the great explanation -- explains why I was seeing rebuilds each time (I was re-creating the child on each rebuild). I'll test again with a constant child. Do you happen to have any code generators published? Would be interested in taking a look :)Transparent
In the usual setup InheritedWidget > MaterialApp > View, the child of your inheritedWidget is constant. And that's like 99% of the cases.Bonacci
So you save the MaterialApp Instance and use it when you change the inherited Widget?Remonstrance
@Transparent Code generator ? What for ? The code I provided can be generated using a simple static vscode snippet. And it doesn't contain any code duplication. I don't see the point of having a code generator here.Bonacci
@RémiRousselet Gotcha. Had slightly misread the State part, thought there was a bit of duplication. It's an interesting technique to grab the State class... I'd been duplicating some of this in my InheritedWidget so that peeps wouldn't be able to access methods like initState. I'll try applying it to the Flutter Architecture Samples and see how it goes. Thanks again for the in-depth explanation :)Transparent
I invented nothing here. After examinating Flutter's code (Theme/Navigator especially) I realised that's how they use InheritedWidget. Not only it avoids duplicates. But it also allows to extend inheritedWidget. For example, Theme extend IconTheme without breaking IconTheme.of(context) !Bonacci
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).Transparent
After looking at @Transparent 's sample I think I now understand it. by referencing widget.child you get everytime the same child which ensures it's not rebuild.Remonstrance
I'm not sure if it's a good idea to use the StatefulWidgets type for the .of() method instead of the InheritedWidgets type to make clear that this is about Data passed along.Remonstrance
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.Chauffer
@Chauffer Yeah my bad. Don't remember why I did that as it obviously won't work. Fixed by editing to true, thanks.Bonacci
In my of(context) call I get this error: NoSuchMethodError: The getter 'data' was called on null.Since
I added String get stuff => 'test'; to _MyInherited and changed the of method to return stuff instead of data, and I still get NoSuchMethodError: The getter 'stuff' was called on null. Which doesn't make a whole lot of sense since stuff is hard coded so cannot be null. Note I also tested that _MyInherited is not coming back as null inside the of method.Since
Okay, so it seems that with a MaterialApp as a child to MyInherited, accessing MyInherited.of() for a property of the MaterialApp causes this problem. @Transparent I noticed in your code you have an additional widget layer called InheritedWidgetApp so I added that to my code and it fixed this issue. why?Since
Would it be a good solution to use this technique to have custom widget like custom container where it has say like a border by default. So rather than applying the same border on every Container then just use this custom Container insteadAntilog
So what/where is onMyFieldChange ever called?Adriene
Best reason to never bother with InheritedWidget is Provider: https://mcmap.net/q/137563/-provider-vs-inheritedwidgetMontana
If I have 20 fields to store, then I should have 20 fields and 20 onMyFieldChange methods both in MyInheritedData and in _MyInheritedState, right?Disarray
In addition to 20 fields in two classes and 20 onChangeXX methods, I should add 20 MyInheritedData constructor arguments. That means 120+ lines for 20 settings. Too excessive. Is there a better solution?Disarray
@RémiRousselet Can you elaborate on how onMyFieldChange fits into this example? And why is the comparison oldWidget.onMyFieldChange != onMyFieldChange?Knuckle
I
32

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.

  1. 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
  2. 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".
  3. 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;
  }
  1. We add ancestor as a dependency of the current element (_dependencies.add(ancestor))
  2. We add current element to ancestor's dependencies (ancestor.updateDependencies(this, aspect))
  3. 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?

  1. Cache widgets by hands(manually)
  2. Use const because const create the only one instance of value/class
Impend answered 11/12, 2018 at 22:44 Comment(3)
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()?Tarnopol
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 wrongConditioner
Where does the clean up happen? i.e. remove all the dependencies from the HashSet's when the widget is removed?Furrier
G
5

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.

Graycegrayheaded answered 3/3, 2019 at 19:45 Comment(0)
U
-8

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

Urion answered 5/1, 2020 at 5:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.