我在使用flutter裡的對話方塊控制元件的時候遇到了一個奇怪的錯誤:
Another exception was thrown: Navigator operation requested with a context that does not include a Navigator
複製程式碼
研究了一下才知道,flutter裡的dialog不是隨便就能用的。
原始碼如下:
import 'package:flutter/material.dart';
main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Test',
home: new Scaffold(
appBar: new AppBar(title: new Text('Test')),
body: _buildCenterButton(context),
),
);
}
}
Widget _buildCenterButton(BuildContext context) {
return new Container(
alignment: Alignment.center,
child: new Container(
child: _buildButton(context),
));
}
Widget _buildButton(BuildContext context) {
return new RaisedButton(
padding: new EdgeInsets.fromLTRB(10.0, 10.0, 10.0, 10.0),
//padding
child: new Text(
'show dialog',
style: new TextStyle(
fontSize: 18.0, //textsize
color: Colors.white, // textcolor
),
),
color: Theme.of(context).accentColor,
elevation: 4.0,
//shadow
splashColor: Colors.blueGrey,
onPressed: () {
showAlertDialog(context);
});
}
void showAlertDialog(BuildContext context) {
showDialog(
context: context,
builder: (_) => new AlertDialog(
title: new Text("Dialog Title"),
content: new Text("This is my content"),
actions:<Widget>[
new FlatButton(child:new Text("CANCEL"), onPressed: (){
Navigator.of(context).pop();
},),
new FlatButton(child:new Text("OK"), onPressed: (){
Navigator.of(context).pop();
},)
]
));
}
複製程式碼
點選按鈕的時候沒有任何反應,控制檯的報錯是: Another exception was thrown: Navigator operation requested with a context that does not include a Navigator。大致意思是,context裡沒有Navigator物件,卻做了Navigator相關的操作。有點莫名其妙。
分析下原始碼吧~
看showDialog方法的原始碼:
Future<T> showDialog<T>({
@required BuildContext context,
bool barrierDismissible: true,
@Deprecated(
'Instead of using the "child" argument, return the child from a closure '
'provided to the "builder" argument. This will ensure that the BuildContext '
'is appropriate for widgets built in the dialog.'
) Widget child,
WidgetBuilder builder,
}) {
assert(child == null || builder == null);
return Navigator.of(context, rootNavigator: true/*注意這裡*/).push(new _DialogRoute<T>(
child: child ?? new Builder(builder: builder),
theme: Theme.of(context, shadowThemeOnly: true),
barrierDismissible: barrierDismissible,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
));
}
複製程式碼
Navigator.of 的原始碼:
static NavigatorState of(
BuildContext context, {
bool rootNavigator: false,
bool nullOk: false,
}) {
final NavigatorState navigator = rootNavigator
? context.rootAncestorStateOfType(const TypeMatcher<NavigatorState>())
: context.ancestorStateOfType(const TypeMatcher<NavigatorState>());
assert(() {
if (navigator == null && !nullOk) {
throw new FlutterError(
'Navigator operation requested with a context that does not include a Navigator.\n'
'The context used to push or pop routes from the Navigator must be that of a '
'widget that is a descendant of a Navigator widget.'
);
}
return true;
}());
return navigator;
}
複製程式碼
找到了一模一樣的錯誤資訊字串!看來就是因為Navigator.of(context)丟擲了一個FlutterError。 之所以出現這個錯誤,是因為滿足了if (navigator == null && !nullOk) 的條件, 也就是說: context.rootAncestorStateOfType(const TypeMatcher()) 是null。
Navigator.of函式有3個引數,第一個是BuildContext,第二個是rootNavigator,預設為false,可不傳,第三個是nullOk,預設為false,可不傳。rootNavigator的值決定了是呼叫ancestorStateOfType還是rootAncestorStateOfType,nullOk的值決定了如果最終結果為null值時該丟擲異常還是直接返回一個null。
我們做個測試,傳入不同的rootNavigator和nullOk的值,看有什麼結果:
void showAlertDialog(BuildContext context) {
try{
debugPrint("Navigator.of(context, rootNavigator=true, nullOk=false)="+
(Navigator.of(context, rootNavigator: true, nullOk: false)).toString());
}catch(e){
debugPrint("error1 " +e.toString());
}
try{
debugPrint("Navigator.of(context, rootNavigator=false, nullOk=false)="+
(Navigator.of(context, rootNavigator: false, nullOk: false)).toString());
}catch(e){
debugPrint("error2 " +e.toString());
}
try{
debugPrint("Navigator.of(context, rootNavigator=false, nullOk=true)="+
(Navigator.of(context, rootNavigator: false, nullOk: true)).toString());
}catch(e){
debugPrint("error3 " +e.toString());
}
//先註釋掉showDialog部分的程式碼
// showDialog(
// context: context,
// builder: (_) => new AlertDialog(
// title: new Text("Dialog Title"),
// content: new Text("This is my content"),
// actions:<Widget>[
// new FlatButton(child:new Text("CANCEL"), onPressed: (){
// Navigator.of(context).pop();
//
// },),
// new FlatButton(child:new Text("OK"), onPressed: (){
// Navigator.of(context).pop();
//
// },)
// ]
//
// ));
}
複製程式碼
列印結果:
error1 Navigator operation requested with a context that does not include a Navigator.
error2 Navigator operation requested with a context that does not include a Navigator.
Navigator.of(context, rootNavigator=false, nullOk=true)=null
複製程式碼
顯然,無論怎麼改rootNavigator和nullOk的值,Navigator.of(context, rootNavigator, nullOk)的值都是null。
為什麼呢?
rootAncestorStateOfType函式的實現位於framework.dart裡,我們可以看一下ancestorStateOfType和rootAncestorStateOfType的區別:
@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;
}
@override
State rootAncestorStateOfType(TypeMatcher matcher) {
assert(_debugCheckStateIsActiveForAncestorLookup());
Element ancestor = _parent;
StatefulElement statefulAncestor;
while (ancestor != null) {
if (ancestor is StatefulElement && matcher.check(ancestor.state))
statefulAncestor = ancestor;
ancestor = ancestor._parent;
}
return statefulAncestor?.state;
}
複製程式碼
可以看出: ancestorStateOfType的作用是: 如果某個父元素滿足一定條件, 則返回這個父節點的state屬性; rootAncestorStateOfType的作用是: 返回最頂層的滿足一定條件的父元素。 這個條件是: 這個元素必須屬於StatefulElement , 而且其state屬性與引數裡的TypeMatcher 相符合。
查詢原始碼可以知道:StatelessWidget 裡的元素是StatelessElement,StatefulWidget裡的元素是StatefulElement。
也就是說,要想讓context.rootAncestorStateOfType(const TypeMatcher())的返回值不為null, 必須保證context所在的Widget的頂層Widget屬於StatefulWidget(注意是頂層Widget,而不是自己所在的widget。如果context所在的Widget就是頂層Widget,也是不可以的)。
這樣我們就大概知道為什麼會出錯了。我們的showAlertDialog方法所用的context是屬於MyApp的, 而MyApp是個StatelessWidget。
那麼,修改方案就比較清晰了,我們的對話方塊所使用的context不能是頂層Widget的context,同時頂層Widget必須是StatefulWidget。
修改後的完整程式碼如下:
import 'package:flutter/material.dart';
main() {
runApp(new MyApp());
}
class MyApp extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return new MyState();
}
}
class MyState extends State<MyApp>{
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Test',
home: new Scaffold(
appBar: new AppBar(title: new Text('Test')),
body: new StatelessWidgetTest(),
),
);
}
}
class StatelessWidgetTest extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _buildCenterButton(context);
}
}
Widget _buildCenterButton(BuildContext context) {
return new Container(
alignment: Alignment.center,
child: new Container(
child: _buildButton(context),
));
}
Widget _buildButton(BuildContext context) {
return new RaisedButton(
padding: new EdgeInsets.fromLTRB(10.0, 10.0, 10.0, 10.0),
//padding
child: new Text(
'show dialog',
style: new TextStyle(
fontSize: 18.0, //textsize
color: Colors.white, // textcolor
),
),
color: Theme.of(context).accentColor,
elevation: 4.0,
//shadow
splashColor: Colors.blueGrey,
onPressed: () {
showAlertDialog(context);
});
}
void showAlertDialog(BuildContext context) {
NavigatorState navigator= context.rootAncestorStateOfType(const TypeMatcher<NavigatorState>());
debugPrint("navigator is null?"+(navigator==null).toString());
showDialog(
context: context,
builder: (_) => new AlertDialog(
title: new Text("Dialog Title"),
content: new Text("This is my content"),
actions:<Widget>[
new FlatButton(child:new Text("CANCEL"), onPressed: (){
Navigator.of(context).pop();
},),
new FlatButton(child:new Text("OK"), onPressed: (){
Navigator.of(context).pop();
},)
]
));
}
複製程式碼
實驗結果:
至於為什麼flutter裡的對話方塊控制元件對BuildContext的要求這麼嚴格,暫時還不清楚原因。
如何知道使用者點的是"OK"還是"CANCEL"呢?
我們可以借鑑下 github.com/flutter/flu… 官方demo裡的寫法:
Widget _buildButton(BuildContext context) {
return new RaisedButton(
padding: new EdgeInsets.fromLTRB(10.0, 10.0, 10.0, 10.0),
//padding
child: new Text(
'test dialog',
style: new TextStyle(
fontSize: 18.0, //textsize
color: Colors.white, // textcolor
),
),
color: Theme.of(context).accentColor,
elevation: 4.0,
//shadow
splashColor: Colors.blueGrey,
onPressed: () {
showDemoDialog<DialogItemAction>(
context: context,
child: new AlertDialog(
title: new Text("Dialog Title"),
content: new Text("This is is a dialog"),
actions: <Widget>[
new FlatButton(
child: new Text("CANCEL"),
onPressed: () {
Navigator.pop(context, DialogItemAction.cancel);
},
),
new FlatButton(
child: new Text("OK"),
onPressed: () {
Navigator.pop(context, DialogItemAction.agree);
},
)
]));
});
}
void showDemoDialog<T>({BuildContext context, Widget child}) {
showDialog<T>(
context: context,
builder: (BuildContext context) => child,
).then<void>((T value) {
// The value passed to Navigator.pop() or null.
if (value != null) {
_scaffoldKey.currentState.showSnackBar(
new SnackBar(content: new Text('hey, You selected: $value')));
}
});
}
複製程式碼
最終的完整原始碼:
import 'package:flutter/material.dart';
main() {
runApp(new MyApp());
}
class MyApp extends StatefulWidget {
@override
MyAppState createState() => new MyAppState();
}
class MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return new MaterialApp(title: 'Test', home: new TestMyApp());
}
}
class TestMyApp extends StatefulWidget {
@override
TestMyAppState createState() => new TestMyAppState();
}
enum DialogItemAction {
cancel,
agree,
}
class TestMyAppState extends State<TestMyApp> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
return new Scaffold(
key: _scaffoldKey,
appBar: new AppBar(title: const Text('Dialogs')),
body: _buildCenterButton(context));
}
Widget _buildCenterButton(BuildContext context) {
return new Container(
alignment: Alignment.center,
child: new Container(
child: _buildButton(context),
));
}
Widget _buildButton(BuildContext context) {
return new RaisedButton(
padding: new EdgeInsets.fromLTRB(10.0, 10.0, 10.0, 10.0),
//padding
child: new Text(
'test dialog',
style: new TextStyle(
fontSize: 18.0, //textsize
color: Colors.white, // textcolor
),
),
color: Theme.of(context).accentColor,
elevation: 4.0,
//shadow
splashColor: Colors.blueGrey,
onPressed: () {
showDemoDialog<DialogItemAction>(
context: context,
child: new AlertDialog(
title: new Text("Dialog Title"),
content: new Text("This is is a dialog"),
actions: <Widget>[
new FlatButton(
child: new Text("CANCEL"),
onPressed: () {
Navigator.pop(context, DialogItemAction.cancel);
},
),
new FlatButton(
child: new Text("OK"),
onPressed: () {
Navigator.pop(context, DialogItemAction.agree);
},
)
]));
});
}
void showDemoDialog<T>({BuildContext context, Widget child}) {
showDialog<T>(
context: context,
builder: (BuildContext context) => child,
).then<void>((T value) {
// The value passed to Navigator.pop() or null.
if (value != null) {
_scaffoldKey.currentState.showSnackBar(
new SnackBar(content: new Text('hey, You selected: $value')));
}
});
}
}
複製程式碼
後記:
在flutter裡,Widget,Element和BuildContext之間的關係是什麼呢?
摘抄部分系統原始碼如下:
abstract class Element extends DiagnosticableTree implements BuildContext{....}
abstract class ComponentElement extends Element {}
class StatelessElement extends ComponentElement {
@override
Widget build() => widget.build(this);
}
class StatefulElement extends ComponentElement {
@override
Widget build() => state.build(this);
}
abstract class Widget extends DiagnosticableTree {
Element createElement();
}
abstract class StatelessWidget extends Widget {
@override
StatelessElement createElement() => new StatelessElement(this);
@protected
Widget build(BuildContext context);
}
abstract class StatefulWidget extends Widget {
@override
StatefulElement createElement() => new StatefulElement(this);
@protected
State createState();
}
abstract class State<T extends StatefulWidget> extends Diagnosticable {
@protected
Widget build(BuildContext context);
}
複製程式碼