Flutter 小技巧之 MediaQuery 和 build 优化你不知道的秘密

网友投稿 281 2022-09-21

Flutter 小技巧之 MediaQuery 和 build 优化你不知道的秘密

今天这篇文章的目的是补全大家对于 ​​MediaQuery​​ 和对应 rebuild 机制的基础认知,相信本篇内容对你优化性能和调试 bug 会很有帮助。

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​​ 变成 336​​padding​​​ 在弹出键盘的前后区别,​​bottom​​ 从 34 变成了 0​​viewPadding ​​ 在键盘弹出前后数据没有发生变化

可以看到 ​​MediaQueryData​​ 里的数据是会根据键盘状态发生变化,又因为 ​​MediaQuery ​​​ 是一个 ​​InheritedWidget​​​ ,所以我们可以通过 ​​MediaQuery.of(context)​​​ 获取到顶层共享的 ​​MediaQueryData​​ 。

那么问题来了,​​InheritedWidget​​ 的更新逻辑,是通过登记的 ​​context​​ 来绑定的,也就是 ​​MediaQuery.of(context)​​ 本身就是一个绑定行为,然后 ​​MediaQueryData​​​ 又和键盘状态有关系,所以:键盘的弹出可能会导致使用 ​​MediaQuery.of(context)​​ 的地方触发 rebuild,举个例子:

如下代码所示,我们在 ​​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

如下图 log 所示 , 可以看到在键盘弹起来的过程,因为 bottom 发生改变,所以 ​​MediaQueryData​​​ 发生了改变,从而导致上一级的 ​​MyHomePage​​ 虽然不可见,但是在键盘弹起的过程里也被不断 build 。

试想一下,如果你在每个页面开始的位置都是用了 ​​MediaQuery.of(context)​​ ,然后打开了 5 个页面,这时候你在第 5 个页面弹出键盘时,也触发了前面 4 个页面 rebuild,自然而然可能就会出现卡顿。

那么如果我不在 ​​MyHomePage​​ 的 build 方法直接使用 ​​MediaQuery.of(context)​​ ,那在 ​​EditPage​​ 里弹出键盘是不是就不会导致上一级的 ​​MyHomePage​​ 触发 build ?

答案是肯定的,没有了 ​​MediaQuery.of(context).size​​​ 之后, ​​MyHomePage​​​ 就不会因为 ​​EditPage​​ 里的键盘弹出而导致 rebuild。

所以小技巧一:要慎重在 ​​Scaffold​​ 之外使用 ​​MediaQuery.of(context)​​ ,可能你现在会觉得奇怪什么是 ​​Scaffold​​ 之外,没事后面继续解释。

那到这里有人可能就要说了:我们通过 ​​MediaQuery.of(context)​​​ 获取到的 ​​MediaQueryData​​​ ,不就是对应在 ​​MaterialApp​​​ 里的 ​​MediaQuery​​ 吗?那它发生改变,不应该都会触发下面的 child 都 rebuild 吗?

这其实和页面路由有关系,也就是我们常说的 ​​PageRoute​​ 的实现。

如下图所示,因为嵌套结构的原因,事实上弹出键盘确实会导致 ​​MaterialApp​​​ 下的 child 都触发 rebuild ,因为设计上 ​​MediaQuery​​​ 就是在 ​​Navigator​​ 上面,所以弹出键盘自然也就触发 ​​Navigator​​ 的 rebuild。

那正常情况下 ​​Navigator​​ 都触发 rebuild 了,为什么页面不会都被 rebuild 呢?

这就和路由对象的基类 ​​ModalRoute​​​ 有关系,因为在它的内部会通过一个 ​​_modalScopeCache​​​ 参数把 ​​Widget​​ 缓存起来,正如注释所说:

缓存区域不随帧变化,以便得到最小化的构建。

