Flutter 必知必会系列 —— 三棵树最终章
往期精彩
前面的几篇文章基本上把 Flutter 的三棵树说明白了,从三棵树的含义到三棵树的关联,从 Element 的更新到 Render 的布局。有了这些铺垫,再看 Flutter 的其他机制就更加清晰。
有人可能会问,这三棵树除了 Widget 之外,其他的离我们开发好远,看这些对我们开发有什么帮助吗? 我从下面几点来给大家抛砖引玉。
可以更好的看 Flutter 源码
Flutter framework 的源码大概分为五层:
Widgets 这一层将所有的机制都封装进了我们熟悉的 Widget 中,比如 ImplicitlyAnimatedWidget
封装了动画,CustomPaint
封装了绘制,GestureDetector
封装了手势机制等等。
开发过程中,免不了要追到源码中对比实现效果。前面我们知道 Widget 基本分为下面的几类,每一类都有基本的代码追踪策略。
StatelessWidget 代码追踪
StatelessWidget
是组合型的无状态 Widget,在build 方法中构造出子树。其对应的 Element 是 StatelessElement
此类型的 Widget,在显示层我们只需要关注其 build 方法,看构造的子树是谁。
以万金油的 Container 组件为例。
@override
Widget build(BuildContext context) {
Widget? current = child;
//... 省略代码
if (color != null)
current = ColoredBox(color: color!, child: current);
//... 省略代码
if (constraints != null)
current = ConstrainedBox(constraints: constraints!, child: current);
if (margin != null)
current = Padding(padding: margin!, child: current);
//... 省略代码
return current!;
}
为啥说 Container 是万金油呢?就是因为在它的 build 方法中,它根据我们的构造方法构造了不同的组件。
如果我们构造方法设置了 color 属性,那么就构造一个专门设置颜色的 ColoredBox 组件
如果我们构造方法设置了 constraints 属性,那么就构造一个专门设置约束的 ConstrainedBox 组件
所以文档中才会有一句话:The eventual widget hierarchy may therefore be deeper than what the code represents
,树的真实深度要比代码多得多。
即使下面简单的代码:
Container(
color: Colors.blue,
child: Row(
children: [
Image.network('https://www.example.com/1.png'),
const Text('A'),
],
),
);
也会生成这么深的树
所以 StatelessWidget 类型的组件,在显示层,我们只需要关注 build 方法,看它构造了谁。
StatefulWidget 代码追踪
StatefulWidget
是组合型的有状态 Widget,核心在 State
对象中 。其对应的 Element 是 StatefulElement
此类型的代码追踪,在显示层,我们需要追踪到 State
中,在 initState
看初始化了什么,在 build
中看构造的子树是什么。
我们以异步模型 FutureBuilder
为例。FutureBuilder 对应的 State 是 _FutureBuilderState
对象。我们看其逻辑。
首先是,**初始化的逻辑 initState **。
@override
void initState() {
super.initState();
_snapshot = widget.initialData == null
? AsyncSnapshot<T>.nothing()
: AsyncSnapshot<T>.withData(ConnectionState.none, widget.initialData as T);
_subscribe();
}
初始化的时候将状态和 UI 建立了映射关系,比如异步任务完成的时候:
widget.future!.then<void>((T data) {
if (_activeCallbackIdentity == callbackIdentity) {
setState(() {
_snapshot = AsyncSnapshot<T>.withData(ConnectionState.done, data);
});
}
})
生成异步完成并携带数据的 AsyncSnapshot
,然后调用 setState 刷新 UI。
其次是,UI 显示的逻辑 build
@override
Widget build(BuildContext context) => widget.builder(context, _snapshot);
调用我们构造方法设置的异步 UI 构造器 AsyncWidgetBuilder
。
这就是为什么,我们使用 FutureBuilder 可以实现网络请求的 Loading、Error、显示UI 的逻辑。 在逻辑层,之前我们通过案例说明了,添加 Key 与否的区别。其实还有一种来实现效果。 StatefulElement 的更新逻辑在 update 中:
@override
void update(StatefulWidget newWidget) {
super.update(newWidget);
final StatefulWidget oldWidget = state._widget!;
_dirty = true;
state._widget = widget as StatefulWidget;
try {
final Object? debugCheckForReturnedFuture = state.didUpdateWidget(oldWidget) as dynamic;
} finally {
}
rebuild();
}
我们看 try 中先调用了 State 的 didUpdateWidget
方法,所以每次在 build 之前,就会先调用 didUpdateWidget 方法。\
我们就可以把更新颜色的逻辑放到 didUpdateWidget 中,实现不加 Key 也可以实现刷新的效果。
比如 在 FutureBuilder 的 didUpdateWidget 中,就添加了重新订阅的逻辑。
@override
void didUpdateWidget(FutureBuilder<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.future != widget.future) {
if (_activeCallbackIdentity != null) {
_unsubscribe();
_snapshot = _snapshot.inState(ConnectionState.none);
}
_subscribe();
}
}
所以,StatefulWidget 的代码追踪,需要关注 initState 和 build 方法。
RenderObjectWidget 代码追踪
RenderObjectWidget
是渲染型的 Widget,持有渲染对象。对应的 Element 是 RenderObjectElement
此类型的 Widget,在显示层我们只需要关注构造的渲染对象是谁,渲染对象布局绘制逻辑是什么,如果涉及到了手势,就看手势检测是什么。
以间距 Padding
组件为例。 createRenderObject
创建的渲染对象是 RenderPadding
@override
RenderPadding createRenderObject(BuildContext context) {
return RenderPadding(
padding: padding,
textDirection: Directionality.maybeOf(context),
);
}
布局逻辑集中在 performLayout 中。
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
_resolve();
/// 省略代码
final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding!);
child!.layout(innerConstraints, parentUsesSize: true);
final BoxParentData childParentData = child!.parentData! as BoxParentData;
childParentData.offset = Offset(_resolvedPadding!.left, _resolvedPadding!.top);
size = constraints.constrain(Size(
_resolvedPadding!.left + child!.size.width + _resolvedPadding!.right,
_resolvedPadding!.top + child!.size.height + _resolvedPadding!.bottom,
));
}
Padding 子节点的约束就是其本身的约束 deflate
开发者设置的间距。
比如 Padding 本身的约束是 100 * 100,开发者设置了 25 的间距,那么子节点的约束就是 50 * 50
InheritedWidget 代码追踪
InheritedWidget
是共享数据的 Widget,对应的 Element 是 InheritedElement。
这种类型的 Widget ,我们只需要看 of 方法提供的是就可以。
比如 DefaultTextStyle 向下提供了文本的样式,其 of 方法如下:
static DefaultTextStyle of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<DefaultTextStyle>() ?? const DefaultTextStyle.fallback();
}
而最根的 DefaultTextStyle
是在 WidgetsApp
中构造的,所以我们在开发的时候,即使只写个 Text,不写TextStyle,也可以有默认的文本效果。
Text("data")
所以,InheritedWidget 一般只需要看 of 方法。并且 of 方法中也会区分是否通知子节点刷新。
小结
不同类型的 Widget 有不同的侧重点,在追踪代码的时候,具体问题具体分析就可以了。😄
可以随心所欲的自定义组件
Flutter 中实现自定义的效果有三种方式:组合既有组件、控制渲染对象、完全自定义绘制。自定义的程度依次从低到高。
学完三棵树之后,我们可以更加随心所欲的实现我们想要的功能和效果。我们以获取子节点尺寸为例,看看怎么改造渲染对象。
前面的文章,渲染类型的 RenderObjectWidget 会参与布局过程,并且 RenderObjectWidget 的若干子类为我们已经实现好了一些规则。RenderObjectWidget 的继承结构如下:
我们只需要继承自 SingleChildRenderObjectWidget 实现指定的抽象方法,即构造出单一子节点的 Widget,供使用者使用。
如下:
class SizedChildWidget extends SingleChildRenderObjectWidget {
final Widget childWidget;
final Function(Size size) childSized;
const SizedChildWidget(
{Key? key, required this.childWidget, required this.childSized})
: super(key: key, child: childWidget);
@override
RenderObject createRenderObject(BuildContext context) {
return SizedChildRender(childSized);
}
}
我们可以将获得的尺寸信息通过回调或者通知的方式发送出去,这里我们通过回调的方式 childSized。
SingleChildRenderObjectWidget
的实现类需要指定 RenderObject。
我们的功能只是参与布局过程即可,所以可以直接继承自 RenderProxyBox
。RenderProxyBox
是盒子模型渲染的基类,它的协议是专门针对具有子节点的情况。比如,它的布局协议里面,会根据约束让子节点去布局,然后根据子节点返回的大小来确定自己的大小。
class SizedChildRender extends RenderProxyBox {
final Function(Size size) childSized;
Size? oldSize;
SizedChildRender(this.childSized, {RenderBox? child}) : super(child);
@override
void performLayout() {
super.performLayout();
if(oldSize!= child!.size){
childSized(child!.size);
oldSize = child!.size;
}
}
}
如上,我们就可以在其布局完成的时候,将子节点的尺寸回调出去。
我们在使用的时候就可以像使用系统组件一样使用了。比如:
Scaffold(
body: SizedChildWidget(
childWidget: Container(
height: 100,
width: 100,
color: Colors.red,
),
childSized: (Size size) {
if (kDebugMode) {
print(size);
}
},
),
)
渲染的结果就是,会将尺寸打印出来。
小结
改造就是跟踪到指定的代码逻辑,要么改造负责布局的 performLayout
,要么改造负责渲染的 paint
,我们根据自己的实际场景灵活重写就可以了。比如还可以直接重写 Element 更新过程,Provider 依赖的 Hook 库就是这么干的。
可以开发更加高级的组件
三棵树是 Flutter 的核心,了解这个核心就可以为所欲为了。比如字节、滴滴和我自己开发的 UI 调试工具,都是在三棵树的基础上搞得。
这个工具要解决三棵问题:
坐标点的获取
坐标点组件的获取
组件信息的获取及操作
坐标点的获取
我们可以通过 GestureDetector
组件来获取手势的操作。
double _top = 0.0; //距顶部的偏移
double _left = 0.0;//距左边的偏移
GestureDetector(
onPanDown: (DragDownDetails e) {
//打印手指按下的位置(相对于屏幕)
print("用户手指按下:${e.globalPosition}");
},
//手指滑动时会触发此回调
onPanUpdate: (DragUpdateDetails e) {
//用户手指滑动时,更新偏移,重新构建
setState(() {
_left += e.delta.dx;
_top += e.delta.dy;
});
},
onPanEnd: (DragEndDetails e) {
//打印滑动结束时在x、y轴上的速度
},
)
这样就拿到了坐标点的位置,GestureDetector 还支持点击、双击、长按 等操作,这里是拖动的时候获取坐标点。
坐标点组件的获取
三棵树中 Element 是桥梁,持有 Widget 和 RenderObject 的引用,所以只要我们找到坐标点的 Element ,就可以拿到所有的信息。
做法是:层序遍历 Element 树,比对拾取的坐标和 Element 持有的 Render 的范围,最深层级的元素就是选中元素。查找流程如下:
经过上图的四步之后,edgeHits 数组的第一个元素(最深层级)就是目标 Element。
坐标点信息的获取
WidgetInspectorService 的 getSelectedSummaryWidget 可以通过 told 方法返回的 id 获取对应的 Widget 信息(如下图所示),包括:组件类型、代码路径和行数等等。这两个方法配合使用就可以拿到所需信息。
上面的 json 结构中,description 字段是 Widget 类型,creationLocation 字段是创建 Widget 的代码位置。有了具体的代码行数,即使不是自己的代码,也可以快速定位,快速进入开发。
总结
转载自:https://juejin.cn/post/7061047450768769038