Flutter: create your custom layout

A long time ago (a couple of weeks ago in fact), in a galaxy far far away (well, not from us, because we talked about our Milky Way)… a developer wanted to reproduce a design found on dribbble.com… In Flutter of course :D

Well, in that article I will show you that you have all needed stuff from the Flutter framework to design yourself a complex layout widget.

This widget will have to display a list of rectangular children having different sizes; a kind of Tetris layout. Or a Maya wall, which may be a better comparison.

The proof that it is a solid layout :D (or maybe I digress?)

After having searched on pub package, I’m not able to find exactly that kind widget. And in fact, that’s not very important: sometimes it can be more easy to develop our own widget than try to twist an existing one for our needs. Especially if it requires no specific knowledge (except Flutter of course).

Methodology

It’s important to have a plan on how to develop such a thing.

  • First check if there is a widget or a combination of widgets in the framework that help you developing your widget.
  • Then study their limitations and constraints: what I won’t be able to do, what information do I need ?
  • Make a Proof Of Concept (POC): the idea is to quickly test that your idea actually works. But that’s not enough: you may want to qualify your solution (proper display, quick layout, moderate memory usage, flexible solution,…)
  • Finally, enhance the POC in order to have a concrete widget. In our case, we may want to make the layout scrollable, as a list. And thinking about ListView widget, why not taking inspiration from it to design our layout ?
Me realizing that it will take more time than expected.

Check and study

First of all I checked it there is a widget in the Flutter framework that layout as expected. The short answer is no, but I found interesting widget that may help us: CustomMultiChildLayout, Flow, Table.

Some of us are not displaying children widgets as we want, but they are interesting because we can study them and understand how they are designed, and what problems we may find.

Because to be honest, I’m not sure to understand properly (and in details) how widgets are rendered. It appears that there is three trees:

  • one for widgets, describing UI as immutable objects. They are relatively cheap in memory, because they contains the “configuration” of the graphical element only.
  • another one for render objects, concretely painting graphical components. They are generally more expensive than widget, therefore the strategy used by the rendering library is to modify them instead of re-create them.
  • the third one, a tree of Element objects, makes the links between widgets and renderObjects. Especially it’s used to decide when to reuse a render object, and when to change it (for example, in case the widget type differs).

So as an advice, I recommend to study framework widgets, to enhance your knowledge about Flutter.

I mean to confront yourself with framework problems, solutions, limitations, designs …

My first thought was to create a layout manager defining how to position children, and then display them into a Stack or a ListView of Row/Column widgets. But it appears to hide two problems:

  • The Stack will try to render all elements, even if not displayed. And what about scroll feature ?
  • The ListView may fix the rendering problem of the Stack. But it may be too complex to render children in a combination of rows and columns.

After investigation and study, I will develop a POC based on the CustomMultiChildLayout widget, because it is the most flexible widget for our purpose. I also found all widget related to the CustomMultiChildLayout widget (by reading the documentation, of course):

  • LayoutId: it’s a proxy widget having a id property to identify a child. So my children will have to use it by inheritance
  • MultiChildLayoutDelegate: Used to position children into the custom layout. Internally, it handle render objects itself, so I don’t need to care about. We can also define the total layout size here, but it cannot be used at rendering time.

So the main difficulty will be to define where to render children. Also, it requires to know the full height of the widget before rendering children.

I think I’m pretty much prepared to start my POC ;D

Developing the Proof Of Concept (POC)

A poc aims to test an idea: you can consider it as a tool that will help you creating your new custom widget. But there is a big differences between POC and final code: most of the time (sweet euphemism) a POC is not correctly documented, not well testable, not well designed (too much public attributes and methods, bad package/class/attribute/method names, too complex code, commented or unused code, …).

I will explain the last version of my code, which is not considered a POC anymore. During the POC development, I used unit tests to check that specific parts of my code were correctly working. And added to that a little example of usage, to check integration. Don’t forget that you will always have these tools to produce testable and reliable code quicker.

Use tools wisely (but use them) !

First, the easy part of the design: the user will be able to define the number of columns the layout have. For each child, the relative width and height. As example, if the user define 3 columns in the layout, a child having width and height equal to 1 will have a side size of a third (1/3) of the width of the parent (the layout).

