[Intro Music]
One of Flutter's most efficient tricks is the ability to use a BuildContext
to look *up* the widget tree for something of interest.
The most common of these are MediaQuery.of(context) and Theme.of(context),
but popular state management solutions like flutter_bloc and Provider use the same mechanism.
And the pattern really is a godsend, too, because without it, every leaf widget that
cares about the MediaQuery would have to accept it as a parameter from their parent,
meaning every one of their ancestors would have to accept it as a parameter, too. This would couple
every widget in your app to their position in the widget tree. Imagine the refactoring pain!
But, the pattern is even more useful than merely preventing a thick jungle of parameters.
Simply having a widget run the line of code, MediaQuery.of(context),
ties the widget to the media query, meaning it'll get rebuilt whenever the media query changes.
And this is important; because if you are laying out a widget based on the screen size, that layout
is unlikely to remain valid if the user rotates their phone or expands their desktop window.
But, how does this work? Just writing MediaQuery.of(context) registers the widget as
a listener to the MediaQuery? Really?
Yes! Let's pull back the curtain.
First, it's helpful to remember what the ubiquitous `context` variable actually is.
You may recall that Flutter has three primary trees,
the Widget tree, Element tree, and RenderObject tree
Into each widget's `build` method is passed its corresponding Element,
which is actually a subclass of BuildContext.
[Alternate Dimension Teresa] So when your widgets assemble themselves with the use of a BuildContext,
it's like they're talking to a more knowledgeable version of themselves from another dimension.
[Normal Teresa] Next is that `OF` method on the MediaQuery class
that we call all the time. It's a static method that returns a
MediaQueryData instance, and internally, it calls `context.dependOnInheritedWidgetOfExactType`,
which can find any inherited widget in constant time.
The last line not only returns the MediaQueryData instance we care about,
but also registers the widget that called it as a dependency of the MediaQuery.
Our widget's Element is added to a special list that the MediaQuery will mark for rebuilding whenever it changes.
And if you are wondering how other packages like Provider work -- it's by doing exactly the same thing.
Remember Provider's `listen` parameter? If you set listen to True, Provider calls
context.dependOnInheritedWidgetOfExactType. That's how the Provider package is able to watch
a provider and automatically rebuild.
As a side note, if you set listen to false,
Provider calls a similar method – getElementForInheritedWidgetOfExactType.
It's similar, but doesn't create a dependency. Technically, you can call these
methods yourself, but the suggested pattern is to use the static `of` methods we all know so well.
Now, there's a few gotchas here. The documentation warns against
calling `of(context)` methods during widget constructors or initState,
saying those methods won't get re-run if you do. In fact, the framework will
throw an exception if you try to depend on an InheritedWidget from within the wrong method.
The only safe place to call this method is in State.didChangeDependencies, or build methods.
Now, how does THIS work? If you call this method from initState,
somehow the framework knows you cheated and won't schedule that widget's initState to be re-run?
The answer is that Flutter always knows what phase of the widget lifecycle it currently is,
so the static `of` methods first check to make sure you're in the `active` phase, which contains
`didChangeDependencies` and `build`, and if so, the inherited widget's element registers
your widget's element as a dependant. `initState` lives in a different phase
which is incompatible with this system -- because initialization only happens once, so it wouldn't
make sense to schedule initState to be executed again in the future. Initialization that happens
repeatedly would just be… Groundhog Day?
Initialization that happens repeatedly...
So you can call the static `of(context)` methods during didChangeDependencies, or
during a build method; but, what's the difference? You may already be familiar with the build method,
but few Flutter developers have spent as much time thinking about `didChangeDependencies`.
First of all, what even are those *dependencies* its name references?
Many developers intuitively assume they're the widget's ancestors, or, something? But no,
it's the dependencies created by dependOnInheritedWidgetOfExactType. For example,
if you register your widget with the Theme, then when the Theme changes, your widget's
didChangeDependencies method will be called first, then afterward, its build method will be called.
Deciding where to put your `of(context)` method calls involves understanding a few trade-offs.
Actually finding the ancestor widget is cheap, but where you call it only matters if you're
doing something expensive with the result. If you only need to run your fancy logic when the Theme,
or the MediaQuery, or the whatever, changes - you can consider caching a computed value in the
didChangeDependencies and then referencing it in your build method.
And it's worth holding here to *truly* understand why this works. If you're loading new assets
or performing expensive calculations that depend on the inherited widget, then it stands to reason
you wouldn't need to re-perform that operation until the inherited widget changes…
which is exactly when didChangeDependencies is called
Of course, if you're not doing anything expensive,
you can always just put that code right in your build method. This is the standard
pattern we all tend to use 99% of the time. To be clear, the performance difference here
is very small. But, Flutter apps have *a lot* of widgets, and especially during animations
when dropped frames are acutely painful, this small difference would add up across your app.
If needed, you can also implement your own InheritedWidgets, too, but that's rarely
necessary. For most things, just use Provider. Hopefully this helps demystify the magic line
of code we've all written a thousand times. For more info on Flutter, head to flutter.dev.
[Outro Music]