Flutter 小技巧之 MediaQuery 和 build 优化你不知道的秘密
「今天这篇文章的目的是补全大家对于 MediaQuery
和重建机制的基础,相信本篇内容对你优化性能和调试错误会很有帮助」。
Flutter 里大家应该都 获取大小的大小,或者 MediaQuery
说通过 获取屏幕大小, 那顺便使用会出现 什么问题吗?MediaQuery.of(context).size``MediaQuery.of(context).padding.top``MediaQuery.of(context)
首先需要简单解释一下,通过 MediaQuery.of
获取到我们的 里面MediaQueryData
有几个很类似的参数:
viewInsets
「被系统用户 界面完整覆盖的部分大小,简单来说就是键盘高度」padding
: 「简单来说就是状态栏和底部安全区域,但是bottom
因为会场上升0」viewPadding
:「和padding
但是一样,bottom
部分不会发生改变」
举个例子,在下面列出的例子,在 iOS 键盘上的一个参数和未显示键盘的变化下,可以看到 MediaQueryData
里一些的变化:
viewInsets
在没有弹出键盘的时候是 0,键盘之后bottom
变成 336padding
在弹出键盘的前后区别,bottom
从34变成了0viewPadding
在弹出键盘之前的数据没有发生变化
❝
「可以
MediaQueryData
根据会键盘状态发生的情况,也可以通过获取到因为是看到里的一个变化的数据 , 是 的MediaQuery
。 」InheritedWidget``MediaQuery.of(context)``MediaQueryData
❞
InheritedWidget
的更新逻辑问题,就是通过登记的 context
来绑定的,也就是 MediaQuery.of(context)
这样一个绑定行为, MediaQueryData
可能又和键盘的状态,键盘的启动关系会导致使用 MediaQuery.of(context)
的地方触发重建,举个例子:
如下代码所示,我们在 MyHomePage
使用了 MediaQuery.of(context).size
并打印输出,然后到跳转 EditPage
页面,弹出键盘,这时候会发生什么情况?
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(),
],
),
);
}
}
如下图日志所示,可以看到在键盘弹起的过程中,因为底部发生 了改变,所以MediaQueryData
发生了改变,导致上一级的 MyHomePage
虽然不明显,在键盘弹起的过程中也被不断的构建。
❝
想一下,如果你在试一试的页面开始的位置都是用的
MediaQuery.of(context)
,然后打开了 5 个,这时候你在第 5 个页面弹出页面的时候,也启动了前面的重建,4 个自然而然的页面可能会出现卡顿。❞
「那么如果我不在 MyHomePage
的构建 直接使用MediaQuery.of(context)
,那在 EditPage
弹出键盘不是就不会导致一级的 MyHomePage
触发构建」 ?
❝
答案是肯定的,没有了
MediaQuery.of(context).size
之后,MyHomePage
就不会因为EditPage
里的键盘弹出而导致重建。❞
所以小提示一:「慎重在 Scaffold
外面使用 MediaQuery.of(context)
」 ,你现在可能会觉得什么是 Scaffold
外面,后面没有解释。
那应该是这里有人可能会说:我们通过 MediaQuery.of(context)
获取到的 MediaQueryData
,不就是在 MaterialApp
里的 MediaQuery
吗?那它发生了改变,不触发吗?下面的孩子都重建了?
❝
「这其实和页面路由有关系,也是我们常说
PageRoute
的实现」。❞
如下图所示,由于天然的结构的原因,初步确定会导致 MaterialApp
重建,因为设计上 会导致重建 ,所以“启动键盘”MediaQuery
就是在 自然的触发下重建的。Navigator
「那正常情况下 Navigator
都触发重建了,为什么页面不会都被重建呢」?
和那个路由对象的基类 ModalRoute
有关系,在它的内部会通过一个 _modalScopeCache
参数把 它Widget
提取起来,因为这样注释:
❝
「区域不随帧变化,以最小化的方式制造」。
❞
举个例子,如下代码所示:
- 首先定义了一个
TextGlobal
,在构建方法里输出"######## TextGlobal"
- 然后在
MyHomePage
里面定义了一个正确的TextGlobal globalText = TextGlobal();
MyHomePage
勾选在里添加3个globalText- 点击
FloatingActionButton
触发 最后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(() {});
},
),
);
}
}
上面的图显示出来了,"######## TextGlobal"
除了开始制作的时候也有外在 setState(() {});
的时候都没有输出的时候触发了,就是重建 ModalRoute
的类似的行为,这就是“弹出屏幕没有提示日志” MediaQuery
触发 Navigator
执行rebuild,但是rebuild到了 ModalRoute
不会往下影响”。
这个 动作也在里面的Scaffold
里头,如果你从源头上 ,你会Scaffold
发现 Scaffold
大量使用了 MediaQuery.of(context)
。
他们的代码,如果你 MyHomePage
的 配置是Scaffold
3333 的 , 但是因为ValueKey
在 弹出的EditPage
键盘时, 会立即触发重建, 所以它的代码是这样的 ,所以并导致 使用对象的 结构。MyHomePage``Scaffold``widget.body``body
❝
是
MyHomePage
如果重建,就要对构建方法里所有的配置的new
对象进行重建;但是如果只是MyHomePage
里的Scaffold
触发了重建,是不会导致MyHomePage
体内参数的孩子执行重建。❞
是不是太代码抽象?举个简单的例子,如下所示:
- 我们定义了一个
LikeScaffold
控件,在控件内部通过widget.body
传递对象 - 在
LikeScaffold
我们内部使用了MediaQuery.of(context).viewInsets.bottom
,模仿Scaffold
里使用MediaQuery
- 在
MyHomePage
里使用LikeScaffold
,并给LikeScaffold
身体配置一个Builder
,输出"############ HomePage Builder Text "
观察用 - 跳到
EditPage
页面打开键盘
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),
),
);
},
),
);
}
}
可以看到,最开始 "####### LikeScaffold build 0.0
和 ############ HomePage Builder Text
正常执行,然后在因为键盘启动之后,"####### LikeScaffold build
跟随键盘动画不断输出 bottom
的大小,但是 "############ HomePage Builder Text ")
输出,它是 widget.body
实例。
「所以通过这个最小的例子,可以看到大部分 Scaffold
使用 MediaQuery.of(context)
情况,但影响范围是约束在 Scaffold
内部」。
继续修改这个例子,如果我们在 LikeScaffold
上面会看到一个多燃 Scaffold
的,那输出结果?
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
var routeLists = routers.keys.toList();
///多加了个 Scaffold
return Scaffold(
body: new LikeScaffold(
body: Builder(
·····
),
),
);
}
答案是 里面LikeScaffold
的 "####### LikeScaffold build
也不会因为键盘的弹起而输出,也就是: 「LikeScaffold
虽然使用了 MediaQuery.of(context)
,但它不再因为键盘的弹起而导致重建」 。
因为 LikeScaffold
是 Scaffold
此时的孩子, LikeScaffold
所以 通过MediaQuery.of(context)
处理指向的,其实是 Scaffold
内部经过处理的 MediaQueryData
。
❝
在
Scaffold
有很多类似的处理中,例如 决定根据内部是否有 和 来是否移除该区域内的 paddingTop 和 paddingBottombody
。Appbar``BottomNavigationBar
❞
所以,看到这里有没有什么?「为什么当时不通过 MediaQuery.of(context)
获取的padding,有的top为0,有的不为0,原因就在于你获取的上下文在哪里」。
举个例子,如下代码所示, ScaffoldChildPage
作为 Scaffold
孩子的 打印,我们分别在MyHomePage
和 ScaffoldChildPage
里 MediaQuery.of(context).padding
:
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();
}
}
如下图所示,可以看到,因为此时 MyHomePage
有 Appbar
,所以 ScaffoldChildPage
里获取到 paddingTop 是 0 ,因为 此时ScaffoldChildPage
获取到的 MediaQueryData
已经被 MyHomePage
里的 改写Scaffold
了。
如果此时你给 MyHomePage
增加了 BottomNavigationBar
,可以 ScaffoldChildPage
看到底部会从原来的 34 变成 90 。
到这里可以看到 MediaQuery.of
里的上下文对象很重要:
- 如果
MediaQuery.of
用 获取到 图片Scaffold
外的 图片,然后出现的“ 重建”context ``MediaQueryData
- “
MediaQuery.of
用是Scaffold
里面的context
,所以获取到 了Scaffold
某个区域内的MediaQueryData
” ,比如前面介绍过的身体,同时获取到了MediaQueryData
因为Scaffold
的配置不同而发生的变化
如下图所示的动图会在当地人,所以时起用各种方式 MediaQuery
来做一些合适的处理,“策划”会导致会在现场举行罢工,并在现场举行发布会不停地重建” ,比如在第2页弹出的过程中,第1页不断地重建。
如果需要做一些简单的事情,那么,建议通过 useInheritedMediaQuery
这种方式来做一些简单的处理。
return MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance!.window).copyWith(boldText: false),
child: MaterialApp(
useInheritedMediaQuery: true,
),
);
所以最后做了个总结,本篇主要理清了:
MediaQueryData
里viewInsets
\padding
\viewPadding
的区别MediaQuery
和键盘的状态关系MediaQuery.of
使用不同上下文对性能的影响- 通过 内部
Scaffold
的context
获取到的MediaQueryData
受到Scaffold
的影响
如果翻阅本篇你还有什么疑问欢迎留言评论交流。