Writing custom Widgets in Flutter (Part 2.a) — ChildSize (with helpers)
Intro
The time has come for my second article. This time it will be a pretty simple Widget that notifies us when its child size changes. The task is pretty simple but the main point of it is to show you how to manage children in RenderObject.
When I just started with the Flutter it was one of the questions I had and even completing course on Udemy, sadly, I didn’t receive my answer. On the StackOverflow people advise to use GlobalKey on the Widget, finding its RenderObject and fetching size for it.
While there is nothing wrong with the above solution I still don’t like some things about it:
- it changes how Widget’s Element is destroyed
- you need to add imperative code into declarative Widget structure
- there is no ability to actually track size of the Widget and it needs to be pulled on demand
In general what I want to achieve is following:
return ChildSize(
child: buildChildWidget(),
onChildSizeChanged: (size) => handleNewChildSize(size),
);
Brief theory
When writing a custom Widget that contains children there are few important things we need to know:
- For each custom Widget we need to write its Element and (sometimes) RenderObject implementations
- Element will inflate its children Widgets into a separate Elements and update/recreate them when needed
- RenderObject has few important roles — children management, layout, paint and hit test (mouse pointers, touch events, etc.)
Code
Lets start. First of all we need to declare an actual Widget:
class ChildSize extends SingleChildRenderObjectWidget {
final void Function(Size)? onChildSizeChanged;
const ChildSize({
Key? key,
Widget? child,
this.onChildSizeChanged,
}) : super(key: key, child: child);
}
Nothing new here except SingleChildRenderObjectWidget. This is one of the helpers in the Flutter framework which allows us to write custom Widgets with no more than one child. This simplifies our code quite a lot because we don’t need to write custom Element at all.
The only thing we need to add to our Widget is the creation of the RenderObject and updating it when our Widget changes:
class ChildSize extends SingleChildRenderObjectWidget {
// ...
@override
RenderObject createRenderObject(BuildContext context) {
return RenderChildSize().._widget = this;
}
@override
void updateRenderObject(BuildContext context, RenderChildSize renderObject) {
renderObject.._widget = this;
}
}
Now we need to create our RenderObject:
class RenderChildSize extends RenderBox
with RenderObjectWithChildMixin<RenderBox> { var _widget = const ChildSize();
}
RenderObjectWithChildMixin is a special mixin that we must add when working with SingleChildRenderObjectWidget. This mixin will handle all the boring stuff (see next article). Almost each helper of this kind requires some mixin to be added. You can find these requirements in the documentation for each helper.
Next thing to do is to layout our child:
class RenderChildSize ... {
// ...
var _lastSize = Size.zero;
@override
void performLayout() {
final child = this.child;
if (child != null) {
child.layout(constraints, parentUsesSize: true);
size = child.size;
} else {
size = constraints.smallest;
} if (_lastSize != size) {
_lastSize = size;
_widget.onChildSizeChanged?.call(_lastSize);
}
}
}
During layout we must decide what size our RenderObject will have. To do that we need to layout our child. Our RenderObject’s size will be the same as the size of the child (we don’t have any paddings, margins, etc.).
Few important notes here:
- we must pass
parentUsesSize = true
to child’s layout function in order to get its size afterwards. Otherwisechild.size
will throw exception. Thanks to this flag Flutter can add additional optimisations. - there can be a case that even though we have a child Widget, there will be no child RenderObject for it. Not all Widgets have RenderObjects. In such case Flutter will try to provide us with nearest nested RenderObject if any exists.
In this method we also check if size has changed and invoke the callback when it does.
The last things we need to do is to paint our child and to route hit test events:
class RenderChildSize ... {
// ...
@override
void paint(PaintingContext context, Offset offset) {
final child = this.child;
if (child != null) {
context.paintChild(child, offset);
}
}
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
return child?.hitTest(result, position: position) == true;
}
}
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.