事故回放
一朋友面试,被问到在Flutter中一些因 context
引起的路由异常的问题,为什么包装一层 Builder
控件之后,路由或点击弹框事件正常使用了?然后就没然后了。。。相信很多人都会用,至于为什么,也没深究。
相信很多刚开始玩Flutter的同学都会在学习过程中都会写到类似下面的这种代码:
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,
),
),
),
),
);
}
}
开开心心写完,然后一顿运行:
void main() => runApp(BuilderA());
点击,发现 SnackBar
并没有正常弹出,而是出现了下面这种异常:
════════ Exception caught by gesture
═══════════════════════════════════════════════════════════════
The following assertion was thrown while handling a gesture:
Scaffold.of() called with a context that does not contain a Scaffold.
...
网上很多资料都说需要外包一层 Builder
可以解决这种问题,但是基本上没说原因,至于为什么说可以外包一层 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
控件就可以了呢?
原因分析
异常原因
上面那种写法为什么会异常?要想知道这个问题,我们首先看这句描述:
Scaffold.of() called with a context that does not contain a Scaffold.
意思是说在不包含Scaffold的上下文中调用了Scaffold.of()。
我们仔细看看这个代码,会发现,此处调用的 context
是 BuilderA
的,而在BuilderA
中的 build
方法中我们才指定了 Scaffold
,因此确实是不存的。
为什么包一层Builder就没问题了?
我们把代码改成下面这种:
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,
),
),
),
),
),
);
}
}
运行之后发现确实没问题了?为什么呢?我们先来看看 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);
}
代码很简单,Builder
类继承 StatelessWidget
,然后通过一个接口回调将自己对应的 context
回调出来,供外部使用。没了~
但是!外部调用:
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('666666'),
));
}
此时的 context
将不再是 BuilderB
的 context
了,而是 Builder
自己的了!!!
那么问题又来了~~~凭什么改成 Builder
中的 context
就可以了?我能这个时候就不得不去看看 Scaffold.of(context)
的源码了:
...
static ScaffoldState of(BuildContext context, { bool nullOk = false }) {
assert(nullOk != null);
assert(context != null);
final ScaffoldState result = context.ancestorStateOfType(const TypeMatcher());
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()
方法中会根据传入的 context
去寻找最近的相匹配的祖先 widget
,如果寻找到返回结果,否则抛出异常,抛出的异常就是上面出现的异常!
此处,Builder
就在 Scafflod
节点下,因在 Builder
中调用 Scafflod.of(context)
刚好是根据 Builder
中的 context
向上寻找最近的祖先,然后就找到了对应的 Scafflod
,因此这也就是为什么包装了一层 Builder
后就能正常的原因!
总结时刻
-
Builder
控件的作用,我的理解是在于重新提供一个新的子context
,通过新的context
关联到相关祖先从而达到正常操作的目的。 - 同样的对于路由跳转
Navigator.of(context)
【注:Navigator
是由MaterialApp
提供的】 等类似的问题,采用的都是类似的原理,只要搞懂了其中一个,其他的都不在话下!
当然,处理这类问题不仅仅这一种思路,道路千万条,找到符合自己的那一条才是关键!