赞
踩
theme: v-green
这篇文章来讲一个只有 10 行代码
的组件: Builder
,下面是它的全部代码。虽然非常简单,但通过它,仍然能让你了解很多知识。
```dart 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 有一个更全面的认识。
通过源码可以看出,Builder 是一个继承自 Stateless
的组件,需要实现 build 抽象方法
,通过 BuildContext
对象,来构建 Widget
对象。而 Builder#build
只是使用了构造传入的 builder
函数,并将 当前的
BuildContext 作为回调传递出去。也就是将构建组件的逻辑交由外界处理,自身只是将 context
对象回调出去而已。
dart typedef WidgetBuilder = Widget Function(BuildContext context);
所以 Builder
组件唯一的价值,就是回调当前的 context
,让用户根据这个 context
进行组件的构建。感觉上它完全是打酱油的,那这到底有什么用呢?这时所有的关注焦点应该被集中到了 BuildContext
上,且听我慢慢道来。
为了更好地认识 BuildContext
,我们来做个调试。断点如下,MyApp
是代码层的顶部组件,我们可以看一下在 build
方法中,回调过来的这个 context
对象到底是个什么东西。
从下面调试结果可以看出:
【1】. 其类型为
StatelessElement
,也就是说此时它的本质是一个 Element 。
【2】. 其持有一个 Widget 对象_widget
,类型是MyApp
即当前组件。
【3】. 其有一个父亲_parent
属性,类型为RenderOnjectToWidgetElement
。
【4】. 其有一个孩子_child
属性,此时为 null。
其实一直所说的 Element
树其实就在这里。根元素是 RenderOnjectToWidgetElement
类型对象,它的父节点为 null ,而这里的持有 MyApp
的 StatelessElement
便是第二元素。
BuildContext
是一个抽象类,也就是说它无法直接构造对象。 而在 Flutter 框架层
,它有且仅有一个实现类 ---- Element
,所以两者之间的关系应该非常明确了。在 Flutter 使用中,你所见到的每个 BuildContext
对象,它的本质都是 Element
对象。 更形象点说,Element 就像是工程师
眼中的电视机,BuildContext
就像一般用户
眼中的电视机。工程师
需要将这个对象的内在功能、逻辑进行完整的实现;而对于用户来说,只需要按按开关,点点按键即可,不需要在意实现细节
,只需要有对电视机的抽象认知
即可。可以说用户
认知中的电视机,并非是真实的电视机,只是电视机功能的抽象
,我们只是知道它能干什么,所有的显示、按键都是内部提供的功能接口
。 对于 Element
对象而言,BuildContext
就是暴露给用户
的功能接口。而用户,便是使用 Flutter 框架的我们。我们在使用时,不需要了解电视机( Element
)内部做了什么,只需要知道如何使用(BuildContext
)即可。这也是为什么抽象出来 BuildContext
而非直接使用 Element
的原因。
那 BuildContext
对象有什么用呢,下面是 BuildContext
的所有信息,可见,除了四个成员变量之外,其他的都是抽象方法。值得注意的是 BuildContext 中并没有树状结构
,也就是说它只是一种抽象
,内部的结构、逻辑完全交于实现类
来完成,抽象只是负责暴露给用户需要的接口功能。里面的方法很多,稍微瞄一眼,可以看到基本上都是在 找东西
。
我们会经常使用 Navigator.of(context).push
来用于路由的跳转。 其实 Navigator.of(context)
是一个静态方法,用于返回 NavigatorState
,而路由的方法都是定义在 NavigatorState
中的。这里 BuildContext
的作用就是获取相关状态类 XXXState
。核心方法是 findAncestorStateOfType
,获取上层第一个某类型组件对应的 State 对象。
dart 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
。
dart static MediaQueryData of(BuildContext context) { assert(context != null); assert(debugCheckHasMediaQuery(context)); return context.dependOnInheritedWidgetOfExactType<MediaQuery>()!.data; }
BuildContext
可以在所有父辈节点中获取特定的对象,在本文中讲到这就差不多了,后面会为 BuildContext
专门写一篇来详细讨论这些接口作用。
经过上面对 BuildContext
的认识后,再来看这里,当前的 context
之上只有根节点,所以用这个 context
开做 MediaQuery
,将会报错,因为MediaQuery
是在 MaterialApp
内部包含的,这时 context 中是找不到的,所以想要使用 MediaQuery
,那就必需将 context 下移到 MaterialApp
构建之后。最简单的方案是抽离组件,来让 context 下移,如果不抽离组件,那么 Builder
组件就可以大显神威。
需要注意的是,Builder 中回调的 上下文
并非是 context
的直接孩子,也就是说,并非仅是下移了一层。这要取决于组件的复杂程度。比如 MaterialApp
的内部实现比较复杂,可以看出,Builder 中回调的 ctx
的深度是 87,就说明在其之上还有这么多的父亲节点。
所以,你认为的 Flutter
中的树,和真实的 Flutter
中的树是完全不同的。这时,也怕
某些连门都没入,却别有用心的人开始扬言:“ 你们看,一个 MaterialApp 组件就这么复杂,性能担忧啊,Flutter 真是太嵌套地狱了,真可怕,早晚要凉”。我只想说,别拿你的脑子跟电脑比,就算是几千个元素节点,在树状结构下,形成树或找在树中出某个元素来也只是探囊取物。 现在相当于我打开电视机的外壳,让你瞟一眼内部是什么,千万不要用自己的无知来感叹这个世界的复杂
。
一直往下翻,你会看到有一个持有 MediaQuery
的元素,这就说明当前的 ctx
可以上溯寻到该祖先节点。也就说明使用 Builder 回调的上下文,是可以使用 MediaQuery.of(ctx)
获取到媒体信息的。
到这里,你应该对上下文的层级有了一定的认识。我们所使用的 XXX.of(context)
,都是在该上下文之上去寻找某些对象,Theme.of
、Scaffold.of
、Navigator.of
、Provider.of
、Bloc.of
都是这样的,如果你的上下文太靠前,是找不到的。所有 Builder
组件就是做这个事的,回调一个较下层的上下文以供使用
。这一点本质上和提取出一个 Widget
没什么两样,如果很简单的东西,不想提出一个组件来处理,那 Builder
就是一个很好的帮手。当你通过 XXX.of(context)
没拿到想要的东西,现在你应该明白该怎么做了。
比如下面的示例,Scaffold
在 BuilderDemo#build
下,这是想在 floatingActionButton
单击时弹出 SnackBar
,而 showSnackBar
是需要 ScaffoldState
对象触发的,当前的 context
上层还没有对应的 元素。这时有两种方案:1,抽离组件,在下层组件的上下文中触发。 2, 使用 Builder
回调下层的上下文。由于这里东西很少,没必要新建个组件,使用 Builder
就很轻轻方便。
dart 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), ), ), ), ); } }
也许你并没有注意到,MaterialApp
本身有一个 builder
属性。MaterialApp
内部使用了 WidgetsApp
在 _WidgetsAppState#build
中可以看到如果 builder 属性非空,会使用 Builder 组件。在使用 MaterialApp
组件时,可以通过 builder
属性,实现和 Builder
组件一样的效果,不过追其本质也还是 Builder
组件的功劳。
在 IconTheme
中的 merge 方法里也使用了 Builder 组件,这是为了在没有上下文的时候拿到上下文
,这样就不需要在 merge 方法中传入上下文了,这也是上下文无中生有的使用方式。同样的使用,也可以在源码的Text.merge
、ListTile.merge
中看到。
在 Provider 相关的类中,你也可以看到一个 TransitionBuilder
类型的 builder
属性,其实它们的作用也是 Builder
赋予的,其作用也就不言而喻了,当你了解 Builder
,源码看到这里时,你就会很有亲切感。
Builder
组件本身难吗?10
行的源码组件肯定不难,难的是你对它存在价值的思考,以及去发现更深层东西的兴趣和能力。通过本文,你应该能对 Flutter 增加了一丢丢的新的认知,那么本文就到此结束,谢谢观看。
@张风捷特烈 2021.01.02 未允禁转
我的公众号:编程之王
联系我--邮箱:1981462002@qq.com -- 微信:zdl1994328
~ END ~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。