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:

  1. Every child you pass to children needs to be a LayoutId and you pass the widget you actually want to show as a child to that LayoutId. The id 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'),
    ),
  ],
)
  1. 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 (remember LayoutId?) was passed to the children, 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 the Size of that child.

  • positionChild, which allows you to change the position from Offset(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 and 2 as the ids in this example, but using an enum is probably the best way to handle the ids if you have specific ids.

  • You can pass a Listenable to super (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 to layoutChild above.

  • getPositionForChild, which is equivalent to positionChild 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.