美文网首页
flutter demo (四):对话框

flutter demo (四):对话框

作者: zty5678 | 来源:发表于2018-08-09 14:53 被阅读4879次

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

    相关文章

      网友评论

          本文标题:flutter demo (四):对话框

          本文链接:https://www.haomeiwen.com/subject/eihqbftx.html