iTranslated by AI
Understanding MediaQuery in Flutter
This article is for day 19 of the Flutter #3 Advent Calendar 2020 - Qiita.
If you are building Flutter apps, I believe you have likely encountered MediaQuery at some point.
For example, to adjust a widget's size to the screen width or height, you might often write something like:
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height
In this article, I want to explore MediaQuery—which is frequently used casually for "getting the screen size"—to understand what it actually is, how it's built, and how it can be utilized in other scenarios.
References
The official documentation. While it mostly uses screen size acquisition as an example, it also covers other use cases and elements, so reading it can be very rewarding.
The source code. You can find many discoveries by tracing the detailed implementation here. When tracing the logic, it's efficient to open this file in VSCode or Android Studio to leverage IDE support.
An article I previously wrote about Flutter's "Three Trees." Understanding this will likely make your comprehension of this article and the official documentation about five times faster and more accurate.
Main Content
In this article, I will investigate MediaQuery primarily from the following perspectives:
- MediaQuery inherits from InheritedWidget
- MediaQuery holds information other than size
- How to utilize MediaQuery as seen in standard widgets
In addition, I will touch upon content judged important for understanding the underlying mechanism.
Since this is a long article overall, I recommend reading it bit by bit, section by section.
MediaQuery is a Subclass of InheritedWidget
First, as you can see from the official documentation and source code, MediaQuery is a subclass of InheritedWidget.
Inheritance
Object > DiagnosticableTree > Widget > ProxyWidget > InheritedWidget > MediaQuery
Regarding InheritedWidget, I previously wrote about it in 【Flutter】What is InheritedWidget, but to explain it briefly, it is a Widget that allows descendant Widgets to reference data held by an ancestor Widget. Furthermore, a Widget that has referenced an InheritedWidget (more precisely, the Element generated by that Widget) is cached by the InheritedWidget (the InheritedElement it generated) and gains the ability to receive notifications when the data held by the InheritedWidget changes.
By utilizing this basic mechanism of InheritedWidget, any StatelessWidget or StatefulWidget we code can reference MediaQuery.
Additionally, MediaQuery.of, which is called when using MediaQuery, performs a process of tracing the tree of the context (which is actually an Element) received as an argument up to its ancestors to find the InheritedElement. This scanning process is similar to what happens in various situations where you want to find a target Widget by tracing ancestors, such as Navigator.of. It is a very important process for understanding how Flutter's three trees work.
Where MediaQuery is Created
MediaQuery is created within WidgetsApp.
To be precise, it follows a flow where _MediaQueryFromWindow is created in the build() method of _WidgetsAppState (the State of WidgetsApp), and then MediaQuery is returned in the build() method of its State, _MediaQueryFromWindowsState.
Since MaterialApp includes WidgetsApp in the Widget it returns in its _MaterialAppState.build(), you can similarly use MediaQuery when using MaterialApp.
As mentioned earlier, MediaQuery is an InheritedWidget, so it cannot be referenced from Widgets located as ancestors of MediaQuery in the tree. However, since you typically pass WidgetsApp or MaterialApp to runApp(), such a problem should not occur.
The Data is Held by the MediaQueryData Class
Up to this point, I have been explaining MediaQuery, but strictly speaking, the data is held by a class called MediaQueryData. (Reference)
final MediaQueryData data;
Additionally, it can be seen from the return value that the object returned by MediaQuery.of is also MediaQueryData. Reading the source code, you can see that it doesn't just return the result of dependOnInheritedWidgetOfExactType(), but appends .data to return the MediaQueryData held by MediaQuery. (Reference)
static MediaQueryData of(BuildContext context) {
.. omitted ..
return context.dependOnInheritedWidgetOfExactType<MediaQuery>()!.data;
}
Therefore, when investigating what kind of data can be referenced using MediaQuery, it is good to refer not only to the MediaQuery documentation but also to the MediaQueryData documentation. The data held is explained in more detail there.
Data held by MediaQueryData
padding, viewPadding, viewInsets
Looking at the MediaQueryData documentation, you can see that in addition to the frequently used size, there is data such as padding, viewPadding, and viewInsets.
MediaQueryData includes three EdgeInsets values: padding, viewPadding, and viewInsets. These values reflect the configuration of the device and are used and optionally consumed by widgets that position content within these insets.
I will omit the details in this article, but these values manage how much of your app's screen width and height are reduced by system-managed UI components like keyboards and notches.
Depending on the widget, you might need to change the height of the UI in response to the keyboard opening or closing. These values are used in such cases.
Also, as mentioned earlier, since MediaQuery is an InheritedWidget, if these values change (e.g., when a user opens the keyboard), you can immediately receive notification from MediaQuery, rebuild the widget, and adjust the height.
Accessibility
MediaQueryData holds values related to text display such as boldText and textScaleFactor, as well as values related to screen display like invertColors and disableAnimations, which can be changed in the OS's "Accessibility" settings.
In particular, textScaleFactor is a value that is easily changed depending on the user's vision. The final font size displayed by Text is determined by multiplying the fontSize specified in style by this value, so measures may be necessary to prevent the layout from breaking even if the textScaleFactor becomes large.
/// The number of font pixels for each logical pixel.
///
/// For example, if the text scale factor is 1.5, text will be 50% larger than
/// the specified font size.
///
/// After this is set, you must call [layout] before the next call to [paint].
double get textScaleFactor => _textScaleFactor;
As mentioned later, elements like the AppBar's title or Cupertino dialogs are designed to be unaffected by the user's OS settings by setting a maximum value for this textScaleFactor.[1]
Others
Additionally, MediaQueryData holds information such as devicePixelRatio, which represents the device's display pixel ratio, orientation for the current screen orientation, and alwaysUse24HourFormat to indicate whether the clock is set to a 24-hour format.
It is a good idea to read the documentation once for more details.
Learning How to Utilize MediaQuery from AppBar and Scaffold
Since MediaQuery is a subclass of InheritedWidget, you can override the MediaQuery values referenced by descendant widgets by placing another MediaQuery with different values in the widget tree.[2]
To make this "overwriting by changing some values" easy, MediaQuery provides methods like MediaQuery.removePadding to create MediaQueryData with modified padding values, and MediaQueryData itself has MediaQueryData.copyWith to create a copy with specific fields modified.
Several standard Flutter widgets use this technique effectively, with AppBar and Scaffold being clear examples. Here, we will look at how these two widgets utilize MediaQuery overrides to understand how "overwriting MediaQuery" can be applied.
Example: AppBar
AppBar displays the widget passed to the title property (usually a Text) as the title in the center of the AppBar.
However, if the device's text size is significantly increased due to the user's accessibility settings, issues can occur: the text might not fit within the AppBar, or the AppBar's height might increase, restricting the height available for the body.
To avoid such inconveniences, AppBar limits the textScaleFactor of MediaQueryData to a maximum of 1.34. This ensures that the layout does not break, regardless of how much the user changes the text size in the device settings. (Reference)
final MediaQueryData mediaQueryData = MediaQuery.of(context);
title = MediaQuery(
data: mediaQueryData.copyWith(
textScaleFactor: math.min(
mediaQueryData.textScaleFactor,
_kMaxTitleTextScaleFactor,
),
),
child: title,
);
This kind of "text size limitation" is used in other components like DatePicker, BottomNavigationBar, and several Cupertino widgets where the UI cannot freely accommodate expanded text sizes.
Example: Scaffold
As another example of overwriting MediaQueryData, Scaffold modifies the top and bottom padding of the screen. (Reference)
return MediaQuery(
data: metrics.copyWith(
padding: metrics.padding.copyWith(
top: top,
bottom: bottom,
),
),
child: body,
);
This is because the height of the Scaffold's body frame changes based on:
- Whether an AppBar is present
- Whether a BottomNavigationBar is present
- Whether
persistentFooterButtonsare present
These calculations are performed in detail within _ScaffoldLayout.performLayout. The results of these calculations determine the top and bottom values, which are then used in the MediaQueryData.copyWith mentioned above.
Knowing this technique—creating a clone of MediaQueryData with specific values changed and wrapping child widgets with a MediaQuery using that data—can be useful when you want to restrict the text scale or drawing area for descendants of a particular widget.
Summary
In this article, we investigated the mechanism and usage of MediaQuery by reading the documentation and source code of MediaQuery itself, as well as the source code of widgets that use it.
Many people probably start learning about MediaQuery from the point of "If you want to get the screen size for now, use MediaQuery.of(context).size" (as did I). However, by investigating it in more detail, you can see that MediaQuery can be utilized for various other purposes as well.
-
Naturally, users expect that increasing the font size in the OS settings will make the text on the screen easier to read. Please note that overriding
textScaleFactorcan lead to betraying those expectations, so it's not always the right measure to take in every situation. ↩︎ -
The details of this mechanism are explained slightly in footnote 2 of 【Flutter】What is InheritedWidget. ↩︎
Discussion