Writing custom Widgets in Flutter (Part 3.a) — SimpleOverlay (with helpers)

Intro

In previous articles we discussed how to create leaf Widget without children and Widget with a single child. Now it is time to create more complex Widget with multiple children.

Today I will describe how to create a SimpleOverlay Widget. The only thing it does is placing one Widget and overlaying it with a different Widget while keeping the size of the first Widget. It is such a simple concept but I’m using it quite often in my projects.

Hm… Can’t we use Stack for this? In the case when size of a child is known in advance — yes. But otherwise it doesn’t allow any dependencies between children.

Update: you actually can use for this with while also specifying to .

Code

First of all we need to create our own Widget:

class SimpleOverlay extends MultiChildRenderObjectWidget {
SimpleOverlay({
required Widget child,
required Widget overlay,
}) : super(children: [child, overlay]);

@override
RenderObject createRenderObject(BuildContext context) {
return RenderSimpleOverlay();
}
}

This time instead of SingleChildRenderObjectWidget we extend MultiChildRenderObjectWidget because we have multiple children. This base class requires us to pass all children via constructor as a list. Order here does matter — painting is done from the first child to the last child while hit tests use reversed order.

Same as with SingleChildRenderObjectWidget, we don’t need to implement our own Element, everything is handled for us.

When using MultiChildRenderObjectWidget we need to declare one additional class — ContainerBoxParentData. Parent data allows us to store arbitrary data inside of each child directly (something similar to LayoutParams in Android). MultiChildRenderObjectWidget uses ParentData to store references to previous and next child resulting in a doubly linked list data structure for children storage:

class _SimpleOverlayChild extends ContainerBoxParentData<RenderBox>
with ContainerParentDataMixin<RenderBox> {}

Thats all, pretty simple. You can find mentioned about linked list references inside ContainerParentDataMixin:

mixin ContainerParentDataMixin<ChildType extends RenderObject> on ParentData {
/// The previous sibling in the parent's child list.
ChildType? previousSibling;
/// The next sibling in the parent's child list.
ChildType? nextSibling;
}

Next thing is to create our RenderObject:

class RenderSimpleOverlay extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, _SimpleOverlayChild>,
RenderBoxContainerDefaultsMixin<RenderBox, _SimpleOverlayChild> {
// ...
}

We must mix ContainerRenderObjectMixin in order to use our RenderObject with MultiChildRenderObjectWidget. This mixin contains logic for adding, attaching, moving, removing, detaching and iterating over children RenderObjects (in other words tons of mandatory boilerplate code). RenderBoxContainerDefaultsMixin by default doesn’t do anything at all but rather contains a few helpers to use when painting or hit testing.

Remember ParentData? Well, now we need to actually set it for each child:

class RenderSimpleOverlay {
// ...

@override
void setupParentData(covariant RenderObject child) {
child.parentData = _SimpleOverlayChild();
}
}

After that goes our usual layout logic:

class RenderSimpleOverlay {
// ...

@override
void performLayout() {
var childConstraints = constraints;

final child = firstChild;
if (child != null) {
child.layout(constraints, parentUsesSize: true);
childConstraints = BoxConstraints.tight(child.size);
}

final overlay = (child == null) ? null : childAfter(child);
if (overlay != null) {
overlay.layout(childConstraints, parentUsesSize: true);
}

size = child?.size ?? overlay?.size ?? constraints.smallest;
}
}

and are provided by ContainerRenderObjectMixin. If you recall, first child is our main child and second child is our overlay. We use instead of because and will return the same object when only one child is present (and this will result in a bug).

The last things we need to implement is painting and hit testing. And here comes RenderBoxContainerDefaultsMixin to the rescue:

class RenderSimpleOverlay {
// ...

@override
void paint(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
}

@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
return defaultHitTestChildren(result, position: position);
}
}

And here is the result:

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

Hope you liked it!

In the next article I’ll show how to achieve the same result without helpers manually.

Other articles: