Writing custom Widgets in Flutter (Part 3.b) — SimpleOverlay (no helpers)

Rostyslav Lesovyi
Geek Culture
Published in
4 min readJun 14, 2021

--

Intro

As promised now I’ll show how to write SimpleOverlay Widget without any helpers. I’ll omit some boilerplate code which was already describer in previous articles.

Quick Theory

There are few more things we need to know when implementing Widgets that might contain multiple children.

The first thing we need to understand is the concept of the Slots. Slot is basically just a position of the child Widget inside our Widget. It has nothing to do with the x and y pixel coordinates, but rather logical position. For example in the case of a Toolbar it can represent navigation icon, logo, title or action (named Slots). But for Column it will be an index of each child.

Children Elements can also move from one Slot to a different one without recreation. For example when we specify GlobalKey for our child and then change its Slot during build method. Or when we call updateChid in our element and pass different Slot for the same Element.

One more thing is the order of calling children methods:

  • Layout — order doesn’t matter unless one child size depends on the size of a different child
  • Paint — back-to-front order when children overlap each other, otherwise order doesn’t really matter
  • HitTest — front-to-back order when children overlap each other, otherwise order doesn’t really matter

Code

The first difference is that our Widget will not extend MultiChildRenderObjectWidget anymore and instead will switch to the much simpler RenderObjectWidget:

class SimpleOverlay extends RenderObjectWidget {
final Widget child;
final Widget overlay;

const SimpleOverlay({
required this.child,
required this.overlay,
});

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

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

Now we need to actually add children management logic to our Element:

class SimpleOverlayElement extends RenderObjectElement {
Element? _child;
Element? _overlay;

// ...

@override
void mount(Element? parent, newSlot) {
super.mount(parent, newSlot);

_child = inflateWidget(widget.child, true);
_overlay = inflateWidget(widget.overlay, false);
}

@override
void update(SimpleOverlay newWidget) {
super.update(newWidget);

_child = updateChild(_child, newWidget.child, true);
_overlay = updateChild(_overlay, newWidget.overlay, false);
}

@override
void unmount() {
super.unmount();

_child = null;
_overlay = null;
}
}

What is actually different here from the previous “no helper” example is the usage of the Slots. In case of the SimpleOverlay we have only two Slots — child and overlay. I chose to use Boolean as a Slot because we have only two possible values — true for the child and false for the overlay.

Again, just to be clear, Slots can be anything you want, but they should be consistent.

One more thing to add — is passing children RenderObjects lifecycle methods to our own:

class SimpleOverlayElement extends RenderObjectElement {
// ...

@override
void insertRenderObjectChild(RenderBox child, bool slot) {
renderObject.insertRenderObjectChild(child, slot);
}

@override
void moveRenderObjectChild(RenderBox child, bool oldSlot, bool newSlot) {
renderObject.moveRenderObjectChild(child, oldSlot, newSlot);
}

@override
void removeRenderObjectChild(RenderBox child, bool slot) {
renderObject.removeRenderObjectChild(child, slot);
}

The only new thing here is the presence of the moveRenderObjectChild method. As I mentioned before, this method will be called when a child moves from the one Slot (oldSlot) to a new one (newSlot).

Thats all for our Element. Nothing really major, most of the code is the same. Now lets implement our RenderObject. First of all we need to handle insert, move and remove calls from our Element:

class SimpleOverlayRenderObject extends RenderBox {
//...
RenderBox? _child;
RenderBox? _overlay;

void insertRenderObjectChild(RenderBox child, bool slot) {
if (slot) {
_child = child;
} else {
_overlay = child;
}
adoptChild(child);
}

void moveRenderObjectChild(RenderBox child, bool oldSlot, bool newSlot) {
if (oldSlot) {
_child = null;
} else {
_overlay = null;
}
if (newSlot) {
_child = child;
} else {
_overlay = child;
}
}

void removeRenderObjectChild(RenderBox child, bool slot) {
if (slot) {
_child = null;
} else {
_overlay = null;
}
dropChild(child);
}
}

Slot, that we receive here, has the same value that we’ve passed to inflateWidget and updateChild in out Element before. Thanks to this Slot we know which RenderBox should go where. Also pay attention to the moveRenderObjectChild method — we don’t need to call adoptChild nor dropChild from it because child is actually reused and is already attached.

The very last thing that is new — is painting and hit testing:

class SimpleOverlayRenderObject extends RenderBox {
//...

@override
void paint(PaintingContext context, Offset offset) {
if (_child != null) {
context.paintChild(_child!, offset);
}
if (_overlay != null) {
context.paintChild(_overlay!, offset);
}
}

@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
if (_overlay?.hitTest(result, position: position) == true) {
return true;
}
if (_child?.hitTest(result, position: position) == true) {
return true;
}
return false;
}
}

Please note the order in which we’re calling out children. We paint overlay last because we want it to be visible above our child but hit tests are using inverse order — overlay should take priority.

And here is the result:

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

Hope you liked it!

Other articles:

--

--