阅读 1110

Flutter 组件 | Builder 构造器与 BuildContext 认知

1. Builder 组件引言

这篇文章来讲一个只有 10 行代码 的组件: Builder ,下面是它的全部代码。虽然非常简单,但通过它,仍然能让你了解很多知识。

typedef WidgetBuilder = Widget Function(BuildContext context);

class Builder extends StatelessWidget {
  const Builder({
    Key key,
    @required this.builder,
  }) : assert(builder != null),
       super(key: key);

  final WidgetBuilder builder;

  @override
  Widget build(BuildContext context) => builder(context);
}
复制代码

可以看出 Flutter 源码中有不少组件是依赖于 Builder 组件实现的。这个小东西何德何能呢?通过本文,你会对它,甚至对 Flutter 有一个更全面的认识。

image-20201231082557130


2. Builder 组件的价值何在

通过源码可以看出,Builder 是一个继承自 Stateless 的组件,需要实现 build 抽象方法,通过 BuildContext 对象,来构建 Widget 对象。而 Builder#build 只是使用了构造传入的 builder 函数,并将 当前的 BuildContext 作为回调传递出去。也就是将构建组件的逻辑交由外界处理,自身只是将 context 对象回调出去而已。

typedef WidgetBuilder = Widget Function(BuildContext context);
复制代码

所以 Builder 组件唯一的价值,就是回调当前的 context ,让用户根据这个 context 进行组件的构建。感觉上它完全是打酱油的,那这到底有什么用呢?这时所有的关注焦点应该被集中到了 BuildContext 上,且听我慢慢道来。


3. 认识 BuildContext 对象

为了更好地认识 BuildContext,我们来做个调试。断点如下,MyApp 是代码层的顶部组件,我们可以看一下在 build 方法中,回调过来的这个 context 对象到底是个什么东西。

image-20201231085648205

从下面调试结果可以看出:

【1】. 其类型为 StatelessElement ,也就是说此时它的本质是一个 Element 。
【2】. 其持有一个 Widget 对象 _widget,类型是 MyApp 即当前组件。
【3】. 其有一个父亲 _parent 属性,类型为 RenderOnjectToWidgetElement
【4】. 其有一个孩子 _child 属性,此时为 null。

image-20201231092013020

其实一直所说的 Element 树其实就在这里。根元素是 RenderOnjectToWidgetElement 类型对象,它的父节点为 null ,而这里的持有 MyAppStatelessElement 便是第二元素。

image-20201231092654996


BuildContext 是一个抽象类,也就是说它无法直接构造对象。 而在 Flutter 框架层,它有且仅有一个实现类 ---- Element ,所以两者之间的关系应该非常明确了。在 Flutter 使用中,你所见到的每个 BuildContext 对象,它的本质都是 Element 对象。 更形象点说,Element 就像是工程师眼中的电视机,BuildContext 就像一般用户眼中的电视机。工程师 需要将这个对象的内在功能、逻辑进行完整的实现;而对于用户来说,只需要按按开关,点点按键即可,不需要在意实现细节,只需要有对电视机的抽象认知即可。可以说用户认知中的电视机,并非是真实的电视机,只是电视机功能的抽象,我们只是知道它能干什么,所有的显示、按键都是内部提供的功能接口。 对于 Element 对象而言,BuildContext 就是暴露给用户 的功能接口。而用户,便是使用 Flutter 框架的我们。我们在使用时,不需要了解电视机( Element )内部做了什么,只需要知道如何使用(BuildContext )即可。这也是为什么抽象出来 BuildContext 而非直接使用 Element 的原因。

image-20201231093502380


4. BuildContext 的作用

BuildContext 对象有什么用呢,下面是 BuildContext 的所有信息,可见,除了四个成员变量之外,其他的都是抽象方法。值得注意的是 BuildContext 中并没有树状结构,也就是说它只是一种抽象,内部的结构、逻辑完全交于实现类来完成,抽象只是负责暴露给用户需要的接口功能。里面的方法很多,稍微瞄一眼,可以看到基本上都是在 找东西

image-20201231123155820

我们会经常使用 Navigator.of(context).push来用于路由的跳转。 其实 Navigator.of(context) 是一个静态方法,用于返回 NavigatorState,而路由的方法都是定义在 NavigatorState 中的。这里 BuildContext 的作用就是获取相关状态类 XXXState。核心方法是 findAncestorStateOfType,获取上层第一个某类型组件对应的 State 对象。

static NavigatorState of(
  BuildContext context, {
  bool rootNavigator = false,
}) {
  // Handles the case where the input context is a navigator element.
  NavigatorState? navigator;
  if (context is StatefulElement && context.state is NavigatorState) {
      navigator = context.state as NavigatorState;
  }
  if (rootNavigator) {
    navigator = context.findRootAncestorStateOfType<NavigatorState>() ?? navigator;
  } else {
    navigator = navigator ?? context.findAncestorStateOfType<NavigatorState>();
  }
  return navigator!;
}
复制代码

还有 MediaQuery 这种 InheritedWidget,通过 .of(context) 可以获取特定的数据。比如下面通过 MediaQuery.of(context) 可以获取 MediaQueryData 数据,从而拿到 屏幕尺寸、设备分辨率等数据信息,这里核心方法 BuildContext 的 dependOnInheritedWidgetOfExactType

