天天看點

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 ​

    ​ 在鍵盤彈出前後資料沒有發生變化
Flutter 小技巧之 MediaQuery 和 build 優化你不知道的秘密
可以看到 ​

​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 。

Flutter 小技巧之 MediaQuery 和 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。

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

那正常情況下 ​

​Navigator​

​ 都觸發 rebuild 了,為什麼頁面不會都被 rebuild 呢?

這就和路由對象的基類 ​

​ModalRoute​

​​ 有關系,因為在它的内部會通過一個 ​

​_modalScopeCache​

​​ 參數把 ​

​Widget​

​ 緩存起來,正如注釋所說:

緩存區域不随幀變化,以便得到最小化的建構。
Flutter 小技巧之 MediaQuery 和 build 優化你不知道的秘密

舉個例子,如下代碼所示:

  • 首先定義了一個​

    ​TextGlobal​

    ​​ ,在 build 方法裡輸出​

    ​"######## 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      

那麼有趣的來了,如下圖 log 所示,​

​"######## TextGlobal"​

​​ 除了在一開始建構時有輸出之外,剩下 ​

​ setState(() {});​

​​ 的時候都沒有在觸發,也就是沒有 rebuild ,這其實就是上面 ​

​ModalRoute​

​ 的類似行為:彈出鍵盤導緻了 ​

​MediaQuery​

​ 觸發 ​

​Navigator​

​ 執行 rebuild,但是 rebuild 到了 ​

​ModalRoute​

​ 就不往下影響。

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

其實這個行為也展現在了 ​

​Scaffold​

​​ 裡,如果你去看 ​

​Scaffold​

​​ 的源碼,你就會發現 ​

​Scaffold​

​​ 裡大量使用了 ​

​MediaQuery.of(context)​

​ 。

比如上面的代碼,如果你給 ​

​MyHomePage​

​​ 的 ​

​Scaffold​

​​ 配置一個 3333 的 ​

​ValueKey​

​​ ,那麼在 ​

​EditPage​

​​ 彈出鍵盤時,其實 ​

​MyHomePage​

​​ 的 ​

​Scaffold​

​​ 是會觸發 rebuild ,但是因為其使用的是 ​

​widget.body​

​​ ,是以并不會導緻 ​

​body​

​ 内對象重構。

Flutter 小技巧之 MediaQuery 和 build 優化你不知道的秘密
如果是 ​

​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<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​

​ 執行個體。

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

是以通過這個最小例子,可以看到雖然 ​

​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​

​​ 内的 ​

​"####### LikeScaffold build​

​​ 也不會因為鍵盤的彈起而輸出,也就是: ​

​LikeScaffold​

​ 雖然使用了 ​

​MediaQuery.of(context)​

​ ,但是它不再因為鍵盤的彈起而導緻 rebuild 。

因為此時 ​

​LikeScaffold​

​​ 是 ​

​Scaffold​

​​ 的 child ,是以在 ​

​LikeScaffold​

​​ 内通過 ​

​MediaQuery.of(context)​

​​ 指向的,其實是 ​

​Scaffold​

​​ 内部經過處理的 ​

​MediaQueryData​

​。

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

​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​

​ 改寫了。

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

如果此時你給 ​

​MyHomePage​

​​ 增加了 ​

​BottomNavigationBar​

​​ ,可以看到 ​

​ScaffoldChildPage​

​ 的 bottom 會從原本的 34 變成 90 。

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

到這裡可以看到 ​

​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。

Flutter 小技巧之 MediaQuery 和 build 優化你不知道的秘密
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​

    ​ 的影響