Flutter 里的 BuildContext
相信大家都不會陌生迈着,雖然它叫 Context蛔趴,但是它實際是 Element 的抽象對象,而在 Flutter 里笨触,它主要來自于 ComponentElement
。
關于 ComponentElement
可以簡單介紹一下雹舀,在 Flutter 里根據(jù) Element 可以簡單地被歸納為兩類:
-
RenderObjectElement
:具備RenderObject
芦劣,擁有布局和繪制能力的 Element -
ComponentElement
:沒有RenderObject
,我們常用的StatelessWidget
和StatefulWidget
里對應的StatelessElement
和StatefulElement
就是它的子類说榆。
所以一般情況下虚吟,我們在 build
方法或者 State 里獲取到的 BuildContext
其實就是 ComponentElement
。
那使用 BuildContext
有什么需要注意的問題签财?
首先如下代碼所示串慰,在該例子里當用戶點擊 FloatingActionButton
的時候,代碼里做了一個 2秒的延遲唱蒸,然后才調用 pop
退出當前頁面邦鲫。
class _ControllerDemoPageState extends State<ControllerDemoPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
onPressed: () async {
await Future.delayed(Duration(seconds: 2));
Navigator.of(context).pop();
},
),
);
}
}
正常情況下是不會有什么問題,但是當用戶在點擊了 FloatingActionButton
之后神汹,又馬上點擊了 AppBar
返回退出應用庆捺,這時候就會出現(xiàn)以下的錯誤提示。
可以看到此時 log 說屁魏,Widget 對應的 Element 已經(jīng)不在了滔以,因為在 Navigator.of(context)
被調用時,context
對應的 Element 已經(jīng)隨著我們的退出銷毀氓拼。
一般情況下處理這個問題也很簡單你画,那就是增加 mounted
判斷,通過 mounted
判斷就可以避免上述的錯誤桃漾。
class _ControllerDemoPageState extends State<ControllerDemoPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
onPressed: () async {
await Future.delayed(Duration(seconds: 2));
if (!mounted) return;
Navigator.of(context).pop();
},
),
);
}
}
上面代碼里的 mounted
標識位來自于 State
坏匪,因為 State
是依附于 Element 創(chuàng)建,所以它可以感知 Element 的生命周期呈队,例如 mounted
就是判斷 _element != null;
剥槐。
那么到這里我們收獲了一個小技巧:使用 BuildContext
時,在必須時我們需要通過 mounted
來保證它的有效性宪摧。
那么單純使用 mounted
就可以滿足 context 優(yōu)化的要求了嗎粒竖?
如下代碼所示,在這個例子里:
- 我們添加了一個列表几于,使用
builder
構建 Item - 每個列表都有一個點擊事件
- 點擊列表時我們模擬網(wǎng)絡請求蕊苗,假設網(wǎng)絡也不是很好,所以延遲個 5 秒
- 之后我們滑動列表讓點擊的 Item 滑出屏幕不可見
class _ControllerDemoPageState extends State<ControllerDemoPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView.builder(
itemBuilder: (context, index) {
return ListItem();
},
itemCount: 30,
),
);
}
}
class ListItem extends StatefulWidget {
const ListItem({Key? key}) : super(key: key);
@override
State<ListItem> createState() => _ListItemState();
}
class _ListItemState extends State<ListItem> {
@override
Widget build(BuildContext context) {
return ListTile(
title: Container(
height: 160,
color: Colors.amber,
),
onTap: () async {
await Future.delayed(Duration(seconds: 5));
if(!mounted) return;
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("Tip")));
},
);
}
}
由于在 5 秒之內沿彭,Item 被劃出了屏幕朽砰,所以對應的 Elment 其實是被釋放了,從而由于 mounted
判斷喉刘,SnackBar
不會被彈出瞧柔。
那如果假設需要在開發(fā)時展示點擊數(shù)據(jù)上報的結果,也就是 Item 被釋放了還需要彈出睦裳,這時候需要如何處理造锅?
我們知道不管是 ScaffoldMessenger.of(context)
還是 Navigator.of(context)
,它本質還是通過 context
去往上查找對應的 InheritedWidget
泛型廉邑,所以其實我們可以提前獲取哥蔚。
所以,如下代碼所示蛛蒙,在 Future.delayed
之前我們就通過 ScaffoldMessenger.of(context);
獲取到 sm
對象糙箍,之后就算你直接退出當前的列表頁面,5秒過后 SnackBar
也能正常彈出牵祟。
class _ListItemState extends State<ListItem> {
@override
Widget build(BuildContext context) {
return ListTile(
title: Container(
height: 160,
color: Colors.amber,
),
onTap: () async {
var sm = ScaffoldMessenger.of(context);
await Future.delayed(Duration(seconds: 5));
sm.showSnackBar(SnackBar(content: Text("Tip")));
},
);
}
}
為什么頁面銷毀了深夯,但是 SnackBar
還能正常彈出 ?
因為此時通過 of(context);
獲取到的 ScaffoldMessenger
是存在 MaterialApp
里诺苹,所以就算頁面銷毀了也不影響 SnackBar
的執(zhí)行咕晋。
但是如果我們修改例子,如下代碼所示筝尾,在 Scaffold
上面多嵌套一個 ScaffoldMessenger
捡需,這時候在 Item 里通過 ScaffoldMessenger.of(context)
獲取到的就會是當前頁面下的 ScaffoldMessenger
。
class _ControllerDemoPageState extends State<ControllerDemoPage> {
@override
Widget build(BuildContext context) {
return ScaffoldMessenger(
child: Scaffold(
appBar: AppBar(),
body: ListView.builder(
itemBuilder: (context, index) {
return ListItem();
},
itemCount: 30,
),
),
);
}
}
這種情況下我們只能保證Item 不可見的時候 SnackBar
還能正常彈出筹淫, 而如果這時候我們直接退出頁面站辉,還是會出現(xiàn)以下的錯誤提示,因為 ScaffoldMessenger
也被銷毀了 损姜。
所以到這里我們收獲第二個小技巧:在異步操作里使用 of(context)
饰剥,可以提前獲取,之后再做異步操作摧阅,這樣可以盡量保證流程可以完整執(zhí)行汰蓉。
既然我們說到通過 of(context)
去獲取上層共享往下共享的 InheritedWidget
,那在哪里獲取就比較好棒卷?
還記得前面的 log 嗎顾孽?在第一個例子出錯時祝钢,log 里就提示了一個方法,也就是 State 的 didChangeDependencies
方法若厚。
為什么是官方會建議在這個方法里去調用 of(context)
拦英?
首先前面我們一直說,通過 of(context)
獲取到的是 InheritedWidget
测秸,而 當 InheritedWidget
發(fā)生改變時疤估,就是通過觸發(fā)綁定過的 Element 里 State 的didChangeDependencies
來觸發(fā)更新,所以在 didChangeDependencies
里調用 of(context)
有較好的因果關系霎冯。
對于這部分內容感興趣的铃拇,可以看 Flutter 小技巧之 MediaQuery 和 build 優(yōu)化你不知道的秘密 和 全面理解State與Provider 。
那我能在 initState
里提前調用嗎沈撞?
當然不行慷荔,首先如果在 initState
直接調用如 ScaffoldMessenger.of(context).showSnackBar
方法,就會看到以下的錯誤提示关串。
這是因為 Element 里會判斷此時的 _StateLifecycle
狀態(tài)拧廊,如果此時是 _StateLifecycle.created
或者 _StateLifecycle.defunct
,也就是在 initState
和 dispose
晋修,是不允許執(zhí)行 of(context)
操作吧碾。
of(context)
操作指的是context.dependOnInheritedWidgetOfExactTyp
。
當然墓卦,如果你硬是想在 initState
下調用也行倦春,增加一個 Future
執(zhí)行就可以成功執(zhí)行
@override
void initState() {
super.initState();
Future((){
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("initState")));
});
}
簡單理解,因為 Dart 是單線程輪詢執(zhí)行落剪,
initState
里的Future
相當于是下一次輪詢睁本,自然也就不在_StateLifecycle.created
的狀態(tài)下。
那我在 build
里直接調用不行嗎忠怖?
直接在 build
里調用肯定可以呢堰,雖然 build
會被比較頻繁執(zhí)行,但是 of(context)
操作其實就是在一個 map 里通過 key - value 獲取泛型對象凡泣,所以對性能不會有太大的影響枉疼。
真正對性能有影響的是 of(context)
的綁定數(shù)量和獲取到對象之后的自定義邏輯,例如你通過 MediaQuery.of(context).size
獲取到屏幕大小之后鞋拟,通過一系列復雜計算來定位你的控件骂维。
@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
var padding = MediaQuery.of(context).padding;
var width = size.width / 2;
var height = size.width / size.height * (30 - padding.bottom);
return Container(
color: Colors.amber,
width: width,
height: height,
);
}
例如上面這段代碼,可能會導致鍵盤在彈出的時候贺纲,雖然當前頁面并沒有完全展示航闺,但是也會導致你的控件不斷重新計算從而出現(xiàn)卡頓。
所以到這里我們又收獲了一個小技巧: 對于 of(context)
的相關操作邏輯,可以盡量放到 didChangeDependencies
里去處理潦刃。
最后侮措,今天主要分享了在使用 BuildContext
時的一些注意事項和技巧,如果你對于這方面還有什么疑問福铅,歡迎留言評論萝毛。
本文轉自 https://juejin.cn/post/7122409135055831053项阴,如有侵權滑黔,請聯(lián)系刪除。