今天這篇文章的目的是補全大家對于
MediaQuery
和對應 rebuild 機制的基礎認知,相信本篇内容對你優化性能和調試 bug 會很有幫助。
Flutter 裡大家應該都離不開
MediaQuery
,比如通過
MediaQuery.of(context).size
擷取螢幕大小 ,或者通過
MediaQuery.of(context).padding.top
擷取狀态欄高度,那随便使用
MediaQuery.of(context)
會有什麼問題嗎?
首先我們需要簡單解釋一下,通過
MediaQuery.of
擷取到的
MediaQueryData
裡有幾個很類似的參數:
-
:被系統使用者界面完全遮擋的部分大小,簡單來說就是鍵盤高度viewInsets
-
:簡單來說就是狀态欄和底部安全區域,但是 padding
會因為鍵盤彈出變成 0bottom
-
:和 viewPadding
一樣,但是 padding
部分不會發生改變bottom
舉個例子,在 iOS 上,如下圖所示,在彈出鍵盤和未彈出鍵盤的情況下,可以看到
MediaQueryData
裡一些參數的變化:
-
在沒有彈出鍵盤時是 0,彈出鍵盤之後viewInsets
變成 336bottom
-
在彈出鍵盤的前後差別,padding
從 34 變成了 0bottom
-
在鍵盤彈出前後資料沒有發生變化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
裡的鍵盤彈出而導緻 rebuild。
EditPage
是以小技巧一:要慎重在
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
緩存起來,正如注釋所說:
緩存區域不随幀變化,以便得到最小化的建構。
舉個例子,如下代碼所示:
- 首先定義了一個
,在 build 方法裡輸出TextGlobal
"######## TextGlobal"
- 然後在
裡定義一個全局的MyHomePage
TextGlobal globalText = TextGlobal();
- 接着在
裡添加 3 個 globalTextMyHomePage
- 最後點選
觸發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
就不往下影響。
其實這個行為也展現在了
Scaffold
裡,如果你去看
Scaffold
的源碼,你就會發現
Scaffold
裡大量使用了
MediaQuery.of(context)
。
比如上面的代碼,如果你給
MyHomePage
的
Scaffold
配置一個 3333 的
ValueKey
,那麼在
EditPage
彈出鍵盤時,其實
MyHomePage
的
Scaffold
是會觸發 rebuild ,但是因為其使用的是
widget.body
,是以并不會導緻
body
内對象重構。
如果是 如果 rebuild ,就會對 build 方法裡所有的配置的
MyHomePage
對象進行 rebuild;但是如果隻是
new
裡的
MyHomePage
内部觸發了 rebuild ,是不會導緻
Scaffold
裡的 body 參數對應的 child 執行 rebuild 。
MyHomePage
是不是太抽象?舉個簡單的例子,如下代碼所示:
- 我們定義了一個
控件,在控件内通過LikeScaffold
傳遞對象widget.body
- 在
内部我們使用了LikeScaffold
,模仿MediaQuery.of(context).viewInsets.bottom
裡使用Scaffold
MediaQuery
- 在
裡使用MyHomePage
,并給LikeScaffold
的 body 配置一個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
内的
"####### LikeScaffold build
也不會因為鍵盤的彈起而輸出,也就是:
LikeScaffold
雖然使用了
MediaQuery.of(context)
,但是它不再因為鍵盤的彈起而導緻 rebuild 。
因為此時
LikeScaffold
是
Scaffold
的 child ,是以在
LikeScaffold
内通過
MediaQuery.of(context)
指向的,其實是
Scaffold
内部經過處理的
MediaQueryData
。
在 内部有很多類似的處理,例如
Scaffold
裡會根據是否有
body
和
Appbar
來決定是否移除該區域内的 paddingTop 和 paddingBottom 。
BottomNavigationBar
是以,看到這裡有沒有想到什麼?為什麼時不時通過
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
,那麼彈出鍵盤時就會導緻頁面 rebuildMediaQueryData
-
用的是 MediaQuery.of
内的 Scaffold
,那麼擷取到的是 context
對于區域内的 Scaffold
,比如前面介紹過的 body ,同時擷取到的MediaQueryData
也會因為MediaQueryData
的配置不同而發生改變Scaffold
是以,如下動圖所示,其實部分人會在 push 對應路由地方,通過嵌套
MediaQuery
來做一些攔截處理,比如設定文本不可縮放,但是其實這樣會導緻鍵盤在彈出和收起時,觸發各個頁面不停 rebuild ,比如在 Page 2 彈出鍵盤的過程,Page 1 也在不停 rebuild。
return MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance!.window).copyWith(boldText: false),
child: MaterialApp(
useInheritedMediaQuery: true,
),
);
-
裡MediaQueryData
\viewInsets
\ padding
的差別viewPadding
-
和鍵盤狀态的關系MediaQuery
-
使用不同 context 對性能的影響MediaQuery.of
- 通過
内的Scaffold
擷取到的context
受到MediaQueryData
的影響Scaffold