Flutter Skill Of MediaQuery and Performance Optimization

GSYTech
CodeX
Published in
8 min readJul 7, 2022

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.topto 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 height
  • padding : It’s the status bar and the bottom safe area, but the bottom will become 0 when the keyboard pops up
  • viewPadding :It is the same as padding, 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 up
  • padding : The difference between the front and back of the pop-up keyboard is that the bottom is changed from 34 to 0
  • viewPadding : 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. Because MediaQuery is an InheritedWidget, we can use MediaQuery.of(context)to get the MediaQueryData 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, withoutMediaQuery.of(context).size , MyHomePage will not be rebuilt when the keyboard in EditPage 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 globalTextGlobal globalText = TextGlobal()in MyHomePage;
  • Then add three globalText in MyHomePage
  • Finally, click FloatingActionButton to trigger setState(() {});
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 configured new objects in the build method will be rebuilt; However, if the Scaffold in MyHomePage triggers rebuild internally, it will not cause the child corresponding to the body parameter in MyHomePage to perform rebuild.

Is it too abstract? Take a simple example, as shown in the following code:

  • We define a LikeScaffold Widget, which using widget.body to pass child.
  • Inside LikeScaffold, we use MediaQuery.of(context).viewInsets.bottomto imitating MediaQuery in Scaffold
  • Use LikeScaffold in MyHomePage, configure a builder for LikeScaffold 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 are Appbar and BottomNavigationBar in the body.

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.ofuses the context outside Scaffold and obtains the top-level MediaQueryData, so when the keyboard pops up, it will cause the page to be rebuilt
  • if MediaQuery.of uses the context in Scaffold, so we get the MediaQueryData of Scaffold in the region, such as the body described above. At the same time, the obtained MediaQueryData will also change due to different configurations of Scaffold

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,
),
);

--

--