I love how Flutter is designed. The combination of immutables objects, the
Widgets and mutables ones called
RenderObjects is very powerful although not super intuitive for beginners.
We can do a lot just by composing widgets with
StatefulWidgets, but we cannot do everything, or not in an efficient way. That’s why you need to understand how to create your own
RenderObject when it’s necessary.
In this post I will not explain what is the difference between a
Widget and a
RenderObject but I will show you how I created a simple
RenderObject in order for you to do the same when you need it. If you want to know more about
RenderObjects before reading this, I advise you to read this excellent article of Norbert.
Last week I was fed up with adding gaps between my widgets in some
Rows. I use
SizedBoxes for that, but we need to specify the width or the height depending on whether we are inside a
Column or a
Row. I don’t find this very productive so I ended up creating my own
RenderObject for that. I called the underlaying widget
Fun fact 🙃: Three days after I started to work on this widget, I saw this tweet in my TL:
I was very amused by the timing! I read the article of Stefan Matthias Aust where he explains how he created a Gap widget with pretty much the same implementation than mine. I will try to make a different analyze 😉.
The following code is the prototype of my widget. I think it’s simple enough to explain how to easily create a new
Widget and its associated
RenderObject is the piece of code responsible of computing the layout and paint it to the screen. I start with the render layer, because it’s completely independent from the widget layer.
There are (for the moment) two kinds of
To know which of them you must extend, it’s pretty simple: If your component needs to be aware of the scroll constraints (for a scroll effect or because it needs to be in a scroll container), the
RenderObject must extend
RenderSliver, otherwise it must extend
Pro tip 💪: In most of the cases you want a
In our context, the parent of our widget will be a
Flex widget, which only understands
RenderBoxes, so we don’t even have a choice, our
RenderObject must extend
RenderGap have only one property called
mainAxisExtent. This is the extent of our gap in the direction of its parent: if the widget’s parent is a
Column, then this is the height and if it’s a
Row then it represents the width.
We set the
mainAxisExtent at the creation and we can update it with the associated setter. This setter makes a call to
markNeedsLayout, which tells the framework that it needs to re-run the
performLayout method before rendering the frame.
But what it this
performLayout method? As its name indicates, it’s the method responsible for computing the layout of our component. In the case of a
RenderBox, it does so by setting its
In this method we know that the constraints are already computed and we can use them to determine our size.
As I mentioned before, the parent of our widget is either a
Column or a
Row. Both of them extend
Flex and the associated
RenderObject of a
Flex is a
We want our
RenderGap to only work if the parent is a
RenderFlex, so this is the first thing we do in the
performLayout method. Then we look at the
direction of its parent to know how to compute the
size. If it’s
Axis.horizontal, then we are in a
Row, and we want the
size to have a
width of the value of our
mainAxisExtent, and a height of 0. But we need to constrain our requested size with the constraints imposed by the parent. If the
Gap is the direct child of a
Row, it will not be constrained horizontally (
maxWidth will have an infinite value), but if there is a
Flexible between the
Row and the
Gap, then the
maxWidth constrain will have a finite value, and thus we have to take into account this constraint. This is the meaning of
Oh wait, didn’t I tell you that the parent of a
Gap must be a
Flex widget before? Then how a
Flexible can be between a
Row and a
Well, I wasn’t totally precise. I should have written that the direct parent of a
RenderGap must be a
RenderFlex but the direct parent of a
Gap is not necessarily a
This can be possible because there are widgets which are not visible and thus don’t need to be tied up with a
This is the case for example with the
Flexible widget. If we look at the code we can see that
ParentDataWidget<Flex> and it does not create any
This is all for the
RenderObject. We don’t even need to override the
paint method because we don’t want to paint anything, we just want to tell the framework that our component takes some place and that’s it.
To fully respect the
RenderObject contract we should also implement other methods such as
computeMinIntrinsicWidth, but we will stop here for now.
We already have our
RenderObject, now we need to create a specific widget in order to add it to the
build method of another widget. Our widget must extend
RenderObjectWidget since it’s associated with a
RenderGap. If we don’t want to manage the children ourselves, we can use one of the three out-of-the-box classes depending on the number of children the widget can take:
- For zero children the widget must extends
- For one child the widget must extends
- For two or more children the widget must extends
In our case, the
Gap widget has no children, that’s why
There are two methods to override when we extend
createRenderObject: this method is called the first time the widget is added to the widget tree. It must return a new instance of the associated
RenderObjectpopulated with the properties set or computed from the widget.
updateRenderObject: this method is called every time the widget is updated. It must update the previous instance of the associated
RenderObjectwith the properties set or computed from the widget.
In our case, the code is very simple since we have only one property to pass to the
RenderGap needs a
mainAxisExtent, so our widget also needs to have a
mainAxisExtent property in order to be set within a
And that’s it, this is the only things we have to do for the widget layer.
This is a small example of this widget for testing it. Enjoy!
This was a very simple widget which uses directly a
RenderObject behind the scene. I made it a package downloadable on pub.dev if you find it useful. The code of the package is a little more complex because I added some options to allow the
Gap to set its own
crossAxisExtent and a
color. Feel free to look at the full code on GitHub.
The rendering world is beautiful, but it can be difficult to understand, you’ll have to start with simple use cases and add complexity step by step.