static MediaQueryData of(BuildContext context) {
  assert(context != null);
  assert(debugCheckHasMediaQuery(context));
  return context.dependOnInheritedWidgetOfExactType<MediaQuery>()!.data;
}
复制代码

BuildContext 可以在所有父辈节点中获取特定的对象,在本文中讲到这就差不多了,后面会为 BuildContext 专门写一篇来详细讨论这些接口作用。


5.Builder 的作用

经过上面对 BuildContext 的认识后,再来看这里,当前的 context 之上只有根节点,所以用这个 context 开做 MediaQuery ,将会报错,因为MediaQuery 是在 MaterialApp 内部包含的,这时 context 中是找不到的,所以想要使用 MediaQuery,那就必需将 context 下移到 MaterialApp 构建之后。最简单的方案是抽离组件,来让 context 下移,如果不抽离组件,那么 Builder 组件就可以大显神威。

image-20201231131829018

需要注意的是,Builder 中回调的 上下文 并非是 context 的直接孩子,也就是说,并非仅是下移了一层。这要取决于组件的复杂程度。比如 MaterialApp 的内部实现比较复杂,可以看出,Builder 中回调的 ctx 的深度是 87,就说明在其之上还有这么多的父亲节点。

image-20201231132254856

所以,你认为的 Flutter 中的树,和真实的 Flutter 中的树是完全不同的。这时,也怕某些连门都没入,却别有用心的人开始扬言:“ 你们看,一个 MaterialApp 组件就这么复杂,性能担忧啊,Flutter 真是太嵌套地狱了,真可怕,早晚要凉”。我只想说,别拿你的脑子跟电脑比,就算是几千个元素节点,在树状结构下,形成树或找在树中出某个元素来也只是探囊取物。 现在相当于我打开电视机的外壳,让你瞟一眼内部是什么,千万不要用自己的无知来感叹这个世界的复杂

image-20201231133344783

一直往下翻,你会看到有一个持有 MediaQuery 的元素,这就说明当前的 ctx 可以上溯寻到该祖先节点。也就说明使用 Builder 回调的上下文,是可以使用 MediaQuery.of(ctx) 获取到媒体信息的。

image-20201231191832339

到这里,你应该对上下文的层级有了一定的认识。我们所使用的 XXX.of(context),都是在该上下文之上去寻找某些对象,Theme.ofScaffold.ofNavigator.ofProvider.ofBloc.of 都是这样的,如果你的上下文太靠前,是找不到的。所有 Builder 组件就是做这个事的,回调一个较下层的上下文以供使用。这一点本质上和提取出一个 Widget 没什么两样,如果很简单的东西,不想提出一个组件来处理,那 Builder 就是一个很好的帮手。当你通过 XXX.of(context) 没拿到想要的东西,现在你应该明白该怎么做了。

image-20201231193245999

比如下面的示例,ScaffoldBuilderDemo#build 下,这是想在 floatingActionButton 单击时弹出 SnackBar ,而 showSnackBar 是需要 ScaffoldState 对象触发的,当前的 context 上层还没有对应的 元素。这时有两种方案:1,抽离组件,在下层组件的上下文中触发。 2, 使用 Builder 回调下层的上下文。由于这里东西很少,没必要新建个组件,使用 Builder 就很轻轻方便。

import 'package:flutter/material.dart';
class BuilderDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      height: 200,
      child: Scaffold(
        appBar: AppBar(
          title: Text('Builder'),
        ),
        floatingActionButton: Builder(
          builder: (ctx) => FloatingActionButton(
            onPressed: () {
              Scaffold.of(ctx).showSnackBar(SnackBar(content: Text('hello builder')));
            },
            child: Icon(Icons.add),
          ),
        ),
      ),
    );
  }
}
复制代码

6.Builder 组件在源码中的应用

也许你并没有注意到,MaterialApp 本身有一个 builder 属性。MaterialApp 内部使用了 WidgetsApp

image-20201231194222536

_WidgetsAppState#build 中可以看到如果 builder 属性非空,会使用 Builder 组件。在使用 MaterialApp 组件时,可以通过 builder 属性,实现和 Builder 组件一样的效果,不过追其本质也还是 Builder 组件的功劳。

image-20201231194530212


IconTheme 中的 merge 方法里也使用了 Builder 组件,这是为了在没有上下文的时候拿到上下文,这样就不需要在 merge 方法中传入上下文了,这也是上下文无中生有的使用方式。同样的使用,也可以在源码的Text.mergeListTile.merge 中看到。

image-20201231195829723


在 Provider 相关的类中,你也可以看到一个 TransitionBuilder 类型的 builder 属性,其实它们的作用也是 Builder 赋予的,其作用也就不言而喻了,当你了解 Builder ,源码看到这里时,你就会很有亲切感。

image-20201231200535163

image-20201231201921349

Builder 组件本身难吗?10 行的源码组件肯定不难,难的是你对它存在价值的思考,以及去发现更深层东西的兴趣和能力。通过本文,你应该能对 Flutter 增加了一丢丢的新的认知,那么本文就到此结束,谢谢观看。


@张风捷特烈 2021.01.02 未允禁转
我的公众号:编程之王
联系我--邮箱:1981462002@qq.com -- 微信:zdl1994328
~ END ~