举个例子,如下代码所示:

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 { TextGlobal globalText = TextGlobal(); @override Widget build(BuildContext context) { print("######## MyHomePage"); return Scaffold( appBar: AppBar(), body: new

那么有趣的来了,如下图 log 所示,​​"######## TextGlobal"​​​ 除了在一开始构建时有输出之外,剩下 ​​ setState(() {});​​​ 的时候都没有在触发,也就是没有 rebuild ,这其实就是上面 ​​ModalRoute​​ 的类似行为:弹出键盘导致了 ​​MediaQuery​​ 触发 ​​Navigator​​ 执行 rebuild,但是 rebuild 到了 ​​ModalRoute​​ 就不往下影响。

其实这个行为也体现在了 ​​Scaffold​​​ 里,如果你去看 ​​Scaffold​​​ 的源码,你就会发现 ​​Scaffold​​​ 里大量使用了 ​​MediaQuery.of(context)​​ 。

比如上面的代码,如果你给 ​​MyHomePage​​​ 的 ​​Scaffold​​​ 配置一个 3333 的 ​​ValueKey​​​ ,那么在 ​​EditPage​​​ 弹出键盘时,其实 ​​MyHomePage​​​ 的 ​​Scaffold​​​ 是会触发 rebuild ,但是因为其使用的是 ​​widget.body​​​ ,所以并不会导致 ​​body​​ 内对象重构。

如果是 ​​MyHomePage​​​ 如果 rebuild ,就会对 build 方法里所有的配置的 ​​new​​​ 对象进行 rebuild;但是如果只是 ​​MyHomePage​​​ 里的 ​​Scaffold​​​ 内部触发了 rebuild ,是不会导致 ​​MyHomePage​​ 里的 body 参数对应的 child 执行 rebuild 。

是不是太抽象?举个简单的例子,如下代码所示:

我们定义了一个​​LikeScaffold​​​ 控件,在控件内通过​​widget.body​​ 传递对象在​​LikeScaffold​​​ 内部我们使用了​​MediaQuery.of(context).viewInsets.bottom ​​​ ,模仿​​Scaffold​​​ 里使用​​MediaQuery​​在​​MyHomePage​​​ 里使用​​LikeScaffold​​​ ,并给​​LikeScaffold​​​ 的 body 配置一个​​Builder​​​ ,输出​​"############ HomePage Builder Text "​​ 用于观察跳到​​EditPage​​ 页面打开键盘

class LikeScaffold extends StatefulWidget{ final Widget body; const LikeScaffold({Key? key, required this.body}) : super(key: key); @override State createState() => _LikeScaffoldState();}class _LikeScaffoldState extends State { @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 { @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 { @override Widget build(BuildContext context) { var routeLists = routers.keys.toList(); ///多加了个 Scaffold return Scaffold( body: new

答案是 ​​LikeScaffold​​​ 内的 ​​"####### LikeScaffold build​​​ 也不会因为键盘的弹起而输出,也就是: ​​LikeScaffold​​ 虽然使用了 ​​MediaQuery.of(context)​​ ,但是它不再因为键盘的弹起而导致 rebuild 。

因为此时 ​​LikeScaffold​​​ 是 ​​Scaffold​​​ 的 child ,所以在 ​​LikeScaffold​​​ 内通过 ​​MediaQuery.of(context)​​​ 指向的,其实是 ​​Scaffold​​​ 内部经过处理的 ​​MediaQueryData​​。

在 ​​Scaffold​​​ 内部有很多类似的处理,例如 ​​body​​​ 里会根据是否有 ​​Appbar​​​ 和 ​​BottomNavigationBar​​ 来决定是否移除该区域内的 paddingTop 和 paddingBottom 。

所以,看到这里有没有想到什么?为什么时不时通过 ​​MediaQuery.of(context)​​ 获取的 padding ,有的 top 为 0 ,有的不为 0 ,原因就在于你获取的 context 来自哪里。

举个例子,如下代码所示, ​​ScaffoldChildPage​​​ 作为 ​​Scaffold​​​ 的 child ,我们分别在 ​​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

如下图所示,可以看到,因为此时 ​​MyHomePage​​​ 有 ​​Appbar​​​ ,所以 ​​ScaffoldChildPage​​​ 里获取到 paddingTop 是 0 ,因为此时 ​​ScaffoldChildPage​​​ 获取到的 ​​MediaQueryData​​​ 已经被 ​​MyHomePage​​​ 里的 ​​Scaffold​​ 改写了。

如果此时你给 ​​MyHomePage​​​ 增加了 ​​BottomNavigationBar​​​ ,可以看到 ​​ScaffoldChildPage​​ 的 bottom 会从原本的 34 变成 90 。

到这里可以看到 ​​MediaQuery.of​​ 里的 context 对象很重要:

如果页面 ​​MediaQuery.of​​ 用的是 ​​Scaffold​​ 外的 ​​context ​​ ,获取到的是顶层的 ​​MediaQueryData​​ ,那么弹出键盘时就会导致页面 rebuild​​MediaQuery.of​​ 用的是 ​​Scaffold​​ 内的 ​​context ​​ ,那么获取到的是 ​​Scaffold​​ 对于区域内的 ​​MediaQueryData​​,比如前面介绍过的 body ,同时获取到的​​MediaQueryData​​​ 也会因为​​Scaffold​​ 的配置不同而发生改变

所以,如下动图所示,其实部分人会在 push 对应路由地方,通过嵌套 ​​MediaQuery​​ 来做一些拦截处理,比如设置文本不可缩放,但是其实这样会导致键盘在弹出和收起时,触发各个页面不停 rebuild ,比如在 Page 2 弹出键盘的过程,Page 1 也在不停 rebuild。

所以,如果需要做一些全局拦截,推荐通过 ​​useInheritedMediaQuery​​ 这种方式来做全局处理。

return MediaQuery( data: MediaQueryData.fromWindow(WidgetsBinding.instance!.window).copyWith(boldText: false), child: MaterialApp( useInheritedMediaQuery: true, ),);

所以最后做个总结,本篇主要理清了:

​​MediaQueryData​​​ 里​​viewInsets​​​ \​​ padding​​​ \​​viewPadding ​​ 的区别​​MediaQuery​​ 和键盘状态的关系​​MediaQuery.of​​ 使用不同 context 对性能的影响通过​​Scaffold​​​ 内的​​context ​​​ 获取到的​​MediaQueryData​​​ 受到​​Scaffold​​ 的影响

那么,如果看完本篇你还有什么疑惑,欢迎留言评论交流。

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Flutter 小技巧之 ListView 和 PageView 的各种花式嵌套
下一篇:公关界的007:怎样树立“喜闻乐见”的综合金融品牌形象,中国人寿这样做!
相关文章

 发表评论

暂时没有评论,来抢沙发吧~