flutter demo (四):對話方塊

zty5678發表於2018-08-09

我在使用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裡,我們可以看一下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())的返回值不為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的要求這麼嚴格,暫時還不清楚原因。

如何知道使用者點的是"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);
}

複製程式碼

相關文章