Flutter: How to correctly use an Inherited Widget?

Question

Asked by Thomas on March 26, 2018 (source).

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?

Answer

Question answered by Rémi R (source).

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.

DART FLUTTER FLUTTER-WIDGET
SHARE: