Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 521
Thomas Avatar asked Mar 26 '18 12:03

Thomas


People also ask

How do you use an inherited widget in Flutter?

Example Using Inherited Widgetconst InheritedWidget({ Key? key, required Widget child }) : super(key: key, child: child); As you can see the child widget is required since the InheritedWidget has to be at the root of the widget tree so it can send the data through the widget tree.

How does an inherited widget work?

InheritedWidget is a base class that allows classes that extend it to propagate information down the tree efficiently. Basically, it works by notifying registered build contexts if there is any change. Therefore, the descendant widgets that depend on it will only be rebuilt if necessary.

How is an inherited widget different from a provider?

If you use InheritedWidget in large application, build methods always rebuilds whole build method. But with Provider you have Consumer widget which is can be very specific to control specific blocks of build method, so you have more efficiency. Also listeners have less complexity than InheritedWidgets'(O(N) vs O(N²)).

How do you use stateful widget Flutter?

To create a stateful widget in a flutter, use the createState() method. The stateful widget is the widget that describes part of a user interface by building a constellation of other widgets that represent a user interface more concretely. A stateful Widget means a widget that has a mutable state.


3 Answers

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.

like image 132
Rémi Rousselet Avatar answered Oct 23 '22 17:10

Rémi Rousselet


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
like image 23
maksimr Avatar answered Oct 23 '22 19:10

maksimr


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.

like image 4
kkurian Avatar answered Oct 23 '22 17:10

kkurian