All in FlutterFlutter圈子Flutter

Flutter之Builder 控件

2020-02-03  本文已影响0人  向日花开

刚开始接触到 SnackBar 的时候,写了一个如下的小栗子:

import 'package:flutter/material.dart';

class BuilderApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BuilderHomePage(),
    );
  }
}

class BuilderHomePage extends StatefulWidget {
  @override
  _BuilderHomePageState createState() => _BuilderHomePageState();
}

class _BuilderHomePageState extends State<BuilderHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Hello"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            RaisedButton(
              onPressed: () {
                Scaffold.of(context).showSnackBar(
                    new SnackBar(content: Text("Hello  SnackBar")));
              },
              child: Text("Test"),
            ),
          ],
        ),
      ),
    );
  }
}

没想到运行的时候报如下错:

════════ 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.

No Scaffold ancestor could be found starting from the context that was passed to Scaffold.of(). This usually happens when the context provided is from the same StatefulWidget as that whose build function actually creates the Scaffold widget being sought.

There are several ways to avoid this problem. The simplest is to use a Builder to get a context that is "under" the Scaffold. For an example of this, please see the documentation for Scaffold.of():
  https://api.flutter.dev/flutter/material/Scaffold/of.html
A more efficient solution is to split your build function into several widgets. This introduces a new context from which you can obtain the Scaffold. In this solution, you would have an outer widget that creates the Scaffold populated by instances of your new inner widgets, and then in these inner widgets you would use Scaffold.of().
A less elegant but more expedient solution is assign a GlobalKey to the Scaffold, then use the key.currentState property to obtain the ScaffoldState rather than using the Scaffold.of() function.


当时包了一层 Builder 控件之后成功运行,但没深究,现在回过头来看看 Builder 到底有何神奇之处。

报错提示

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

意思是:

在不包含 Scaffold 的上下文(context)中调用了 Scaffold.of()。

现在回过头去看看上面的代码,Scaffold.of(context)传入的 context 是 build 方法中传入的。而此时 build 中 Scaffold 才刚刚创建,context 和 Scaffold 此时是同一级别的节点上的。

大胆尝试

报错既然提示在不包含 Scaffold 的上下文(context)中调用了 Scaffold.of(),那我们再创建一个子控件,在子控件中调用 Scaffold.of()。如下:


      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            RaisedButton(
              onPressed: () {
                Scaffold.of(context).showSnackBar(
                    new SnackBar(content: Text("Hello  SnackBar")));
              },
              child: Text("Test"),
            ),
            WidgetTest()
          ],
        ),
      ),

class WidgetTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child:     RaisedButton(
        onPressed: () {
          Scaffold.of(context).showSnackBar(
              new SnackBar(content: Text("Hello  SnackBar")));
        },
        child: Text("No Builder SnackBar"),
      ),
    );
  }
}

WidgetTest 中的 SnackBar 成功显示。为什么 WidgetTest 能成功,这答案还在 Scaffold.of(context)的源码中。

Scaffold.of() 源码

scaffold.dart
static ScaffoldState of(BuildContext context, { bool nullOk = false }) {
    assert(nullOk != null);
    assert(context != null);
    final ScaffoldState result =
    context.findAncestorStateOfType<ScaffoldState>();//调用BuildContext接口中的findAncestorStateOfType寻找对应节点
    if (nullOk || result != null)
      return result;
    throw FlutterError.fromParts(<DiagnosticsNode>[
     ...
    ]);
  }

framework.dart

  @override
  T findAncestorStateOfType<T extends State<StatefulWidget>>() {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    Element ancestor = _parent;//从父节点中开始找起
    while (ancestor != null) {
      if (ancestor is StatefulElement && ancestor.state is T)
        break;
      ancestor = ancestor._parent;
    }
    final StatefulElement statefulAncestor = ancestor;
    return statefulAncestor?.state;
  }

根据上面源码我们可知 of()方法会根据 context 通过 findAncestorStateOfType 方法去找对应的 ScaffoldState 实例,找到后成功返回,找不到则抛出异常。而在 findAncestorStateOfType 方法中,

是从 context 的父节点开始查找的。

此时你应该理解为什么 WidgetTest()可以成功运行,而初始案例不行了吧。WidgetTest()中调用的 Scaffold.of()传入的 context 是 WidgetTest 自己的,其父类节点正是 BuilderHomePage 中的 context 上的 ScaffoldState,而初始例子中传入的 context 父类节点中并没有 ScaffoldState 实例。

说了这么多终于要说到 Builder 控件了,使用 Builder 控件解决的代码形如:

 Builder(
              builder: (BuildContext context) {
                return RaisedButton(
                  onPressed: () {
                    Scaffold.of(context).showSnackBar(
                        new SnackBar(content: Text("Hello  SnackBar")));
                  },
                  child: Text("Builder SnackBar"),
                );
              },
        ),

为什么 Builder 能解决问题?也还得看源码:

framework.dart

typedef WidgetBuilder = Widget Function(BuildContext context);

basic.dart


/// A platonic widget that calls a closure to obtain its child widget.
///
/// See also:
///
///  * [StatefulBuilder], a platonic widget which also has state.
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 控件,但是需要传入一个 WidgetBuilder 类型的 builder 委托方法,用于把传入进来的 context 参数回调出去,但传传出去的 context 不在是 BuilderHomePage 的了,而是 Builder 控件自己的 context。

此处,Builder 就在 Scafflod 节点下,而在 Builder 中调用 Scafflod.of(context) 的 context 是 Builder 自己的 context,根据的 Builder 自己的 context 向上查找父节点正好找到 BuilderHomePage 中的 Scafflod。因此程序正常运行。

小结

共同学习

贴一张自己学习Flutter的公众号,感兴趣的小伙伴可以一起学习哦。。。

完整代码

import 'package:flutter/material.dart';

class BuilderApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BuilderHomePage(),
    );
  }
}

class BuilderHomePage extends StatefulWidget {
  @override
  _BuilderHomePageState createState() => _BuilderHomePageState();
}

class _BuilderHomePageState extends State<BuilderHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Hello"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            RaisedButton(
              onPressed: () {
                Scaffold.of(context).showSnackBar(
                    new SnackBar(content: Text("Hello  SnackBar")));
              },
              child: Text("Test"),
            ),
            Builder(
              builder: (BuildContext context) {
                return RaisedButton(
                  onPressed: () {
                    Scaffold.of(context).showSnackBar(
                        new SnackBar(content: Text("Hello  SnackBar")));
                  },
                  child: Text("Builder SnackBar"),
                );
              },
            ),
            WidgetTest()
          ],
        ),
      ),
    );
  }
}


class WidgetTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child:     RaisedButton(
        onPressed: () {
          Scaffold.of(context).showSnackBar(
              new SnackBar(content: Text("Hello  SnackBar")));
        },
        child: Text("No Builder SnackBar"),
      ),
    );
  }
}

上一篇下一篇

猜你喜欢

热点阅读