本文由 简悦SimpRead 转码,原文地址 rlesovyi.medium.com
了解如何管理子元素/RenderObjects以及如何在子Widget变化时接收回调......
在之前的文章中,我们已经创建了一个自定义的Widget,当它的子尺寸发生变化时通知我们。为了完成这个任务,我们使用了SingleChildRenderObjectWidget帮助器。它简化了代码的某些部分,因为我们不需要编写我们自己的Element。
这一次我们将从头开始写一切。编写元素并不难,但也不是没有一些棘手的地方。主要的有点难理解的地方是Element和RenderObject是如何相互作用的。
为了避免重复(并保持本文的合理规模),我将只描述前一部分的变化。
和以前一样,我们需要做的第一件事是--创建我们的小部件。以下是变化的内容。
class ChildSize extends RenderObjectWidget {
// ...
final Widget? child;
const ChildSize({
Key? key,
this.child,
this.onChildSizeChanged,
}) : super(key: key);
@override
RenderObjectElement createElement() {
return ChildSizeElement(this);
}
// ...
}
我们的Widget现在扩展了RenderObjectWidget而不是SingleChildRenderObjectWidget。这个基类在它的构造函数中不接受子Widget,所以我们需要自己来存储它。
此外,现在我们有额外的方法要实现--createElement。这里我们需要返回,你猜对了,就是我们的自定义元素。这个方法将只在Widget第一次膨胀时被调用一次。此后,Element将被重复使用,直到Widget的类型发生变化或者我们提供一个不同的Key。
现在让我们来看看我们的元素。
class ChildSizeElement extends RenderObjectElement {
ChildSizeElement(ChildSize widget) : super(widget);
@override
ChildSize get widget {
return super.widget as ChildSize;
}
@override
RenderChildSize get renderObject {
return super.renderObject as RenderChildSize;
}
}
到目前为止,没有什么特别之处--我们扩展了RenderObjectElement并覆盖了两个getters。后者只是为了简化代码的其余部分(避免在每次调用时铸造widget和renderObject)。
接下来我们需要覆盖三个生命周期的回调。
class ChildSizeElement extends RenderObjectElement {
// ...
Element? _child;
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
_child = updateChild(_child, widget.child, null);
}
@override
void update(ChildSize newWidget) {
super.update(newWidget);
_child = updateChild(_child, newWidget.child, null);
}
@override
void unmount() {
super.unmount();
_child = null;
}
}
mount- 当我们的元素被创建并连接到它的父元素时被调用。这里我们需要给所有的子元素充气。在我们的例子中,我们只有一个子元素。update- 当我们的Widget发生变化时被调用。这可能也会改变我们的子元素,所以我们也需要更新它。unmount- 这是最后的回调,在这一点上Element被销毁。现在我们可以释放我们的子元素的引用。
你可能会问:"我们调用的 updateChild 方法是什么?"。这是一个万能的方法。它可以做所有的事情--创建、更新和删除元素,都在一次调用中完成。
- 第一个参数是我们的子元素的前一个版本。如果没有可用的元素,可以传递null。
- 第二个参数 - 子元素的新Widget。基于这个Widget,它将决定是否重新使用以前的元素,或者删除它并创建一个新的。
- 第三个参数是一个槽。槽是一种有趣的东西,可以是任何东西--ID、索引、一些其他特征。槽的主要作用是确定子元素的位置。因为我们只有一个子元素--null可以被传递。
调用这个方法的结果是,我们将收到一个新的、以前的(如果重复使用)或空的(如果删除)元素。
好了,到目前为止还不错。但是还有两个方法必须要实现。
class ChildSizeElement extends RenderObjectElement {
// ...
@override
void visitChildren(ElementVisitor visitor) {
final child = _child;
if (child != null) {
visitor(child);
}
super.visitChildren(visitor);
}
@override
void forgetChild(Element child) {
assert(child == _child);
_child = null;
super.forgetChild(child);
}
}
第一个(visitChildren)只是被Flutter用来遍历子元素。这是我们的责任,因为RenderObjectElement本身并不存储任何对其子元素的引用。为什么呢?很简单,优化--我们更知道哪种数据结构最适合存储子元素,而且Flutter不会像其他框架那样用索引来限制我们。
forgetChild,你可能已经猜到了,当Element被移除时,我们也需要移除对上述Element的任何引用。通常这个方法是在执行 updateChild 时调用的。
我们终于完成了对子元素的管理。但是...这还是不够的。元素还会收到一些回调。
class ChildSizeElement extends RenderObjectElement {
// ...
@override
void insertRenderObjectChild(RenderBox child, covariant Object? slot) {
renderObject.insertRenderObjectChild(child, slot);
}
@override
void removeRenderObjectChild(RenderBox child, covariant Object? slot) {
renderObject.removeRenderObjectChild(child, slot);
}
}
这里我们需要在新的子RenderObjects被添加或移除时通知我们的RenderObject。实际上,还有一个回调,当一个子RenderObject从旧槽移到新槽时,它被调用。但这并不影响我们,因为我们只有一个槽。
PS:坦率地说,我不明白为什么Flutter团队把这些回调加到了Element而不是RenderObject上,但可能有一些隐藏的原因。
好了。现在我们的元素终于完成了。让我们转到RenderObject。值得庆幸的是,它需要的改动较少。
class RenderChildSize extends RenderBox {
RenderBox? _child;
@override
void attach(covariant PipelineOwner owner) {
super.attach(owner);
_child?.attach(owner);
}
@override
void detach() {
super.detach();
_child?.detach();
}
@override
void visitChildren(RenderObjectVisitor visitor) {
final child = _child;
if (child != null) {
visitor(child);
}
super.visitChildren(visitor);
}
@override
void redepthChildren() {
final child = _child;
if (child != null) {
redepthChild(child);
}
super.redepthChildren();
}
}
这些都是不言自明的。attach/detach 是在RenderObject与父级连接/分离时被调用的。visitChildren 与元素中的相同,但针对子RenderObjects。
唯一的新东西是 redepthChildren 。这更像是一个调试的助手,可以知道RenderObjects树有多深。
最后的两个方法将实际添加/删除我们的孩子。
class RenderChildSize extends RenderBox {
// ...
void insertRenderObjectChild(RenderBox child, covariant Object? slot) {
assert(_child == null);
_child = child;
adoptChild(child);
}
void removeRenderObjectChild(RenderBox child, covariant Object? slot) {
assert(_child == child);
_child = null;
dropChild(child);
}
}
除了 adoptChild 和 removeChild,这里没有什么值得注意的。它们主要是设置父级ParentData和设置RenderObjects之间的父-子关系。
这就是全部,没有更多的变化。正如你所看到的,SingleChildRenderObjectWidget帮了我们很大的忙,使我们不用写近100行的模板代码。我个人认为,每个人都需要知道事情是如何从内部运作的,因为有时候帮助者是不够的。
下面是结果(和以前一样,但现在没有使用帮助器)。
你可以在我的GitHub上找到实现。 github.com/MatrixDev/F…
希望你喜欢它!
- Part 1 - EllipsizedText
- Part 2.a - ChildSize (with helpers)
- Part 3.a - SimpleOverlay (with helpers)
- Part 3.b - SimpleOverlay (no helpers)