Understanding InheritedModels on an example of a MediaQuery

Roman Ismagilov
4 min readJul 25, 2024

--

This article aims to give an overview of how InheritedModels work, what is happening on the Element level and how to avoid excessive rebuilds when working with it.

1. What is a MediaQuery

This is a widget, descendants of which have access to such data as window size, font scale, device orientation and many others. MediaQuery extends an InheritedModel which allows us to rebuild parts of the widget tree when some of these properties have changed. InheritedModel, in turn, is a superclass of an InheritedWidget, which does the same, except for being able to work only with one property.

Getting window size

One of the most common use cases is getting the size, insets and padding. But what exactly are we getting? Let’s have a look by simply printing the values in a build method and comparing the values:

class MediaQueryExample extends StatelessWidget {
const MediaQueryExample({super.key});

@override
Widget build(BuildContext context) {
print("----");
print("size: ${MediaQuery.sizeOf(context)}");
print("viewInsets: ${MediaQuery.viewInsetsOf(context)}");
print("viewPadding: ${MediaQuery.viewPaddingOf(context)}");
print("padding: ${MediaQuery.paddingOf(context)}");

return Scaffold();
}
}

First, let’s see what happens:

2. InheritedModel’s aspects

As we can see, every time the window is resized, the printed values are changed. It happens because MediaQuery extends InheritedModel. Every time an aspect of InheritedModel is changed, all the widgets, that mention that aspect in their build methods are going to be rebuilt. To make it more clear, let’s see the sizeOf method implementation:

static Size sizeOf(BuildContext context) => 
_of(context, _MediaQueryAspect.size).size;
static MediaQueryData _of(BuildContext context, [_MediaQueryAspect? aspect]) {
assert(debugCheckHasMediaQuery(context));
return InheritedModel.inheritFrom<MediaQuery>(context, aspect: aspect)!.data;
}

_MediaQueryAspect.size is passed down to InheritedModel.inheritFrom method. Note, that the context is an Element of a widget, which has the build method where sizeOf was called.

If we go deeper and read the inheritFrom method, we will see that it creates a dependency between the context and an InheritedModelElement.

for (final InheritedElement model in models) {
final T value = context.dependOnInheritedElement(model, aspect: aspect) as T;
...
}

In case no aspect is passed, it will work similarly to InheritedWidget. the context would depend on any change in the MediaQuery’s data.

if (aspect == null) {
return context.dependOnInheritedWidgetOfExactType<T>();
}

So, how exactly does the update mechanism work?

3. InheritedElement’s dependents

Every InhertedWidget has an InheritedElement assigned to it. Similarly, every InheritedModel has an assigned InheritedModelElement. InheritedElement has a hashmap of dependencies. InheritedModelElement extends InheritedElement with only the difference that it creates dependencies between a hash set of different properties to a dependent element. Sounds very tangled, let’s watch it in the debugger.

We can access the Element in our build method like this:

final element = context.getElementForInheritedWidgetOfExactType<MediaQuery>();

As we can see, InheritedModelElement has a _dependents map, where we can see that the element of our MediaQueryExample widget is set dependent on several aspects of MediaQuery.

If we do the same for some InheritedWidget, the value in the map would be null, there will be just dependent elements, without any aspects. Again, because InheritedModel is an advanced version of InheritedWidget that supports depending on some specific properties, while InheritedWidget depends on the whole data object.

DefaultTextStyle.of(context); // create a dependency in the build method
// DefaultTextStyle is just some random InheritedWidget implementation
final defaultTextStyleElement =
context.getElementForInheritedWidgetOfExactType<DefaultTextStyle>();

Moreover, if you try to get some data like in the fragment below, there won’t be any aspects passed to the InheritedModelElement, the value would be null similar to using an InheritedWidget:

MediaQuery.of(context).size; // DON'T

By writing it that way the widget would be rebuilt every time any aspect is updated. To avoid that behaviour, always use methods that pass the aspects:

MediaQuery.sizeOf(context); //DO

So, how does the update mechanism work?

When extending an InheritedModel, developers should implement these methods:

bool updateShouldNotify(MediaQuery oldWidget)

bool updateShouldNotifyDependent(MediaQuery oldWidget, Set<Object> dependencies)

The first one comes from InheritedWidget and is needed in case null is passed as an aspect when calling inheritFrom. The second method is unique to InheritedModel and allows us to have deep control of when to update dependent widgets.

Below is a fragment of MediaQuery:

  @override
bool updateShouldNotify(MediaQuery oldWidget) => data != oldWidget.data;
  @override
bool updateShouldNotifyDependent(MediaQuery oldWidget, Set<Object> dependencies) {
return dependencies.any((Object dependency) => dependency is _MediaQueryAspect && switch (dependency) {
_MediaQueryAspect.size => data.size != oldWidget.data.size,
_MediaQueryAspect.orientation => data.orientation != oldWidget.data.orientation,
...
_MediaQueryAspect.accessibleNavigation => data.accessibleNavigation != oldWidget.data.accessibleNavigation,
_MediaQueryAspect.alwaysUse24HourFormat => data.alwaysUse24HourFormat != oldWidget.data.alwaysUse24HourFormat,
});
}

Should be pretty simple now: comparing every aspect and if the value is different, trigger a rebuild.

The updateShouldNotifyDependent method is called by InheritedModelElement in the notifyDependent method, which is in turn called by the notifyClients method of the same Element. In turn, it’s called in the updated method during the build phase when the widget’s configuration has changed.

                                                         // this
if (dependencies.isEmpty || (widget as InheritedModel<T>).updateShouldNotifyDependent(oldWidget, dependencies)) {
dependent.didChangeDependencies();
}

The notifyClients/update mechanism comes from ProxyElement, which is a superclass of InheritedElement. There are other implementations of that class as well. A similar mechanism happens in NotificationListener and InheritedNotifier.

Hope you’ve found this article useful. I will update it with more techniques whenever I find something useful. Follow me on Twitter to get the latest updates.

--

--

Roman Ismagilov

Covering some non-obvious nuances of Flutter development in my articles