flutter demo (四):对话框

我在使用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<NavigatorState>()) 是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里,我们可以看一下ancestorStateOfTyperootAncestorStateOfType的区别:

@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<NavigatorState>())的返回值不为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();

            },)
          ]
      ));
}

实验结果:

screen1.png

至于为什么flutter里的对话框控件对BuildContext的要求这么严格,暂时还不清楚原因。


后记:

在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);
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,639评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,093评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,079评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,329评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,343评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,047评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,645评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,565评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,095评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,201评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,338评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,014评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,701评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,194评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,320评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,685评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,345评论 2 358