class Stone extends LayoutId {

/// Stone width (relative to the number of wall layers count). Must be higher or equal to 1.
final int width;

/// Stone height (relative to the number of wall layers count). Must be higher or equal to 1.
final int height;

Stone({int id, Widget child, this.width, this.height})
: assert(width != null),
assert(width > 0),
assert(height != null),
assert(height > 0),
super(child: child, id: id);

The complex part will be to develop the algorithm positioning the layout children. Here is my trick to do that: create a class exclusively dedicated to that, and make the main method (concretely the one executing the algorithm) the most maintainable.

To do so, I wrote as comment every main steps of the algorithm, then I create methods for every steps.

/// Compute stones position and wall size.
void setup() {
// instantiate grid
final surface = this.stones.fold(0, (sum, cell) => sum + cell.surface);
this.grid = List<int>.generate(surface * axisSeparations, (index) => null);

// set stones positions in grid
this.stones.forEach((stone) => computeStonePosition(stone));

//compute grid height and width
_wallSize = computeSize();

//remove unwanted grid data
this.
grid.removeRange(_wallSize.surface, grid.length);

...
}

As you can see, main methods involved in the algorithm are public. This is for a good cause: being testable! So as you might understand, instances of that class will be hidden in another way (private class in my case).

KISS principle: Keep It Simple and Stupid (not like these guys).

I repeat that process for other complex methods, such as computeStonePosition method.

/// Compute the position of the stone, and set it on the grid.
/// Only public for testing purpose; consequently this class must remain hidden from users.
void computeStonePosition(Stone stone) {
bool found = false;
int startSearchPlace = 0;
int availablePlace;

// find first place in grid that accept stone's surface
while (!found) {
availablePlace = this.grid.indexWhere((element) => element == null, startSearchPlace);
found = __canFit(stone, availablePlace);
startSearchPlace = availablePlace + 1;
}
// insert stone into available place
__placeOnGrid(stone, availablePlace);
}

You can note that in computeStonePosition, we use two usefull method: __canFit and __placeOnGrid. In could have been unit tested, but I want to keep a hierarchy in the methods. I was not able to do it, as there is no protected method. If you have a method to test private members without using reflection/introspection mechanism, please tell me!

After having developed and unit tested the algorithm, then I wrapped inside a class inheriting from MultiChildLayoutDelegate, to properly display layout children.

I override the getSize method, that let you define the layout size. In my case, I used the algorithm (handler attribute below) to compute the full height, depending on the width.

/// Delegates for CustomMultiChildLayout that position and size Stones.
class _WallLayoutDelegate extends MultiChildLayoutDelegate {
final WallBuildHandler handler;
...@override
Size getSize(BoxConstraints constraints) {
final constrainedSide =
this.handler.direction == Axis.vertical ? constraints.maxWidth : constraints.maxHeight;

final side = (constrainedSide / this.handler.layersCount);
return this.handler.size * side;
}

The other useful method to override is performLayout, that let you define position for each child of your custom layout. It implies you to use positionChild and layoutChild methods inside. No need to override them!

My “little inner voice” about overriding positionChild and layoutChild methods.
@override
void performLayout(Size size) {
double side = ((this.handler.direction == Axis.vertical ? size.width : size.height) - this.stonePadding) /
this.handler.layersCount;
final initialPadding = Offset(this.stonePadding, this.stonePadding);
this.handler.stones.forEach((stone) {
Offset offset = this.handler.getPosition(stone) * side;
Size size = Size(
stone.width * side - this.stonePadding,
stone.height * side - this.stonePadding,
);

positionChild(stone.id, initialPadding + offset);
layoutChild(stone.id, BoxConstraints.tight(size));
});
}

Thanks to delegating computation parts to the algorithm, this method remains enough simple: for each child, we compute the child’s offset ( position inside the layout) and child’s size.

Then we use positionChild to let the MultiChildLayoutDelegate set render object positions, and layoutChild to let MultiChildLayoutDelegate update its render objects.

Don’t forget that MultiChildLayoutDelegate is internally optimized to render your layout children (widgets). So no need to worry too much about optimization at this stage.

Trust me, or go check internal MultiChildLayoutDelegate code ;)

Enhance POC to create a widget

Well, that’s not finished yet. At this point, you “made something”. But not clearly good stuff. You may acknowledge that a good widget must be easy to use. So in our case, we have to wrap all complex stuff into a widget that will be as simple as ListView. Why ListView ? Because every Flutter developer know this widget, and the custom layout usage I want is approximately the same.

Consider that at this stage, you have done 50% of the work (if you already have some unit tests).

I want the user (you) to define a list of children, and control layout as ListView (in terms of scrolling and display direction).

One inconvenient is that every child must be an instance of Stone. By the way, it hides the fact that custom layout children must inherit from LayoutId widget.

WallLayout(
{this.layersCount,
this.stones,
this.stonePadding = DEFAULT_BRICK_PADDING,
this.scrollController,
this.primary,
this.physics,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.dragStartBehavior = DragStartBehavior.start,
this.scrollDirection = Axis.vertical,
this.reverse = false})
: assert(stones != null && stones.isNotEmpty),
assert(layersCount != null && layersCount >= 2,
"You must define layers count from as an integer higher or equal to 2"),
assert(stonePadding != null && stonePadding >= 0.0),
assert(!(scrollController != null && primary == true),
'Primary ScrollViews obtain their ScrollController via inheritance from a PrimaryScrollController widget. '
'You cannot both set primary to true and pass an explicit controller.'
),
super() {
assert(this.stones.map((stone) => stone.id).toSet().length == this.stones.length, "Stones identifier must be unique.");
this.stones.forEach((stone) {
final constrainedSide = this.scrollDirection == Axis.vertical ? stone.width : stone.height;
assert(constrainedSide <= this.layersCount,
"Stone $stone is too big to fit in wall : constrained side ($constrainedSide) is higher than axisDivision ($layersCount)");
});
}

As you can see, I simply retrieved ListView input that are interesting, and also implement a similar behavior by adding default values. Added to that, we need to check that all stones can be displayed: they must have unique identifier, and must be entirely layout (constrained side must not exceed the number of layers).

You will understand that 50% of the remaining work will occupy you in adding new parameters, enhancing reliability (assert and unit tests), adding documentation and last but not least, finding proper names !

Really, this is a lot of work to have code and design cleaned !

Conclusion

It was fun to do that, so I packaged my code into pub.package. You can use it here :

Follow Flutter Community on Twitter: https://www.twitter.com/FlutterComm

Hi, I’m David, a french mobile applications developer. As a freelance, I work on Android and iOS applications since 2010. I also work on Flutter now !