Writing custom Widgets in Flutter (Part 2.b) — ChildSize (no helpers)

Rostyslav Lesovyi
5 min readMay 30, 2021

Into

In previous article we have created a custom Widget to notify us when its child size changes. To accomplish this task SingleChildRenderObjectWidget helper was used. It simplifies some parts of the code because we don’t need to write our own Element.

This time we will write everything from scratch. Writing Elements is not that hard but not without some tricky points. The main somewhat difficult thing to understand is how Element and RenderObject interacts with each other.

Code

To avoid repeating myself (and to keep this article reasonable size) I will describe only changes from the previous part.

As before, first thing we need to do — create our widget. Here is what has changed:

class ChildSize extends RenderObjectWidget {
// ...
final Widget? child;
const ChildSize({
Key? key,
this.child,
this.onChildSizeChanged,
}) : super(key: key);

@override
RenderObjectElement createElement() {
return ChildSizeElement(this);
}

// ...
}

Our Widget now extends RenderObjectWidget instead of the SingleChildRenderObjectWidget. This base class doesn’t accept child Widget in its constructor so we need to store it by ourself.

Moreover, now we have additional method to implement — createElement. Here we need to return, you guessed it, our custom Element. This method will be called only once when the Widget is first inflated. After that Element will be reused until type of the Widget changes or we provide a different Key.

Now lets move to our Element:

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

So far nothing special — we extended RenderObjectElement and override two getters. The latter ones are just to simplify the rest of the code (avoid casting widget and renderObject on each call).

Next we need to override three lifecycle callbacks:

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 — called when our Element is created and attached to its parent. Here we need to inflate all our children elements. In our case we have only one child.
  • update — called when our Widget has changed. This might also change our child, so we need to update it as well.
  • unmount — this is the final callback, at this point Element is destroyed. Now we can release the reference to our child.

You might ask: “what is this updateChild method that we’re calling?”. This is the jack-of-all-trades method. It can do everything — create, update and delete Element, all in one call:

  • First argument is a previous version of our child Element. If there is no Element available — null can be passed.
  • Second argument — new Widget for the child Element. Based on this Widget it will decide whether to reuse previous Element, or to delete it and create a new one.
  • Third argument is a slot. Slots are kind of interesting and can be anything — id, index, some other characteristic. The main point of slots is to identify child Element’s position. Because we have only one child — null can be passed.

As a result of calling this method we will receive a new, previous (if reused) or null (if deleted) Element.

Okay, so far so good. But there are two more methods that must be implemented:

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

First one (visitChildren) is simply used by the Flutter to iterate over the children Elements. This is our responsibility because RenderObjectElement by itself doesn’t store any references to its children. Why? Simple, optimisations — we know better which data structure is best suited to store children and Flutter doesn’t limit us with indexes, unlike other frameworks.

forgetChild as you might have guessed, is called when Element is removed and we also need to remove any references to the above Element. Usually this method is called during updateChild execution.

We are finally finished with children Elements management. But… it’s still not enough. Element will receive few more callbacks:

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

Here we need to inform our RenderObject when new children RenderObjects are added or removed. In reality there is one more callback which is called when a child RenderObject is moved from the old slot to the new one. But it doesn’t affect us because we have only a single slot.

PS: frankly, I don’t understand why Flutter team has added these callbacks to the Element instead of the RenderObject, but there might be some hidden reasons.

YEY. Now our Element is finally complete. Lets move to the RenderObject. Thankfully it needs less changes.

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

Those are self-explanatory. attach/detach are called when RenderObject is attached/detached from the parent. visitChildren is the same as in the Element but for the children RenderObjects.

The only new thing is redepthChildren. This is more of a helper for the debugging to know how deep RenderObjects tree goes.

And finally the last two methods which will actually add/remove our child:

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

Nothing worth noting here except maybe adoptChild and removeChild. They mostly just set parent ParentData and set parent-child relations between RenderObjects.

Thats all, no more changes. As you can see SingleChildRenderObjectWidget helped us quite a lot and saved us from writing nearly 100 lines of boiler-plate code. I personally think that everyone needs to know how things work from the inside because sometimes helpers are just not enough.

And here is the result (the same as before, but now without the usage of the helpers):

You can find implementation on my GitHub:
https://github.com/MatrixDev/Flutter-CustomWidgets

Hope you liked it!

Other articles:

--

--