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();

            },)
          ]

      ));
}

點擊按鈕的時候沒有任何反應(yīng)漱竖,控制臺的報錯是:
Another exception was thrown: Navigator operation requested with a context that does not include a Navigator津滞。大致意思是椭坚,context里沒有Navigator對象蔼啦,卻做了Navigator相關(guān)的操作辣吃。有點莫名其妙动遭。

分析下源碼吧~

看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神得。
之所以出現(xiàn)這個錯誤厘惦,是因為滿足了if (navigator == null && !nullOk) 的條件, 也就是說:
context.rootAncestorStateOfType(const TypeMatcher<NavigatorState>()) 是null哩簿。

Navigator.of函數(shù)有3個參數(shù)宵蕉,第一個是BuildContext酝静,第二個是rootNavigator,默認(rèn)為false羡玛,可不傳别智,第三個是nullOk,默認(rèn)為false稼稿,可不傳薄榛。rootNavigator的值決定了是調(diào)用ancestorStateOfType還是rootAncestorStateOfType,nullOk的值決定了如果最終結(jié)果為null值時該拋出異常還是直接返回一個null让歼。

我們做個測試敞恋,傳入不同的rootNavigator和nullOk的值,看有什么結(jié)果:

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();
//
//            },)
//          ]
//
//      ));
}

打印結(jié)果:

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函數(shù)的實現(xiàn)位于framework.dart里,我們可以看一下ancestorStateOfTyperootAncestorStateOfType的區(qū)別:

@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的作用是: 如果某個父元素滿足一定條件, 則返回這個父節(jié)點的state屬性馏予;
rootAncestorStateOfType的作用是: 返回最頂層的滿足一定條件的父元素天梧。
這個條件是: 這個元素必須屬于StatefulElement , 而且其state屬性與參數(shù)里的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();

            },)
          ]
      ));
}

實驗結(jié)果:

screen1.png

至于為什么flutter里的對話框控件對BuildContext的要求這么嚴(yán)格,暫時還不清楚原因猜丹。


后記:

在flutter里芝加,Widget,Element和BuildContext之間的關(guān)系是什么呢?

摘抄部分系統(tǒng)源碼如下:

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末射窒,一起剝皮案震驚了整個濱河市藏杖,隨后出現(xiàn)的幾起案子将塑,更是在濱河造成了極大的恐慌,老刑警劉巖制市,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抬旺,死亡現(xiàn)場離奇詭異,居然都是意外死亡祥楣,警方通過查閱死者的電腦和手機(jī)开财,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來误褪,“玉大人责鳍,你說我怎么就攤上這事∈藜洌” “怎么了历葛?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長嘀略。 經(jīng)常有香客問我恤溶,道長,這世上最難降的妖魔是什么帜羊? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任咒程,我火速辦了婚禮,結(jié)果婚禮上讼育,老公的妹妹穿的比我還像新娘帐姻。我一直安慰自己,他們只是感情好奶段,可當(dāng)我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布饥瓷。 她就那樣靜靜地躺著,像睡著了一般痹籍。 火紅的嫁衣襯著肌膚如雪呢铆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天蹲缠,我揣著相機(jī)與錄音刺洒,去河邊找鬼。 笑死吼砂,一個胖子當(dāng)著我的面吹牛逆航,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播渔肩,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼因俐,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起抹剩,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤撑帖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后澳眷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胡嘿,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年钳踊,在試婚紗的時候發(fā)現(xiàn)自己被綠了衷敌。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡拓瞪,死狀恐怖缴罗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情祭埂,我是刑警寧澤面氓,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站蛆橡,受9級特大地震影響舌界,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜泰演,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一禀横、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧粥血,春花似錦、人聲如沸酿箭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缭嫡。三九已至缔御,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間妇蛀,已是汗流浹背耕突。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留评架,地道東北人眷茁。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像纵诞,于是被迫代替她去往敵國和親上祈。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,933評論 2 355