Flutter | 一文搞懂 BuildContext

2,698 阅读4分钟

bc602a6bda0e49b7acda73aa54ad6c4f_tplv-k3u1fbpfcp-watermark.jpg

概述

[BuildContext] objects are actually [Element] objects. The [BuildContext] ,interface is used to discourage direct manipulation of [Element] objects.

翻译过来的意思就是 [BuildContext] 对象实际上是 [Element] 对象。 [BuildContext] 接口用于阻止直接操作 [Element] 对象。

根据官方的注释,我们可以知道 BuildContext 实际上就是 Element 对象,主要是为了防止开发者直接操作 Element 对象。通过源码我们也可以看到 Element 是实现了 BuildContext 这个抽象类

abstract class Element extends DiagnosticableTree implements BuildContext {}

BuildContext 的作用

在之前的一篇文章中讲过 Element 和 Widget 对应的关系,不太清楚的可以看一下

Element 是 Widget 树中特定位置所对应的实例,Widget 的状态都会保存在 Element 当中。

那么 BuildContext 到底能干什么呢?只要是 Element 能做的事情,BuildContext 基本都能做,如:

var size = (context.findRenderObjec  var size = (context.findRenderObject() as RenderBox).size;
var local = (context.findRenderObject() as RenderBox).localToGlobal;
var widget = context.widget;t() as RenderBox).size;

例如上面通过 context 之前获取到宽高度,距离左上角的偏移,element 对应的 widget 等

因为 Elment 是继承自 BuildContext ,我们甚至可以通过 context 来直接刷新 Element 的状态,如下:

(context as Element).markNeedsBuild();

这样就可以直接对当前的 Element 进行刷新,而不必去通过 SetState,但是这种做法是极其的不推荐的。

其实在 SetState 中,最终也是调用的 markNeedsBuild 方法,如下:

void setState(VoidCallback fn) {
  assert(fn != null);
  assert(() {
    if (_debugLifecycleState == _StateLifecycle.defunct) {
     ///......
    }
    if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
     ///......
    }
    return true;
  }());
  final dynamic result = fn() as dynamic;
  assert(() {
    if (result is Future) {
      ///......
      ]);
    }
    return true;
  }());
  ///最终调用
  _element!.markNeedsBuild();
}

我们在写代码的过程中还会发现一个问题,就是要更新的状态不是必须要写在 setState 里面,只要写在 setState 上面 即可,这样也没有问题,例如有些其他的响应式框架就没有这个回调,只提供了一个通知页面刷新的方法,早期的 flutter 也是如此。但是最后发现了这个问题的弊端了,如大多数人会在每个方法的后面加一个 setState,导致过度的开销,并且在删除的时候也是不知道这个这个 setState 到底有没有实际的意义,这就会造成一些不必要的麻烦。

所以 Flutter 在 setState 中加了一个回调,我们可以需要更新的状态直接放在回调里面,和状态没关系的放在外边即可。


常见的一些方法

  • (context as Element).findAncestorStateOfType()

    沿着当前的 Element 向上寻找,直到直到一个特定的类型之后,将他的 State 返回

  • (context as Element).findRenderObject()

    获取 Element 渲染的对象

  • (context as Element).findAncestorRenderObjectOfType()

    向上遍历,获取与泛型对应的渲染对象

  • (context as Element).findAncestorWidgetOfExactType()

    遍历,获取与 T 对应的 Widget

上面这些方法在源码中还是有一些使用的栗子的,例如:

  • Scaffold.of(context).showSnackBar()

在 Scaffold 的底部显示一个 SnackBar

static ScaffoldState of(BuildContext context) {
  assert(context != null);
  final ScaffoldState? result = context.findAncestorStateOfType<ScaffoldState>();
  if (result != null)
    return result;
//......    
}

查看 of 方法,可以发现,里面使用的就是 findAncestorStateOfType 方法来获取的 Scaffold 的状态,最终来实现一些操作,

  • Theme.of(context).accentColor

    我们可以通过如上的方法来获取一下主题颜色等,其内部实现如下:

    static ThemeData of(BuildContext context) {
      final _InheritedTheme? inheritedTheme = context.dependOnInheritedWidgetOfExactType<_InheritedTheme>();
      final MaterialLocalizations? localizations = Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
      final ScriptCategory category = localizations?.scriptCategory ?? ScriptCategory.englishLike;
      final ThemeData theme = inheritedTheme?.theme.data ?? _kFallbackTheme;
      return ThemeData.localize(theme, theme.typography.geometryThemeFor(category));
    }
    

    它和上面的一样,也是找到离你最近的 _InheritedTheme,最后再将它还给你


栗子

写一个侧滑栏,通过点击按钮来实现打开 侧滑栏

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      drawer: Drawer(),
      floatingActionButton: FloatingActionButton(onPressed: () {
        Scaffold.of(context).openDrawer();
      }),
    );
  }
}

运行代码,就会发现报错:Scaffold.of() called with a context that does not contain a Scaffold.

意思就是当前的 context 里面没有找到 Drawer,所以无法打开。

为什么呢? 因为这个 context 是当前 MyHomePage 这个层级的,在他的上层确实没有 Drawer,所以自然也就没有办法打开了。 那么如何解决呢?如下:

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text(widget.title)),
        drawer: Drawer(),
        floatingActionButton: Floating());
  }
}

class Floating extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(onPressed: () {
      Scaffold.of(context).openDrawer();
    });
  }
}

修改为如上代码即可解决。

在 Floating 中的 context 是 MyHomePage 下面的层级,所以说他的上级时候 Scaffold 的,自然也就不会报错了。

但是一般这种情况下,我们是不用多创建一个组件的,所以我们还需要一个更好的解决方案,如下:

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text(widget.title)),
        drawer: Drawer(),
        floatingActionButton: Builder(
          builder: (context) {
            return FloatingActionButton(onPressed: () {
              Scaffold.of(context).openDrawer();
            });
          },
        ));
  }
}

我们可以通过 Builder 来创建一个匿名的组件就可以了。


参考文献

B站王叔不秃

如果本文有帮助到你的地方,不胜荣幸,如有文章中有错误和疑问,欢迎大家提出!