Understanding InheritedModels on an example of a MediaQuery
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.