likes
comments
collection
share

Flutter 必知必会系列 —— 三棵树最终章

作者站长头像
站长
· 阅读数 47

往期精彩

前面的几篇文章基本上把 Flutter 的三棵树说明白了,从三棵树的含义到三棵树的关联,从 Element 的更新到 Render 的布局。有了这些铺垫,再看 Flutter 的其他机制就更加清晰。

有人可能会问,这三棵树除了 Widget 之外,其他的离我们开发好远,看这些对我们开发有什么帮助吗? 我从下面几点来给大家抛砖引玉。

可以更好的看 Flutter 源码

Flutter framework 的源码大概分为五层:

Flutter 必知必会系列 —— 三棵树最终章

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'),
    ],
  ),
);

也会生成这么深的树

Flutter 必知必会系列 —— 三棵树最终章

所以 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

Flutter 必知必会系列 —— 三棵树最终章

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 的继承结构如下:

Flutter 必知必会系列 —— 三棵树最终章

我们只需要继承自 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。

我们的功能只是参与布局过程即可,所以可以直接继承自 RenderProxyBoxRenderProxyBox 是盒子模型渲染的基类,它的协议是专门针对具有子节点的情况。比如,它的布局协议里面,会根据约束让子节点去布局,然后根据子节点返回的大小来确定自己的大小。

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);
      }
    },
  ),
)

渲染的结果就是,会将尺寸打印出来。

Flutter 必知必会系列 —— 三棵树最终章

小结

改造就是跟踪到指定的代码逻辑,要么改造负责布局的 performLayout,要么改造负责渲染的 paint ,我们根据自己的实际场景灵活重写就可以了。比如还可以直接重写 Element 更新过程,Provider 依赖的 Hook 库就是这么干的。

可以开发更加高级的组件

三棵树是 Flutter 的核心,了解这个核心就可以为所欲为了。比如字节、滴滴和我自己开发的 UI 调试工具,都是在三棵树的基础上搞得。

Flutter 必知必会系列 —— 三棵树最终章

这个工具要解决三棵问题:

坐标点的获取

坐标点组件的获取

组件信息的获取及操作

坐标点的获取

我们可以通过 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 的范围,最深层级的元素就是选中元素。查找流程如下:

Flutter 必知必会系列 —— 三棵树最终章

经过上图的四步之后,edgeHits 数组的第一个元素(最深层级)就是目标 Element。

坐标点信息的获取

WidgetInspectorService 的 getSelectedSummaryWidget 可以通过 told 方法返回的 id 获取对应的 Widget 信息(如下图所示),包括:组件类型、代码路径和行数等等。这两个方法配合使用就可以拿到所需信息。

Flutter 必知必会系列 —— 三棵树最终章

上面的 json 结构中,description 字段是 Widget 类型,creationLocation 字段是创建 Widget 的代码位置。有了具体的代码行数,即使不是自己的代码,也可以快速定位,快速进入开发

总结

转载自:https://juejin.cn/post/7061047450768769038
评论
请登录