How to use CustomMultiChildLayout & CustomSingleChildLayout in Flutter
First of all, I want to say that I am glad to help you with this as I can understand your struggles - there are benefits to figuring it out by yourself as well (the documentation is amazing).
What CustomSingleChildLayout
does will be obvious after I explained CustomMultiChildLayout
to you.
CustomMultiChildLayout
The point of this widget is allowing you to layout the children you pass to this widget in a single function, i.e. their positions and sizes can depend on each other, which is something you cannot achieve using e.g. the prebuilt Stack
widget.
CustomMultiChildLayout(
children: [
// Widgets you want to layout in a customized manner
],
)
Now, there are two more steps you need to take before you can start laying out your children:
- Every child you pass to
children
needs to be aLayoutId
and you pass the widget you actually want to show as a child to thatLayoutId
. Theid
will uniquely identify your widgets, making them accessible when laying them out:
CustomMultiChildLayout(
children: [
LayoutId(
id: 1, // The id can be anything, i.e. any Object, also an enum value.
child: Text('Widget one'), // This is the widget you actually want to show.
),
LayoutId(
id: 2, // You will need to refer to that id when laying out your children.
child: Text('Widget two'),
),
],
)
- You need to create a
MultiChildLayoutDelegate
subclass that handles the layout part. The documentation here seems to be very elaborate.
class YourLayoutDelegate extends MultiChildLayoutDelegate {
// You can pass any parameters to this class because you will instantiate your delegate
// in the build function where you place your CustomMultiChildLayout.
// I will use an Offset for this simple example.
YourLayoutDelegate({this.position});
final Offset position;
}
Now, all the setup is done and you can start implementing the actual layout. There are three methods you can use for that:
hasChild
, which lets you check whether a particular id (rememberLayoutId
?) was passed to thechildren
, i.e. if a child of that id is present.layoutChild
, which you need to call for every id, every child, provided exactly once and it will give you theSize
of that child.positionChild
, which allows you to change the position fromOffset(0, 0)
to any offset you specify.
I feel like the concept should be pretty clear now, which is why I will illustrate how to implement a delegate for the example CustomMultiChildLayout
:
class YourLayoutDelegate extends MultiChildLayoutDelegate {
YourLayoutDelegate({this.position});
final Offset position;
@override
void performLayout(Size size) {
// `size` is the size of the `CustomMultiChildLayout` itself.
Size leadingSize = Size.zero; // If there is no widget with id `1`, the size will remain at zero.
// Remember that `1` here can be any **id** - you specify them using LayoutId.
if (hasChild(1)) {
leadingSize = layoutChild(
1, // The id once again.
BoxConstraints.loose(size), // This just says that the child cannot be bigger than the whole layout.
);
// No need to position this child if we want to have it at Offset(0, 0).
}
if (hasChild(2)) {
final secondSize = layoutChild(
2,
BoxConstraints(
// This is exactly the same as above, but this can be anything you specify.
// BoxConstraints.loose is a shortcut to this.
maxWidth: size.width,
maxHeight: size.height,
),
);
positionChild(
2,
Offset(
leadingSize.width, // This will place child 2 to the right of child 1.
size.height / 2 - secondSize.height / 2, // Centers the second child vertically.
),
);
}
}
}
Two other examples are the one from the documentation (check preparation step 2) and a real world example I wrote some time back for the feature_discovery
package: MultiChildLayoutDelegate
implementation and CustomMultiChildLayout
in the build
method.
The last step is overriding the shouldRelayout
method, which simple controls whether performLayout
should be called again at any given point in time by comparing to an old delegate, (optionally you can also override getSize
) and adding the delegate to your CustomMultiChildLayout
:
class YourLayoutDelegate extends MultiChildLayoutDelegate {
YourLayoutDelegate({this.position});
final Offset position;
@override
void performLayout(Size size) {
// ... (layout code from above)
}
@override
bool shouldRelayout(YourLayoutDelegate oldDelegate) {
return oldDelegate.position != position;
}
}
CustomMultiChildLayout(
delegate: YourLayoutDelegate(position: Offset.zero),
children: [
// ... (your children wrapped in LayoutId's)
],
)
Considerations
I used
1
and2
as the ids in this example, but using anenum
is probably the best way to handle the ids if you have specific ids.You can pass a
Listenable
tosuper
(e.g.super(relayout: animation)
) if you want to animate the layout process or trigger it based on a listenable in general.
CustomSingleChildLayout
The documentation explains what I described above really well and here you will also see why I said that CustomSingleChildLayout
will be very obvious after understanding how CustomMultiChildLayout
works:
CustomMultiChildLayout is appropriate when there are complex relationships between the size and positioning of a multiple widgets. To control the layout of a single child, CustomSingleChildLayout is more appropriate.
This also means that using CustomSingleChildLayout
follows the same principles I described above, but without any ids because there is only a single child.
You need to use a SingleChildLayoutDelegate
instead, which has different methods for implementing the layout (they all have default behavior, so they are technically all optional to override):
getConstraintsForChild
, which is equivalent to the constraints I passed tolayoutChild
above.getPositionForChild
, which is equivalent topositionChild
above.
Everything else is exactly the same (remember that you do not need LayoutId
and only have a single child instead of children
).
MultiChildRenderObjectWidget
This is what CustomMultiChildLayout
is built on.
Using this requires even deeper knowledge about Flutter and is again a bit more complicated, but it is the better option if you want more customization because it is even lower level. This has one major advantage over CustomMultiChildLayout
(generally, there is more control):
CustomMultiChildLayout
cannot size itself based on its children (see issue regarding better documentation for the reasoning).
I will not explain how to use MultiChildRenderObjectWidget
here for obvious reasons, but if you are interested, you can check out my submission to the Flutter Clock challenge after January 20, 2020, in which I use MultiChildRenderObjectWidget
extensively - you can also read an article about this, which should explain a bit of how all of it works.
For now you can remember that MultiChildRenderObjectWidget
is what makes CustomMultiChildLayout
possible and using it directly will give you some nice benefits like not having to use LayoutId
and instead being able to access the RenderObject
's parent data directly.
Fun fact
I wrote all the code in plain text (in the StackOverflow text field), so if there are errors, please point them out to me and I will fix them.