Flutter-为什么包装一层Builder控件之后,路由或点击弹框事件正常使用了?

事故回放

一朋友面试,被问到在Flutter中一些因 context 引起的路由异常的问题,为什么包装一层 Builder 控件之后,路由或点击弹框事件正常使用了?然后就没然后了。。。相信很多人都会用,至于为什么,也没深究。

相信很多刚开始玩Flutter的同学都会在学习过程中都会写到类似下面的这种代码:

import 'package:flutter/material.dart';

class BuilderA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: GestureDetector(
          onTap: () {
            Scaffold.of(context).showSnackBar(SnackBar(
              content: Text('666666'),
            ));
          },
          child: Center(
            child: Container(
              width: 100,
              height: 100,
              color: Colors.red,
            ),
          ),
        ),
      ),
    );
  }
}

开开心心写完,然后一顿运行:

void main() => runApp(BuilderA());

点击,发现 SnackBar 并没有正常弹出,而是出现了下面这种异常:

════════ Exception caught by gesture
═══════════════════════════════════════════════════════════════

The following assertion was thrown while handling a gesture:

<font color=red>Scaffold.of() called with a context that does not contain a Scaffold.</font>

...

网上很多资料都说需要外包一层 Builder 可以解决这种问题,但是基本上没说原因,至于为什么说可以外包一层 Builder 就可以解决,我想大部分只是看了 Scaffold 的源码中的注释了解到的:

scaffold.dart 第1209行到1234行:
...
/// {@tool snippet --template=stateless_widget_material}
/// When the [Scaffold] is actually created in the same `build` function, the
/// `context` argument to the `build` function can't be used to find the
/// [Scaffold] (since it's "above" the widget being returned in the widget
/// tree). In such cases, the following technique with a [Builder] can be used
/// to provide a new scope with a [BuildContext] that is "under" the
/// [Scaffold]:
///
/// ```dart
/// Widget build(BuildContext context) {
///   return Scaffold(
///     appBar: AppBar(
///       title: Text('Demo')
///     ),
///     body: Builder(
///       // Create an inner BuildContext so that the onPressed methods
///       // can refer to the Scaffold with Scaffold.of().
///       builder: (BuildContext context) {
///         return Center(
///           child: RaisedButton(
///             child: Text('SHOW A SNACKBAR'),
///             onPressed: () {
///               Scaffold.of(context).showSnackBar(SnackBar(
///                 content: Text('Have a snack!'),
///               ));
///             },
...

那到底是什么原因外包一层 Builder 控件就可以了呢?


原因分析

异常原因

上面那种写法为什么会异常?要想知道这个问题,我们首先看这句描述:

Scaffold.of() called with a context that does not contain a Scaffold.

意思是说在不包含Scaffold的上下文中调用了Scaffold.of()

我们仔细看看这个代码,会发现,此处调用的 contextBuilderA 的,而在BuilderA中的 build 方法中我们才指定了 Scaffold ,因此确实是不存的。

为什么包一层Builder就没问题了?

我们把代码改成下面这种:

class BuilderB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Builder(
          builder: (context) => GestureDetector(
            onTap: () {
              Scaffold.of(context).showSnackBar(SnackBar(
                content: Text('666666'),
              ));
            },
            child: Center(
              child: Container(
                width: 100,
                height: 100,
                color: Colors.red,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

运行之后发现确实没问题了?为什么呢?我们先来看看 Builder 源码:

// ##### framework.dart文件下
typedef WidgetBuilder = Widget Function(BuildContext context);


// ##### basic.dart文件下
class Builder extends StatelessWidget {
  /// Creates a widget that delegates its build to a callback.
  ///
  /// The [builder] argument must not be null.
  const Builder({
    Key key,
    @required this.builder,
  }) : assert(builder != null),
       super(key: key);

  /// Called to obtain the child widget.
  ///
  /// This function is called whenever this widget is included in its parent's
  /// build and the old widget (if any) that it synchronizes with has a distinct
  /// object identity. Typically the parent's build method will construct
  /// a new tree of widgets and so a new Builder child will not be [identical]
  /// to the corresponding old one.
  final WidgetBuilder builder;

  @override
  Widget build(BuildContext context) => builder(context);
}

代码很简单,Builder 类继承 StatelessWidget ,然后通过一个接口回调将自己对应的 context 回调出来,供外部使用。没了~
但是!外部调用:

onTap: () {
  Scaffold.of(context).showSnackBar(SnackBar(
    content: Text('666666'),
  ));
}

此时的 context 将不再是 BuilderBcontext 了,而是 Builder 自己的了!!!

那么问题又来了~~~凭什么改成 Builder 中的 context 就可以了?我能这个时候就不得不去看看 Scaffold.of(context) 的源码了:

...

static ScaffoldState of(BuildContext context, { bool nullOk = false }) {
    assert(nullOk != null);
    assert(context != null);
    final ScaffoldState result = context.ancestorStateOfType(const TypeMatcher<ScaffoldState>());
    if (nullOk || result != null)
      return result;
    throw FlutterError(
      
...省略不重要的
  @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;
  } 

上面的核心部分揭露了原因:
of() 方法中会根据传入的 context 去寻找最近的相匹配的祖先 widget,如果寻找到返回结果,否则抛出异常,抛出的异常就是上面出现的异常!

此处,Builder 就在 Scafflod 节点下,因在 Builder 中调用 Scafflod.of(context) 刚好是根据 Builder 中的 context 向上寻找最近的祖先,然后就找到了对应的 Scafflod,因此这也就是为什么包装了一层 Builder 后就能正常的原因!

总结时刻

  • Builder 控件的作用,我的理解是在于重新提供一个新的子 context ,通过新的 context 关联到相关祖先从而达到正常操作的目的。
  • 同样的对于路由跳转 Navigator.of(context)【注:Navigator 是由 MaterialApp 提供的】 等类似的问题,采用的都是类似的原理,只要搞懂了其中一个,其他的都不在话下!

当然,处理这类问题不仅仅这一种思路,道路千万条,找到符合自己的那一条才是关键!

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

推荐阅读更多精彩内容