Flutter: Understanding the Basics of the Flow Widget

Sachin Kumar Singh
FlutterFlakes
Published in
4 min readJul 14, 2024

Flow Widget is a unique kind of layout widget that provides developers with fine-grained control over the layout. The Flow widget’s flow-delegate distinguishes it from other layout widgets. Flow allows you to construct simple layouts such as:

  • Column
  • Row
  • Grid
  • Stack

But why use Flow when we already have the above layout widgets directly defined? The answer to this question is the level of customization the Flow widget offers. Not only can you design a simple layout using the Flow widget, but you can also add various animations and effects. For more details, you can check out the official documentation offered by Flutter.

To get you started with Flow, let’s begin with its syntax. The Flow widget takes three parameters:

Flow
delegate: YourFlowDelegate()
children: [],
clipBehaviour:
)

The Flow widget takes three parameters:

  1. delegate: This is a required property where you provide a FlowDelegate. The FlowDelegate is responsible for determining the layout of the children. It controls how the children are positioned and sized within the Flow widget.
  2. clipBehavior: Determines how to clip the content within the Flow widget's boundaries. The default value is Clip.hardEdge. Other options include Clip.none, Clip.antiAlias, and Clip.antiAliasWithSaveLayer.
  3. children: A list of child widgets.

Flow Delegate

class YourFlowDelegate extends FlowDelegate {

@override
void paintChildren(FlowPaintingContext context) {
}

@override
bool shouldRepaint(covariant FlowDelegate oldDelegate) {
}

//optional
@override
Size getSize(BoxConstraints constraints) {
}

//optional
@override
BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) {
}
}

When you create your own Flow delegate by extending the abstract class FlowDelegate, there are two mandatory methods that you have to implement:

  1. paintChildren(FlowPaintingContext context): This method is called to paint the children of the Flow widget. You use the FlowPaintingContext to get information about the children and to position and size them. You can use transformation matrices to create various layouts and effects.
  2. shouldRepaint(covariant FlowDelegate oldDelegate): This method is called to determine whether the layout should be repainted when the delegate changes.
  3. getSize: This method returns the size of the Flow widget. You can override this if you need to provide a custom size.
  4. getConstraintsForChild: This method returns the constraints for each child. This can be useful if you want to limit the size of individual children based on some criteria.

Let’s try to create a simple row layout and understand how we can use these methods of the delegate class.

Output of below code
import 'package:flutter/material.dart';

class BasicLayouts extends StatelessWidget {
const BasicLayouts({Key? key}) : super(key: key);

List<Widget> generateContainers() {
final colors = [
Colors.redAccent,
Colors.blueAccent,
Colors.greenAccent,
Colors.orangeAccent
];
final texts = ['1', '2', '3', '4'];

return List<Widget>.generate(colors.length, (index) {
return Container(
decoration: BoxDecoration(shape: BoxShape.circle,
color: colors[index]),
width: 100,
height: 100,
alignment: Alignment.center,
child: Text(texts[index]),
);
});
}

@override
Widget build(BuildContext context) {
return Row(
children: generateContainers(),
);
}
}

In the above code, let’s replace the Row with the Flow widget and create our own FlowDelegate class.

 @override
Widget build(BuildContext context) {
return Flow(
delegate: LayoutFlowDelegate(),
children: generateContainers(),
);
}
class LayoutFlowDelegate extends FlowDelegate {

@override
void paintChildren(FlowPaintingContext context) {
for (int childIndex = 0; childIndex < context.childCount; childIndex++) {
double dx = 0;
if (context.getChildSize(childIndex) != null) {
dx = childIndex * context.getChildSize(childIndex)!.width;
}
context.paintChild(childIndex,
transform: Matrix4.translationValues(dx, 0, 0));
}

@override
bool shouldRepaint(FlowDelegate oldDelegate) {
return false;
}

@override
Size getSize(BoxConstraints constraints) {
return const Size(double.infinity, 400);
}
}

Let’s go step by step and see what each function is doing:

  1. paintChildren: Inside this function, we are doing three things:
  • Looping through each child using a for loop.
  • Calculating the horizontal offset (dx) for each child. dx is calculated based on the child's index and its width. For example, if the first child has a width of 100, its dx will be 0. The second child's dx will be 100, the third child's dx will be 200, and so on.
  • Translating and painting each child. The context.paintChild method is called with the child index and the transformation matrix created by Matrix4.translationValues(dx, 0, 0). This moves each child horizontally to its calculated position.

2. shouldRepaint: This method returns false because, in this scenario, we don't want the UI to be repainted.

3. getSize: It returns the size of the Flow widget. Here, I am limiting the height to 400 pixels and keeping the width flexible.

Suppose, for some reason, you want to change the height of the fourth child. You can achieve this by overriding getConstraintsForChild.

  @override
BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) {
return i == 3
? const BoxConstraints.tightFor(width: 100, height: 50)
: const BoxConstraints.tightFor(width: 100, height: 100);
}
output of above code

Conclusion:

In the above section, we saw how we can use the Flow widget to create some basic layouts. We also saw the comprehensive control Flow provides to a developer at the pixel level. With the Flow widget, we can accomplish a lot more amazing things, such as creating complex layouts and animations.

In my next article, I will explain how we can use Flow along with AnimationController to achieve some basic sliding effects. Until then, you can check out the full code of the above example here.

--

--