Flutter: How to correctly use an Inherited Widget?

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.


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

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.