Adding a Gap to Flutter

Stefan Matthias Aust
ICNH
Published in
3 min readMay 2, 2020

--

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.

--

--

Stefan Matthias Aust
ICNH
Editor for

App Developer · Co-Founder of I.C.N.H GmbH · Pen & paper role player