Creating Cool UIs with Flow Widget
FlutterPulseThis article was translated specially for the channel FlutterPulseYou'll find lots of interesting things related to Flutter on this channel. Don't hesitate to subscribe!🚀

A Flow widget arranges and positions its child widgets efficiently using a FlowDelegate. Unlike traditional layout widgets, it applies…
A Flow widget arranges and positions its child widgets efficiently using a FlowDelegate. Unlike traditional layout widgets, it applies transformation matrices to reposition children dynamically, making it ideal for animations and frequent updates.
The Flow widget lays out and positions its children using a FlowDelegate, offering more control and performance than traditional layout widgets. Instead of determining positions during layout, Flow applies transformation matrices in the painting phase, making it well-suited for complex animations and frequent updates.
The FlowDelegate defines the container's size using getSize, and assigns constraints to each child with getConstraintsForChild. Actual positioning happens in paintChildren, allowing the widget to update child positions without triggering a full layout pass—unlike widgets like Stack, which handle both sizing and positioning during layout.
To boost performance, Flow can listen to an animation passed to its delegate. This lets it repaint in sync with the animation, avoiding unnecessary rebuilds or layout recalculations.
Let's take a look at some practical examples:

If you're curious about how this widget is built, here's the code:
import 'package:flutter/material.dart';
import 'dart:math';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Flow Example'),
),
body: Center(
child: Flow(
delegate: _FlowDelegate(),
children: List.generate(5, (index) {
return FloatingActionButton(
onPressed: () {},
backgroundColor: Colors.primaries[index % Colors.primaries.length],
child: const Icon(Icons.accessibility_new),
);
}),
),
),
);
}
class _FlowDelegate extends FlowDelegate {
@override
void paintChildren(FlowPaintingContext context) {
const double radius = 100.0;
double angle = 0.0;
double angleIncrement = 2 * pi / context.childCount;
for (int i = 0; i < context.childCount; i++) {
final double x =
radius * cos(angle) +
context.size.width / 2 -
context.getChildSize(i)!.width / 2;
final double y =
radius * sin(angle) +
context.size.height / 2 -
context.getChildSize(i)!.height / 2;
context.paintChild(i, transform: Matrix4.translationValues(x, y, 0.0));
angle += angleIncrement;
}
}
@override
bool shouldRepaint(covariant FlowDelegate oldDelegate) => false;
}
The _FlowDelegate class extends FlowDelegate and overrides paintChildren to create a custom layout. In this case, it arranges children in a circular pattern with a fixed radius of 100.0 units. The angleIncrement sets the spacing between children based on how many there are. For each child, the method calculates its position using cos and sin functions, placing them evenly around the center. Each child is then painted at its calculated position using context.paintChild
@override
bool shouldRepaint(covariant FlowDelegate oldDelegate) => false;
This returns false because there's no animation, so we don't need any extra conditions.
Now let's level up and build a dynamic UI: an animated Floating Action Button (FAB) menu from scratch — no third-party packages. Using AnimationController and the Flow widget, we'll create smooth, efficient animations. Here's a preview of what we're building:

In this initial step, the animation setup involves two key components: AnimationController and Animation. The AnimationController, which handles the timing of the animation, is initialized in initState with a 300-millisecond duration and synced to the screen's refresh rate via vsync. A Tween<double> defines the animation range from 0 to 1, and the animate method connects it to the controller, allowing the animation to interpolate smoothly over time. The controller is disposed of in dispose to avoid memory leaks when the widget is removed from the tree. This setup establishes the core structure for managing animations in the app.
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_animation = Tween<double>(begin: 0, end: 1).animate(_controller);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
In this second step, the code defines a FloatingButton widget as a custom StatelessWidget that takes two required parameters: iconData and onPressed. The iconData is used to specify the icon to be displayed on the floating action button, while the onPressed is a callback function that is triggered when the button is pressed.
class FloatingButton extends StatelessWidget {
const FloatingButton({
super.key,
required this.iconData,
required this.onPressed,
});
final IconData iconData;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) => FloatingActionButton(
elevation: 0,
onPressed: onPressed,
child: Icon(iconData, size: 30),
);
}In this third step, a custom FlowWidgetDelegate class is defined, extending FlowDelegate to control the layout and rendering of children within a Flow widget. The delegate takes an Animation<double>, elementsSpacing, iconsSpacing, and a Curve as parameters to control movement, spacing, and easing behavior. Inside paintChildren, each child's position is calculated based on the current animation value. A Matrix4.translationValues transformation is applied to animate the offset. The last child is handled separately, typically remaining static, while the rest are positioned dynamically using the animation value and curve to determine their offset. The shouldRepaint method returns true if any relevant properties change, ensuring that updates are reflected correctly. This delegate enables precise, animated control over child positioning within the Flow layout.
class FlowWidgetDelegate extends FlowDelegate {
const FlowWidgetDelegate({
required this.animation,
required this.elementsSpacing,
required this.iconsSpacing,
required this.curve,
}) : super(repaint: animation);
final Animation<double> animation;
final double elementsSpacing;
final double iconsSpacing;
final Curve curve;
@override
void paintChildren(FlowPaintingContext context) {
final size = context.size;
final x = size.width;
final y = size.height;
for (int i = 0; i < context.childCount; i++) {
if (i == context.childCount - 1) {
context.paintChild(
i,
transform: Matrix4.translationValues(
x - 50 - elementsSpacing,
y - 50 - elementsSpacing,
0,
),
);
} else {
var offset = 0.0;
if (animation.value == 0) {
offset = 0;
} else {
offset =
(curve.transform(animation.value) * 30) +
(iconsSpacing * context.childCount);
}
context.paintChild(
i,
transform: Matrix4.translationValues(
x - 50 - elementsSpacing,
y - offset * (i + 1) - 50 - elementsSpacing,
0,
),
);
}
}
}Finally we put all together using AnimatedBuilder:
Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Flow Widget'),
),
body: AnimatedBuilder(
animation: _animation,
builder:
(context, _) => Flow(
delegate: FlowWidgetDelegate(
animation: _animation,
elementsSpacing: 35,
iconsSpacing: 10,
curve: Curves.ease,
),
children: [
FloatingButton(iconData: Icons.phone, onPressed: () {}),
FloatingButton(iconData: Icons.email, onPressed: () {}),
FloatingButton(iconData: Icons.rss_feed, onPressed: () {}),
FloatingButton(
iconData: Icons.more_horiz,
onPressed: () async {
if (_controller.isCompleted) {
await _controller.reverse();
} else {
await _controller.forward();
}
},
),
],
),
),
);
To view and run the full example, check out the GitHub repository. If you find it useful, consider giving it a star — it's a small gesture that goes a long way in supporting future content.
In summary, Flutter's Flow widget is a powerful tool for building custom, animated layouts. This article covered the basics of how it works, and walked through examples to show how it can be used to create dynamic, interactive UI components. But this is just the starting point—the Flow widget gives you the flexibility to design virtually any layout or animation pattern your app requires.
Thanks for reading, and happy coding!