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

「可以 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 提取起来,因为这样注释:

「区域不随帧变化,以最小化的方式制造」

640-165655682609841

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

  • 首先定义了一个 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) ,但它不再因为键盘的弹起而导致重建」

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

Scaffold 有很多类似的处理中,例如 决定根据内部是否有 和 来是否移除该区域内的 paddingTop 和 paddingBottom bodyAppbar``BottomNavigationBar

所以,看到这里有没有什么?「为什么当时不通过 MediaQuery.of(context) 获取的padding,有的top为0,有的不为0,原因就在于你获取的上下文在哪里」

举个例子,如下代码所示, ScaffoldChildPage 作为 Scaffold 孩子的 打印,我们分别在MyHomePageScaffoldChildPageMediaQuery.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();
  }
}

如下图所示,可以看到,因为此时 MyHomePageAppbar ,所以 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页不断地重建。

640-165655682609948

如果需要做一些简单的事情,那么,建议通过 useInheritedMediaQuery 这种方式来做一些简单的处理。

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

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

  • MediaQueryDataviewInsets \ padding \ viewPadding 的区别
  • MediaQuery 和键盘的状态关系
  • MediaQuery.of 使用不同上下文对性能的影响
  • 通过 内部Scaffoldcontext 获取到的 MediaQueryData 受到 Scaffold 的影响

如果翻阅本篇你还有什么疑问欢迎留言评论交流。

注册登录 后评论
    // 作者
    shuyu1991 发布于 声网开发者社区
    • 0
    // 本帖子
    关键词
    // 相关帖子
    Coming soon...
    • 0
    Flutter 小技巧之 MediaQuery 和 build 优化你不知道的秘密shuyu1991 发布于 声网开发者社区