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

事故回放

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

相信不少刚开始玩Flutter的同窗都会在学习过程当中都会写到相似下面的这种代码:markdown

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,
            ),
          ),
        ),
      ),
    );
  }
}
复制代码

开开心心写完,而后一顿运行:app

void main() => runApp(BuilderA());
复制代码

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

════════ Exception caught by gesture
═══════════════════════════════════════════════════════════════
The following assertion was thrown while handling a gesture:
Scaffold.of() called with a context that does not contain a Scaffold.
...ide

网上不少资料都说须要外包一层 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 控件就能够了呢?ui


缘由分析

异常缘由

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

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

意思是说在不包含Scaffold的上下文中调用了Scaffold.of()
咱们仔细看看这个代码,会发现,此处调用的 contextBuilderA 的,而在BuilderA中的 build 方法中咱们才指定了 Scaffold ,所以确实是不存的。debug

为何包一层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 提供的】 等相似的问题,采用的都是相似的原理,只要搞懂了其中一个,其余的都不在话下!

固然,处理这类问题不单单这一种思路,道路千万条,找到符合本身的那一条才是关键!

相关文章
相关标签/搜索