事故回放
一朋友面試,被問(wèn)到在Flutter中一些因 context
引起的路由異常的問(wèn)題厨钻,為什么包裝一層 Builder
控件之后扼雏,路由或點(diǎn)擊彈框事件正常使用了?然后就沒(méi)然后了夯膀。诗充。。相信很多人都會(huì)用诱建,至于為什么蝴蜓,也沒(méi)深究。
相信很多剛開(kāi)始玩Flutter的同學(xué)都會(huì)在學(xué)習(xí)過(guò)程中都會(huì)寫(xiě)到類(lèi)似下面的這種代碼:
import 'package:flutter/material.dart';
class BuilderA extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('666666'),
));
},
child: Center(
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
),
),
);
}
}
開(kāi)開(kāi)心心寫(xiě)完俺猿,然后一頓運(yùn)行:
void main() => runApp(BuilderA());
點(diǎn)擊茎匠,發(fā)現(xiàn) SnackBar
并沒(méi)有正常彈出,而是出現(xiàn)了下面這種異常:
════════ Exception caught by gesture
═══════════════════════════════════════════════════════════════
The following assertion was thrown while handling a gesture:
<font color=red>Scaffold.of() called with a context that does not contain a Scaffold.</font>
...
網(wǎng)上很多資料都說(shuō)需要外包一層 Builder
可以解決這種問(wèn)題押袍,但是基本上沒(méi)說(shuō)原因诵冒,至于為什么說(shuō)可以外包一層 Builder
就可以解決,我想大部分只是看了 Scaffold
的源碼中的注釋了解到的:
scaffold.dart 第1209行到1234行:
...
/// {@tool snippet --template=stateless_widget_material}
/// When the [Scaffold] is actually created in the same `build` function, the
/// `context` argument to the `build` function can't be used to find the
/// [Scaffold] (since it's "above" the widget being returned in the widget
/// tree). In such cases, the following technique with a [Builder] can be used
/// to provide a new scope with a [BuildContext] that is "under" the
/// [Scaffold]:
///
/// ```dart
/// Widget build(BuildContext context) {
/// return Scaffold(
/// appBar: AppBar(
/// title: Text('Demo')
/// ),
/// body: Builder(
/// // Create an inner BuildContext so that the onPressed methods
/// // can refer to the Scaffold with Scaffold.of().
/// builder: (BuildContext context) {
/// return Center(
/// child: RaisedButton(
/// child: Text('SHOW A SNACKBAR'),
/// onPressed: () {
/// Scaffold.of(context).showSnackBar(SnackBar(
/// content: Text('Have a snack!'),
/// ));
/// },
...
那到底是什么原因外包一層 Builder
控件就可以了呢谊惭?
原因分析
異常原因
上面那種寫(xiě)法為什么會(huì)異常汽馋?要想知道這個(gè)問(wèn)題,我們首先看這句描述:
Scaffold.of() called with a context that does not contain a Scaffold.
意思是說(shuō)在不包含Scaffold的上下文中調(diào)用了Scaffold.of()圈盔。
我們仔細(xì)看看這個(gè)代碼惭蟋,會(huì)發(fā)現(xiàn),此處調(diào)用的 context
是 BuilderA
的药磺,而在BuilderA
中的 build
方法中我們才指定了 Scaffold
告组,因此確實(shí)是不存的。
為什么包一層Builder就沒(méi)問(wèn)題了癌佩?
我們把代碼改成下面這種:
class BuilderB extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Builder(
builder: (context) => GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('666666'),
));
},
child: Center(
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
),
),
),
);
}
}
運(yùn)行之后發(fā)現(xiàn)確實(shí)沒(méi)問(wèn)題了木缝?為什么呢?我們先來(lái)看看 Builder
源碼:
// ##### framework.dart文件下
typedef WidgetBuilder = Widget Function(BuildContext context);
// ##### basic.dart文件下
class Builder extends StatelessWidget {
/// Creates a widget that delegates its build to a callback.
///
/// The [builder] argument must not be null.
const Builder({
Key key,
@required this.builder,
}) : assert(builder != null),
super(key: key);
/// Called to obtain the child widget.
///
/// This function is called whenever this widget is included in its parent's
/// build and the old widget (if any) that it synchronizes with has a distinct
/// object identity. Typically the parent's build method will construct
/// a new tree of widgets and so a new Builder child will not be [identical]
/// to the corresponding old one.
final WidgetBuilder builder;
@override
Widget build(BuildContext context) => builder(context);
}
代碼很簡(jiǎn)單围辙,Builder
類(lèi)繼承 StatelessWidget
我碟,然后通過(guò)一個(gè)接口回調(diào)將自己對(duì)應(yīng)的 context
回調(diào)出來(lái),供外部使用姚建。沒(méi)了~
但是矫俺!外部調(diào)用:
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('666666'),
));
}
此時(shí)的 context
將不再是 BuilderB
的 context
了,而是 Builder
自己的了!@逋小友雳!
那么問(wèn)題又來(lái)了~~~憑什么改成 Builder
中的 context
就可以了?我能這個(gè)時(shí)候就不得不去看看 Scaffold.of(context)
的源碼了:
...
static ScaffoldState of(BuildContext context, { bool nullOk = false }) {
assert(nullOk != null);
assert(context != null);
final ScaffoldState result = context.ancestorStateOfType(const TypeMatcher<ScaffoldState>());
if (nullOk || result != null)
return result;
throw FlutterError(
...省略不重要的
@override
State ancestorStateOfType(TypeMatcher matcher) {
assert(_debugCheckStateIsActiveForAncestorLookup());
Element ancestor = _parent;
while (ancestor != null) {
if (ancestor is StatefulElement && matcher.check(ancestor.state))
break;
ancestor = ancestor._parent;
}
final StatefulElement statefulAncestor = ancestor;
return statefulAncestor?.state;
}
上面的核心部分揭露了原因:
of()
方法中會(huì)根據(jù)傳入的 context
去尋找最近的相匹配的祖先 widget
铅匹,如果尋找到返回結(jié)果押赊,否則拋出異常,拋出的異常就是上面出現(xiàn)的異常包斑!
此處流礁,Builder
就在 Scafflod
節(jié)點(diǎn)下,因在 Builder
中調(diào)用 Scafflod.of(context)
剛好是根據(jù) Builder
中的 context
向上尋找最近的祖先罗丰,然后就找到了對(duì)應(yīng)的 Scafflod
神帅,因此這也就是為什么包裝了一層 Builder
后就能正常的原因!
總結(jié)時(shí)刻
-
Builder
控件的作用萌抵,我的理解是在于重新提供一個(gè)新的子context
找御,通過(guò)新的context
關(guān)聯(lián)到相關(guān)祖先從而達(dá)到正常操作的目的。 - 同樣的對(duì)于路由跳轉(zhuǎn)
Navigator.of(context)
【注:Navigator
是由MaterialApp
提供的】 等類(lèi)似的問(wèn)題谜嫉,采用的都是類(lèi)似的原理萎坷,只要搞懂了其中一個(gè),其他的都不在話下沐兰!
當(dāng)然哆档,處理這類(lèi)問(wèn)題不僅僅這一種思路,道路千萬(wàn)條住闯,找到符合自己的那一條才是關(guān)鍵瓜浸!