It’s been said, many developers learning Flutter first encounter the MediaQuery widget with an error. Something akin to, “MediaQuery.of() called with a context that does not contain a MediaQuery.” For example, these developers were no doubt told to use the static function, of, in the MediaQuery class when they wanted the width and or height of the phone’s screen. However, they did so with a little too much enthusiasm without starting their app up with a MaterialApp widget or a CupertinoApp widget.
The example app, mediaQuery.dart, we’re using, today, in this article is listed below. To demonstrate such an error, I initially didn’t use the class, MyApp, with its MaterialApp widget (first arrow) but merely supplied the widget, _MediaQueryWidget, to Flutter’s runApp() function and ran it. Because of the call MediaQuery.of() function (second arrow), the resulting ‘Red Screen of Dread’ is presented displaying the message, “No MediaQuery widget found.” The last screenshot comes about when the MaterialApp widget is now used listing all the information currently provided by the MediaQuery widget.
In this article, we’re going to review how the MediaQuery widget is first instantiated, and why it’s necessary to first establish which ‘platform widget’ your Flutter app is going to use (Material, Cupertino, or otherwise) before you can even access this widget.
I Like Screenshots. Click For Gists.
As always, I prefer using screenshots in my articles over gists to show concepts rather than just show code. I find them easier to work with frankly. However, you can click or tap on these screenshots to see the code in a gist or in Github. Further, it’s better to read this article about mobile development on your computer than on your phone. Besides, we program mostly on our computers — not on our phones. Not yet anyway.
The Media Is The Message
What this widget can do for you is pretty straight forward. I’m just going to review a few of its fields in any detail as its role is plainly to provide information about the media (the phone) in which your app runs on. Frankly, how the MediaQuery widget is first instantiated and how that information comes about is more interesting, and so let’s get this out of the way first.
Note, my little example app, when calling the MediaQuery.of() function, now uses the ‘nullOk’ parameter to return a null if the function is unsuccessful. That means there’s no exception. The instance field, media, will instead be assigned null. Thus, just in case that happens, the list of information from this widget (next screenshot below) is repeatedly accessed using the ‘conditional member access operator:’
Looking at the list on the screenshot above, you get an appreciation of what sort of information is available to you. Most of them describe the characteristics of the device’s display screen. More often than not, a developer is looking up the screen dimensions for the particular phone the app is running on. The idea being the app can then adapt its interface to accommodate the various size of the screens it may encounter.
The Landscape of Things
As part of the MediaQuery widget’s role, it will actually notify your app if the phone, for example, rotates from portrait to landscape leaving it to your app to adjust its ‘look and feel’ and compensate or not for such an event.
The MediaQuery widget has this ability because its parent widget involves the mixin, WidgetsBinderObserver, while the MediaQuery class itself extends the class, InheritedWidget. We‘ll discuss this further in a little bit. At the moment, let’s examine some of the information offered to your app by the MediaQuery widget starting with how your app can determine the phone’s orientation. Taking a peek at this widget’s ‘orientation’ getter, we see it’s really quite simple. If the screen’s width is greater than the screen's height at that moment, it’s oriented landscape. See below.
That’s About The Size Of It
The getter above reveals another instance field offered by the MediaQuery object: size. Possibly the most used property, size is of the class, Size, and in turn, has two getters, width, and height, that supplies the aforementioned dimensions involving the device's screen.
Note, to get a more meaningful screen height for your app, you may wish to consider the height of both the app’s app bar and the bottom navigation bar (if they are both being used). Thankfully, Flutter offers two constant values separately representing these heights: kToolbarHeight and kBottomNavigationBarHeight. With that, the following equation could be used:
I just whipped up this getter to illustrate the equation. It includes yet another MediaQuery instance field called, padding. Its getter, top, provides you the height of the phone's status bar (usually situated along the very top of the screen). Substracting all three values from ‘the height’ will give you the remaining height traditionally reserved for your Flutter app.
Is It Safe?
The SafeArea widget, for example, is essentially just the MediaQuery widget excluding the areas of the screen usually reserved for the phone’s operating system leaving what’s left to the specified child widget. See below.
Physical vs. Logical
Flutter was designed to work with any number of devices. Hence, whenever the framework is dealing with pixels, it deals with logical pixels. As far as I know, there’s never a one-to-one relationship between the logical pixels and the physical pixels involved in any given device screen. The size or number of pixels in the actual hardware of the device will differ from that of the logical pixels. The number of physical pixels for each logical pixel is described by MeadiaQuery’s instance field, devicePixelRatio. As stated in its documentation, for the Nexus 6, one logical pixel is represented by 3.5 physical pixels. As for the emulator used with this example app, the device pixel ratio is 2.625.
Factor Of Scale
When Flutter is dealing with text (more specifically with the pixels that make up the text) there is a factor of scale involved. Certainly, it’s usually at a scale of 1.0 — meaning the size of the text is the size designated by the font size. However, MediaQuery’s instance field, textScaleFactor, could either be a negative value or a value greater than 1.0. For example, if the text scale factor is 1.5, the text will be 50% larger than the specified font size.
The rest of the information is, again, pretty straight forward, and I would rather talk about how the MediaQuery object is conceived in the first place. With that, I’ll leave it to the next few screenshots to highlight the more interesting field properties offered by in the class, MediaQuery.
The New Bottom
MediaQuery.of(context).viewInsets.bottom changes to reflect the new bottom, for example, when your phone’s keyboard is displayed. This means the new bottom is now at the top of the keyboard.
Your Default Media
Notice, in the example app, there are four static functions coming from the MediaQuery widget itself. They are being used instead of directly accessing their corresponding instance fields. This is so to demonstrate that default values are provided if, for example, the MediaQuery is indeed not available.
These are four static functions are now listed below revealing their default values.
Test Your Media
It’s in the documentation that if you’re going to use the MediaQuery.of() function, it’s recommended you perform a test first using the debugCheckHasMediaQuery() function. In the screenshot of the example app below on the left-hand side, you see I’ve implemented that very test. In fact, it was that test that produces that lovely ‘red screen’ you saw above. It couldn’t find an instance of the MediaQuery widget in the app’s widget tree.
As you see in the screenshot of the debugCheckHasMediaQuery() function below on the right-hand side, another function called, findAncestorWidgetOfExactType(), defined in the Element class is used to look back through the widget tree for a widget of type, MediaQuery.
The Widget’s App
Conceivably, the findAncestorWidgetOfExactType() function would go all the way back to ‘the first’ widget in the widget tree — an exception is likely thrown as a result. Again, the common fix for such an error is to use either a MaterialApp & CupertinoApp near ‘the root’ of your app‘s widget tree. You see, both such widgets work with yet another widget called, WidgetsApp, and it’s that widget the first introduces the MediaQuery widget to your app.
Your App’s Wrapper
The screenshot on the left-hand side below is found in the MaterialApp widget. More specifically in that widget’s State object, _MaterialAppState. It’s a screenshot of its build() function. It’s there where the WidgetsApp is then instantiated in the function, _buildWidgetApp. The screenshot below on the right-hand side is the build() function for the State object, _WidgetsAppState. It’s finally there where the MediaQuery widget is created. That MediaQuery widget is supplied another object called, MediaQueryData, using the variable, data. This variable comes from the static function, fromWindow().
As you see below, it’s the function fromWindow() that collects all that device information we were introduced to earlier. If and when any of these metrics change, a new MediaQueryData object is again made here.
That’s A Wrap
The MaterialApp widget, since it was chosen in this example app, is really a wrapper class for the widget, WidgetsApp. In turn, the WidgetsApp is the wrapper class for the MediaQuery widget. And as you see below, the MedisQuery widget is the wrapper class for your very app. Your app is the ‘child’ parameter for a MediaQuery widget. The MediaQuery widget is given your app in the form of, widget.child, as well as supplied the MediaQueryData object, data. This happens with every Flutter app ever written. Now you know, with every call to the function,
MediaQuery.of(), it is this very widget (actually this Widget’s State object) you see below that’s retrieved.
Notify The Media
As the documentation states in the screenshot below, that MediaQuery object is instantiated again every time a change occurs on the device. The State object, _MediaQueryFromWindowsState, because of its mixin, WidgetsBinderObserver, calls its build() function again with such a change (next screenshot). In turn, this creates the MediaQuery object calling the static function, fromWindow().
Note, to call its build() function with every application lifecycle event, the State object, _MediaQueryFromWindowsState, must further involve the WidgetsBinding class and be included as an ‘observer.’ You can see below, the State object does this in its initState() function.
By design, when events occur and this State object’s build() function is called again and again, the StatefulWidget, MediaQuery, is suppled a new MediaQueryData object and instantiated with ‘your app.’
Inherit The Widget
To put it plainly, any build() function in your app that had called the MediaQuery.of() function will be called again if the instance field, data, in that MediaQuery object has changed for one reason or another. That’s because the MediaQuery class is extending the class, InheritedWidget.
Like any InheritedWidget, the updateShouldNotify() returns true, in this case, if the instance field, data. contains any ‘new’ information. Note, because its parent widget, _MediaQueryFromWindowsState, is bound to application lifecycle events, this function will return true in most instances.
As you know, the instance field, data, is of type MediaQueryData. In the MediaQueryData class, we see below it has overridden the operator, ==. And so, the updateShouldNotify() function involves comparing each and every property in the MediaQueryData object.
Note, when you see the operator, !=, in Flutter it’s not an actual operator. As they say, it’s syntactic sugar. In truth, the expression
e1 != e2 is compiled to the following
!(e1 == e2). Thus, that extensive expression you see above is used to notify your app of a change to those settings on your device.
There you have it! This ‘media’ object has proven to be a vital means to reflect an efficient interface to your users. And now you know how it comes about, right?