How does Flutter InheritedWidget work?

Manabie Tech-Product Blog
Manabie
Published in
3 min readJan 10, 2020

I. What is InheritedWidget for?

To pass data from an ancestor widget to descendant ones, which are possibly deep down the widget tree.

InheritedWidget is immutable and its attributes are final; therefore, Flutter needs to rebuild InheritedWidget if we want to refresh it with new attributes.

II. How does InheritedWidget work?

Let’s figure out how Flutter InheritedWidget works by using the example code below.

Figure 1: The UI of the example code
Figure 2: The widget tree of the example code
  1. The MyContainer widget can be clicked to increase the counter value in the CounterValue by 1.
  2. The CounterLabel widget is a sibling of the CounterValue widget.
  3. The CounterValue should be automatically refreshed with a new counter value whenever the MyContainer is clicked, but the CounterLabel & the DummyContainer should not be rebuilt.
Snippet 1: The example code

1. How does the CounterValue access the current counter value stored in the MyInheritedWidget?

MyInheritedWidget myInheritedWidget = context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();

The dependOnInheritedWidgetOfExactType method enables a descendant widget to access the closest ancestor MyInheritedWidget instance enclosed in its BuildContext.

2. How does Flutter find the closest ancestor MyInheritedWidget instance from a widget’s BuildContext?

T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object aspect}) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
if (ancestor != null) {
assert(ancestor is InheritedElement);
return dependOnInheritedElement(ancestor, aspect: aspect);
}
_hadUnsatisfiedDependencies = true;
return null;
}

Every widget keeps a map _inheritedWidgets, which stores all ancestor InheritedWidget instances indexed by their type.

3. How is this _inheritedWidgets map constructed for each widget’s BuildContext in a widget tree?

When an element is added to the widget tree, Flutter calls the mount method, which then invokes the _updateInheritance method, which copies the _inheritedWidgets map from the parent.

void _updateInheritance() {
assert(_active);
_inheritedWidgets = _parent?._inheritedWidgets;
}

The InheritedElement class overrides _updateInheritance to copy the _inheritedWidgets from the parent and then assigns itself to _inheritedWidgets with the key which is its runtime type.

void _updateInheritance() {
assert(_active);
final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
if (incomingWidgets != null)
_inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
else
_inheritedWidgets = HashMap<Type, InheritedElement>();
_inheritedWidgets[widget.runtimeType] = this;
}

By running two above code snippets recursively when mounting a widget tree, every widget’s _inheritedWidgets stores all ancestor InheritedWidget instances.

4. In the above example, when the instance of MyInheritedWidget is rebuilt, the MyContainer and the CounterValue instances are rebuilt while the DummyContainer and CounterValue instances are not rebuilt. How does it work?

The body of the method dependOnInheritedWidgetOfExactType calls dependOnInheritedElement.

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

The line, ancestor.updateDependencies(this, aspect), registers MyContainer and CounterValue as MyInheritedWidget’s dependencies. When the MyInheritedWidget is rebuilt, Flutter loops through MyInheritedWidget’s dependencies and decides whether to rebuild these registered widgets by calling the updateShouldNotify method. If the method returns true, Flutter rebuilds all registered widgets. Otherwise, Flutter does not.

void notifyClients(InheritedWidget oldWidget) {
assert(_debugCheckOwnerBuildTargetExists('notifyClients'));
for (Element dependent in _dependents.keys) {
assert(() {
// check that it really is our descendant
Element ancestor = dependent._parent;
while (ancestor != this && ancestor != null)
ancestor = ancestor._parent;
return ancestor == this;
}());
// check that it really depends on us
assert(dependent._dependencies.contains(this));
notifyDependent(oldWidget, dependent);
}
}
void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
dependent.didChangeDependencies();
}

By calling didChangeDependencies on a dependent, the dependent is rebuilt.

The example code can be found at this GitHub link. If we miss out any important information or some details are incorrect, please help to comment.

--

--