Adding a Gap to Flutter
Recently, I wished that Row
or Column
widgets had a spacing
property to configure gaps between child widgets.
Because I find it too cumbersome to add padding to those children — both because this further indents the widgets in the widget tree and because its difficult to decide whether one should add only a bottom padding, or only a top padding, or paddings to both the top and the bottom of child widgets — I most often use a SizedBox
widget to introduce gaps. And there’s another inconvenience: I have to explicitly set the width
or the height
property dependent on whether the gap is added to a Row
or Column
and to use constants, I have to introduce both smallVerticalGap
and smallHorizontalGap
or something similar.
Can we create a Gap
widget that automatically does the right thing?
Yes, we can!
The Gap
Here is my implementation.
Because its layout is depdent on the direction
of the Flex
widget it is a child of (the Spacer
widget has the same problem) this cannot be achieved with widgets alone. I need a special render object that performs the special layout. Therefore, Gap
shall be a subclass of SingleChildRenderObjectWidget
:
class Gap extends SingleChildRenderObjectWidget {
const Gap({Key key, this.size = 16})
: assert(size != null),
super(key: key); final double size; …
Gap
is now required to implement a createRenderObject
method which, well, creates a render object. I shall call this object RenderGap
, following Flutter’s naming convention.
… @override
RenderGap createRenderObject(BuildContext context) {
return RenderGap(size);
} …
As Gap
has one property which must be passed down, I also implement updateRenderObject
to make sure that changes to that property also affect my render object:
… @override
void updateRenderObject(BuildContext context, RenderGap renderObject) {
renderObject.gapSize = size;
}
}
The RenderGap
is implemented as a subclass of RenderBox
which provides most of the functionallity needed. Because render objects — in constrast to widgets — are mutable, I make sure that changes to the gapSize
property trigger a re-layout, but only if the size really changes.
class RenderGap extends RenderBox {
RenderGap(this._gapSize); double _gapSize;
double get gapSize => _gapSize;
set gapSize(double gapSize) {
if (_gapSize != gapSize) {
_gapSize = gapSize;
markNeedsLayout();
}
} …
We’re done with the boiler plate code.
To implement layout, we have to override the performLayout
method which must set the object’s size
property. The documentation says that it also should honor the object’s constraints
, but for now, I ignore them because I’m a bit in a hurry. Hopefully, my assumption is correct that performLayout
is only called once the object has been added to the render tree and therefore always has a parent
.
I assign a size based on the layout direction of its parent like so:
… @override
void performLayout() {
final parent = this.parent;
if (parent is RenderFlex) {
if (parent.direction == Axis.horizontal) {
size = Size(_gapSize, 0);
} else {
size = Size(0, _gapSize);
}
} else {
throw FlutterError(‘Gap must be used inside a Flex widget’);
}
}
}
Now, it is possible to add gaps to columns or rows:
Column(
children: [
Text(‘Here is a message’),
Gap.large,
Row(
children: [
Spacer(),
RaisedButton(
onPressed: () => null,
child: Text(‘Cancel’),
),
Gap.small,
RaisedButton(
onPressed: () => null,
child: Text(‘OK’),
),
],
),
]
);
For a complete implementation one must also override the four compute{Min,Max}Intrinsic{Width,Height}
methods. Instead of 0, they should return gapSize
if there is a parent
of class RenderFlex
with the right `direction
`.
Here is the complete source code.
Alternatives to consider
After implementing this, I’d love to hear other people’s opinion whether this is useful or not. Or should we define something like
List<Widget> rowGap(double gap, Iterable<Widget> children) {
return children.expand((child) sync* {
yield SizedBox(width: gap);
yield child;
});
}List<Widget> columnGap(double gap, Iterable<Widget> children) {
return children.expand((child) sync* {
yield SizedBox(height: gap);
yield child;
});
}
and then use those method like this
Column(
children: columnGap(32, [
Text(‘Here is a message’),
Row(
children: rowGap(8, [
Spacer(),
RaisedButton(
onPressed: () => null,
child: Text(‘Cancel’),
),
RaisedButton(
onPressed: () => null,
child: Text(‘OK’),
),
]),
),
])
);
instead?
At least, this articles demonstrates how to create a simple render object in case you need one. At lease leaf render object are simple to create and sometimes useful.