[Flutter翻译]在Flutter中编写自定义Widget(第2.b部分)--ChildSize(无辅助工具)

176 阅读5分钟

本文由 简悦SimpRead 转码,原文地址 rlesovyi.medium.com

了解如何管理子元素/RenderObjects以及如何在子Widget变化时接收回调......

image.png

在之前的文章中,我们已经创建了一个自定义的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);
  }
}

除了 adoptChildremoveChild,这里没有什么值得注意的。它们主要是设置父级ParentData和设置RenderObjects之间的父-子关系。

这就是全部,没有更多的变化。正如你所看到的,SingleChildRenderObjectWidget帮了我们很大的忙,使我们不用写近100行的模板代码。我个人认为,每个人都需要知道事情是如何从内部运作的,因为有时候帮助者是不够的。

下面是结果(和以前一样,但现在没有使用帮助器)。

1.gif

你可以在我的GitHub上找到实现。 github.com/MatrixDev/F…

希望你喜欢它!


www.deepl.com 翻译