Flutter Skill Of MediaQuery and Performance Optimization
Everyone in Flutter should be inseparable from MediaQuery
, for example, through MediaQuery.of(context).size
to get the screen size, or through MediaQuery.of(context).padding.top
to get the height of the status bar.
First of all, we need to explain briefly for MediaQuery.of
, becasue there are several similar parameters in MediaQueryData
:
viewInsets
: The size of the part completely blocked by the system user interface like the keyboard heightpadding
: It’s the status bar and the bottom safe area, but the bottom will become 0 when the keyboard pops upviewPadding
:It is the same aspadding
, but the bottom part will not change when the keyboard pops up
For example, on iOS, as shown in below, you can see the changes of some parameters in MediaQueryData
when the keyboard is pops up or not:
viewInsets
: It is 0 when the keyboard is not pops up, and the bottom becomes 336 after the keyboard is pops uppadding
: The difference between the front and back of the pop-up keyboard is that thebottom
is changed from 34 to 0viewPadding
: The data does not change before and after the keyboard pops up
We can see that the data in
MediaQueryData
will change according to the keyboard state. BecauseMediaQuery
is anInheritedWidget
, we can useMediaQuery.of(context)
to get theMediaQueryData
in MaterialApp.
Then the problem arises. The update logic of InheritedWidget
is bound through the registered Context
, that is, MediaQuery.of(context)
itself is a binding behavior, and then MediaQueryData
is related to the keyboard state.
Therefore, the pop-up of the keyboard may lead to the use of MediaQuery.of(context)
triggers rebuild, for example:
As shown in the following code, we used MediaQuery.of(context).size
in MyHomePage
and print out , then jump to the EditPage
and pop up the keyboard. Now What will be happens at this time?
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("######### MyHomePage ${MediaQuery.of(context).size}");
return Scaffold(
body: Container(
alignment: Alignment.center,
child: InkWell(
onTap: () {
Navigator.of(context).push(CupertinoPageRoute(builder: (context) {
return EditPage();
}));
},
child: new Text(
"Click",
style: TextStyle(fontSize: 50),
),
),
),
);
}
}class EditPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text("ControllerDemoPage"),
),
extendBody: true,
body: Column(
children: [
new Spacer(),
new Container(
margin: EdgeInsets.all(10),
child: new Center(
child: new TextField(),
),
),
new Spacer(),
],
),
);
}
}
As shown in the log , you can see the process of the keyboard popping up. Because the bottom changes, so the MediaQueryData
changed , resulting in theMyHomePage
is invisible, but it is also constantly built in the process of the keyboard popping up.
Imagine if you use
MediaQuery.of(context)
, and then open 5 pages. At this time, when you pop up the keyboard on the fifth page, it also trigger the rebuild of the first four pages, then you may got stuck.
So if I don’t use MediaQuery.of(context)
directly in the build method of MyHomePage
, does popping up the keyboard in EditPage
not cause the MyHomePage
to trigger build?
The answer is yes, without
MediaQuery.of(context).size
,MyHomePage
will not be rebuilt when the keyboard inEditPage
pops up.
So tip 1: Be careful to use MediaQuery.of(context)
outside Scaffold
, you may feel strange now what is outside Scaffold
. Never mind, I will continue to explain it later.
Then someone here may have to say: we use MediaQuery.of(context)
to got MediaQueryData
from MaterialApp
? If it changes, shouldn’t it trigger the following children to rebuild?
This is actually related to page routing, which is often referred to as the implementation of
PageRoute
.
As shown in below, because of the nested structure, in fact, the pop-up keyboard will indeed trigger the rebuild of the children under the MaterialApp
. Because MediaQuery
is designed to be on the Navigator
, the pop-up keyboard will naturally trigger the rebuild of the Navigator
.
But the Navigator
triggers rebuild. Why don’t all route pages be rebuilt?
This is related to the base class ModalRoute
of the routing object, because inside it, The _modalScopeCache
parameter caches the widget, as the comment says:
We cache the part of the modal scope that doesn't change from frame to frame
For example, the following code is shown:
- First, define a
TextGlobal
, and output “######## TextGlobal” in it’s build method - Then define a global
TextGlobal globalText = TextGlobal()
inMyHomePage
; - Then add three
globalText
inMyHomePage
- Finally, click
FloatingActionButton
to triggersetState(() {});
class TextGlobal extends StatelessWidget {
const TextGlobal({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
print("######## TextGlobal");
return Container(
child: new Text(
"测试",
style: new TextStyle(fontSize: 40, color: Colors.redAccent),
textAlign: TextAlign.center,
),
);
}
}
class MyHomePage extends StatefulWidget {
final String? title;
MyHomePage({Key? key, this.title}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
TextGlobal globalText = TextGlobal();
@override
Widget build(BuildContext context) {
print("######## MyHomePage");
return Scaffold(
appBar: AppBar(),
body: new Container(
alignment: Alignment.center,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
globalText,
globalText,
globalText,
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {});
},
),
);
}
}
Something interesting has come. As shown in the log below,"######## TextGlobal"
except for the output at the beginning of construction, there is setState(() {});
is not triggered at all, there is no rebuild, which is actually a similar behavior of the above ModalRoute
: the pop-up of the keyboard causes MediaQuery
to trigger the Navigator
to perform rebuild, but the rebuild will not affect the ModalRoute
.
In fact, this behavior is also reflected in Scaffold
. If you look at the source code of Scaffold
, you will find that MediaQuery.of(context)
is widely used in Scaffold
.
For example, in the above code, if you configure a 3333 ValueKey
for the Scaffold
of MyHomePage
, when the keyboard pops up in EditPage
, the Scaffold
of MyHomePage
will trigger rebuild, but because it uses widget.body
, so it will not lead to the reconstruction of objects in the body
.
If
MyHomePage
is rebuilt, all configurednew
objects in the build method will be rebuilt; However, if theScaffold
inMyHomePage
triggers rebuild internally, it will not cause the child corresponding to the body parameter inMyHomePage
to perform rebuild.
Is it too abstract? Take a simple example, as shown in the following code:
- We define a
LikeScaffold
Widget, which usingwidget.body
to pass child. - Inside
LikeScaffold
, we useMediaQuery.of(context).viewInsets.bottom
to imitatingMediaQuery
inScaffold
- Use
LikeScaffold
inMyHomePage
, configure a builder forLikeScaffold
body, and output"############ HomePage Builder Text "
for observation - Jump to the
EditPage
page and open the keyboard
class LikeScaffold extends StatefulWidget {
final Widget body;
const LikeScaffold({Key? key, required this.body}) : super(key: key);
@override
State<LikeScaffold> createState() => _LikeScaffoldState();
}
class _LikeScaffoldState extends State<LikeScaffold> {
@override
Widget build(BuildContext context) {
print("####### LikeScaffold build ${MediaQuery.of(context).viewInsets.bottom}");
return Material(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [widget.body],
),
);
}
}
····
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
var routeLists = routers.keys.toList();
return new LikeScaffold(
body: Builder(
builder: (_) {
print("############ HomePage Builder Text ");
return InkWell(
onTap: () {
Navigator.of(context).push(CupertinoPageRoute(builder: (context) {
return EditPage();
}));
},
child: Text(
"FFFFFFF",
style: TextStyle(fontSize: 50),
),
);
},
),
);
}
}
You can see that “"####### LikeScaffold build 0.0
” and “############ HomePage Builder Text"
” are executed normally at first, and then after the keyboard pops up, “####### LikeScaffold build
” continuously outputs the size of the bottom following the keyboard animation, but "############ HomePage Builder Text "
does not output because it is a widget.body
instance.
So through this example, we can see that although MediaQuery.of(context)
is widely used in Scaffold
, but the scope of influence is constrained within Scaffold
.
Next, let’s continue to look at the example of modification. If there is one more Scaffold
nested on LikeScaffold
, what will the output result be?
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
var routeLists = routers.keys.toList();
///多加了个 Scaffold
return Scaffold(
body: new LikeScaffold(
body: Builder(
·····
),
),
);
}
The answer is that “####### LikeScaffold build
” in LikeScaffold
will not be output because of the bounce of the keyboard, that is, although LikeScaffold
uses “MediaQuery.of(context)
”, it will no longer rebuild because of the bounce of the keyboard.
Because LikeScaffold
is the child of Scaffold
at this time, MediaQuery.of(context)
refers to MediaQueryData
processed inside Scaffold
.
There are many similar processes in
Scaffold
. For example, whether to remove the paddingTop and paddingBottom in the area will be decided according to whether there areAppbar
andBottomNavigationBar
in thebody
.
So, did you think of anything here? Why use MediaQuery.of(context)
get the padding, some top is 0, some is not 0, the reason is that you get the context from where.
For example, as shown in the following code, as the child of Scaffold
, we print MediaQuery.of(context).padding
in MyHomePage
and ScaffoldChildPage
:
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("MyHomePage MediaQuery padding: ${MediaQuery.of(context).padding}");
return Scaffold(
appBar: AppBar(
title: new Text(""),
),
extendBody: true,
body: Column(
children: [
new Spacer(),
ScaffoldChildPage(),
new Spacer(),
],
),
);
}
}
class ScaffoldChildPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("ScaffoldChildPage MediaQuery padding: ${MediaQuery.of(context).padding}");
return Container();
}
}
As shown in the following , it can be seen that the paddingTop obtained in the ScaffoldChildPage
is 0 because the MyHomePage
has an Appbar
at this time, because the MediaQueryData
obtained by the ScaffoldChildPage
has been rewritten by the Scaffold
in the MyHomePage
.
If you add BottomNavigationBar
to MyHomePage
at this time, you can see that the bottom of ScaffoldChildPage
will change from 34 to 90.
Here you can see the context object in of is very important to MediaQuery.of
:
- If the page
MediaQuery.of
uses the context outsideScaffold
and obtains the top-levelMediaQueryData
, so when the keyboard pops up, it will cause the page to be rebuilt - if
MediaQuery.of
uses the context inScaffold
, so we get theMediaQueryData
ofScaffold
in the region, such as the body described above. At the same time, the obtainedMediaQueryData
will also change due to different configurations ofScaffold
Therefore, as shown in following , some people will do some interception processing by nesting MediaQuery
in the place corresponding to the route of push, such as setting the text non scalable, but in fact, this will cause the keyboard to trigger the continuous rebuild of each page when it pops up and retracts, such as the process of popping up the keyboard on page 2, and the continuous rebuild of page 1.
Therefore, if you need to do some global interception, it is recommended to do global processing through useInheritedMediaQuery
.
return MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance!.window).copyWith(boldText: false),
child: MaterialApp(
useInheritedMediaQuery: true,
),
);