Custom Layouts
Understand how to create custom layout widgets in Flutter.
What is it?
Custom Layouts are widgets that implement your own layout logic when the built-in layout widgets (Row, Column, Stack, etc.) don't meet your needs. By creating custom layout widgets, you gain complete control over how children are sized and positioned, enabling complex and specialized layout patterns.
Why does it exist?
Custom Layouts exist to:
- Implement custom layout algorithms
- Create specialized layout patterns
- Handle complex positioning logic
- Build custom UI components
- Support unique design requirements
- Optimize layout performance
- Enable creative UI designs
MultiChildRenderObjectWidget
The foundation for custom multi-child layouts.
// 1. Create a custom widget class
class CustomFlowWidget extends MultiChildRenderObjectWidget {
const CustomFlowWidget({
super.key,
required super.children,
this.spacing = 8,
});
final double spacing;
@override
RenderObject createRenderObject(BuildContext context) {
return CustomFlowRenderObject(spacing: spacing);
}
@override
void updateRenderObject(
BuildContext context,
CustomFlowRenderObject renderObject,
) {
renderObject.spacing = spacing;
}
}
// 2. Create the render object
class CustomFlowRenderObject extends RenderBox
with ContainerRenderObjectMixin<RenderBox, BoxParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, BoxParentData> {
CustomFlowRenderObject({required double spacing}) : _spacing = spacing;
double _spacing;
set spacing(double value) {
if (_spacing == value) return;
_spacing = value;
markNeedsLayout();
}
@override
void setupParentData(RenderObject child) {
if (child.parentData is! BoxParentData) {
child.parentData = BoxParentData();
}
}
@override
void performLayout() {
// Layout logic here
}
@override
void paint(PaintingContext context, Offset offset) {
// Paint children
}
}
// 3. Using the custom widget
class CustomLayoutExample extends StatelessWidget {
const CustomLayoutExample({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: 300,
height: 200,
color: Colors.grey[200],
child: CustomFlowWidget(
spacing: 8,
children: [
Container(width: 50, height: 50, color: Colors.red),
Container(width: 60, height: 40, color: Colors.green),
Container(width: 40, height: 60, color: Colors.blue),
Container(width: 70, height: 30, color: Colors.orange),
Container(width: 30, height: 70, color: Colors.purple),
],
),
);
}
}
What's happening here? - Custom widget extends MultiChildRenderObjectWidget - RenderObject handles layout and painting - Children are managed with mixins - Full control over layout logic
Custom Layout Example: Masonry Flow
Creating a masonry/pinterest-style layout.
// 1. Masonry widget
class MasonryFlow extends MultiChildRenderObjectWidget {
const MasonryFlow({
super.key,
required super.children,
this.columnCount = 2,
this.spacing = 8,
});
final int columnCount;
final double spacing;
@override
RenderObject createRenderObject(BuildContext context) {
return MasonryRenderObject(
columnCount: columnCount,
spacing: spacing,
);
}
@override
void updateRenderObject(
BuildContext context,
MasonryRenderObject renderObject,
) {
renderObject
..columnCount = columnCount
..spacing = spacing;
}
}
// 2. Masonry render object
class MasonryRenderObject extends RenderBox
with ContainerRenderObjectMixin<RenderBox, BoxParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, BoxParentData> {
MasonryRenderObject({
required int columnCount,
required double spacing,
}) : _columnCount = columnCount,
_spacing = spacing;
int _columnCount;
set columnCount(int value) {
if (_columnCount == value) return;
_columnCount = value;
markNeedsLayout();
}
double _spacing;
set spacing(double value) {
if (_spacing == value) return;
_spacing = value;
markNeedsLayout();
}
@override
void setupParentData(RenderObject child) {
if (child.parentData is! BoxParentData) {
child.parentData = BoxParentData();
}
}
@override
void performLayout() {
// 1. Get constraints
final BoxConstraints constraints = this.constraints;
// 2. Calculate column widths
final totalSpacing = (_columnCount - 1) * _spacing;
final columnWidth = (constraints.maxWidth - totalSpacing) / _columnCount;
// 3. Track column heights
List<double> columnHeights = List.filled(_columnCount, 0.0);
// 4. Layout each child
RenderBox? child = firstChild;
while (child != null) {
// Find shortest column
int shortestColumn = 0;
double minHeight = columnHeights[0];
for (int i = 1; i < columnHeights.length; i++) {
if (columnHeights[i] < minHeight) {
minHeight = columnHeights[i];
shortestColumn = i;
}
}
// Layout child
child.layout(
BoxConstraints(
minWidth: columnWidth,
maxWidth: columnWidth,
minHeight: 0,
maxHeight: constraints.maxHeight,
),
parentUsesSize: true,
);
// Position child
final BoxParentData childParentData =
child.parentData! as BoxParentData;
childParentData.offset = Offset(
shortestColumn * (columnWidth + _spacing),
columnHeights[shortestColumn],
);
// Update column height
columnHeights[shortestColumn] += child.size.height + _spacing;
child = childAfter(child);
}
// 5. Set own size
final maxHeight = columnHeights.reduce((a, b) => a > b ? a : b);
size = constraints.constrain(Size(
constraints.maxWidth,
maxHeight,
));
}
@override
void paint(PaintingContext context, Offset offset) {
// Paint children
RenderBox? child = firstChild;
while (child != null) {
final BoxParentData childParentData =
child.parentData! as BoxParentData;
context.paintChild(child, offset + childParentData.offset);
child = childAfter(child);
}
}
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
return defaultHitTestChildren(result, position: position);
}
}
// 3. Using masonry layout
class MasonryExample extends StatelessWidget {
const MasonryExample({super.key});
@override
Widget build(BuildContext context) {
return Container(
height: 400,
color: Colors.grey[200],
child: MasonryFlow(
columnCount: 3,
spacing: 8,
children: [
_buildMasonryItem(100, Colors.red),
_buildMasonryItem(150, Colors.green),
_buildMasonryItem(80, Colors.blue),
_buildMasonryItem(120, Colors.orange),
_buildMasonryItem(200, Colors.purple),
_buildMasonryItem(90, Colors.pink),
_buildMasonryItem(160, Colors.teal),
_buildMasonryItem(110, Colors.indigo),
],
),
);
}
Widget _buildMasonryItem(double height, Color color) {
return Container(
height: height,
color: color,
child: Center(
child: Text(
'${height.toInt()}px',
style: const TextStyle(color: Colors.white),
),
),
);
}
}
What's happening here? - Masonry layout with variable heights - Items placed in shortest column - Custom layout algorithm - Full control over positioning
Custom Layout Example: Radial Layout
Creating a radial/circular layout.
// 1. Radial widget
class RadialLayout extends MultiChildRenderObjectWidget {
const RadialLayout({
super.key,
required super.children,
this.radius = 100,
this.startAngle = 0,
});
final double radius;
final double startAngle;
@override
RenderObject createRenderObject(BuildContext context) {
return RadialRenderObject(
radius: radius,
startAngle: startAngle,
);
}
@override
void updateRenderObject(
BuildContext context,
RadialRenderObject renderObject,
) {
renderObject
..radius = radius
..startAngle = startAngle;
}
}
// 2. Radial render object
class RadialRenderObject extends RenderBox
with ContainerRenderObjectMixin<RenderBox, BoxParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, BoxParentData> {
RadialRenderObject({
required double radius,
required double startAngle,
}) : _radius = radius,
_startAngle = startAngle;
double _radius;
set radius(double value) {
if (_radius == value) return;
_radius = value;
markNeedsLayout();
}
double _startAngle;
set startAngle(double value) {
if (_startAngle == value) return;
_startAngle = value;
markNeedsLayout();
}
@override
void setupParentData(RenderObject child) {
if (child.parentData is! BoxParentData) {
child.parentData = BoxParentData();
}
}
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
// Calculate center
final centerX = constraints.maxWidth / 2;
final centerY = constraints.maxHeight / 2;
// Layout each child
RenderBox? child = firstChild;
int index = 0;
while (child != null) {
// Layout child with loose constraints
child.layout(
BoxConstraints.loose(constraints),
parentUsesSize: true,
);
// Calculate position on circle
final angle = _startAngle + (index / childCount) * 2 * 3.14159;
final x = centerX + _radius * cos(angle) - child.size.width / 2;
final y = centerY + _radius * sin(angle) - child.size.height / 2;
// Position child
final BoxParentData childParentData =
child.parentData! as BoxParentData;
childParentData.offset = Offset(x, y);
child = childAfter(child);
index++;
}
// Set own size
size = constraints.constrain(const Size(
constraints.maxWidth,
constraints.maxHeight,
));
}
@override
void paint(PaintingContext context, Offset offset) {
// Paint children
RenderBox? child = firstChild;
while (child != null) {
final BoxParentData childParentData =
child.parentData! as BoxParentData;
context.paintChild(child, offset + childParentData.offset);
child = childAfter(child);
}
}
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
return defaultHitTestChildren(result, position: position);
}
}
// 3. Using radial layout
class RadialExample extends StatelessWidget {
const RadialExample({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: 300,
height: 300,
color: Colors.grey[200],
child: RadialLayout(
radius: 100,
startAngle: 0,
children: [
_buildCircleItem(Colors.red, '1'),
_buildCircleItem(Colors.green, '2'),
_buildCircleItem(Colors.blue, '3'),
_buildCircleItem(Colors.orange, '4'),
_buildCircleItem(Colors.purple, '5'),
_buildCircleItem(Colors.pink, '6'),
],
),
);
}
Widget _buildCircleItem(Color color, String label) {
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
child: Center(
child: Text(
label,
style: const TextStyle(color: Colors.white),
),
),
);
}
}
What's happening here? - Items positioned on a circle - Custom positioning algorithm - Trigonometric calculations - Unique visual layout
Custom Layout Example: Waterfall Grid
Creating a waterfall/pinterest grid with custom spacing.
// Waterfall grid widget
class WaterfallGrid extends MultiChildRenderObjectWidget {
const WaterfallGrid({
super.key,
required super.children,
this.crossAxisCount = 2,
this.mainAxisSpacing = 8,
this.crossAxisSpacing = 8,
});
final int crossAxisCount;
final double mainAxisSpacing;
final double crossAxisSpacing;
@override
RenderObject createRenderObject(BuildContext context) {
return WaterfallRenderObject(
crossAxisCount: crossAxisCount,
mainAxisSpacing: mainAxisSpacing,
crossAxisSpacing: crossAxisSpacing,
);
}
@override
void updateRenderObject(
BuildContext context,
WaterfallRenderObject renderObject,
) {
renderObject
..crossAxisCount = crossAxisCount
..mainAxisSpacing = mainAxisSpacing
..crossAxisSpacing = crossAxisSpacing;
}
}
// Using waterfall grid
class WaterfallExample extends StatelessWidget {
const WaterfallExample({super.key});
@override
Widget build(BuildContext context) {
return Container(
height: 400,
color: Colors.grey[200],
child: WaterfallGrid(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: [
_buildWaterfallItem(100, Colors.red),
_buildWaterfallItem(150, Colors.green),
_buildWaterfallItem(80, Colors.blue),
_buildWaterfallItem(120, Colors.orange),
_buildWaterfallItem(200, Colors.purple),
_buildWaterfallItem(90, Colors.pink),
_buildWaterfallItem(160, Colors.teal),
_buildWaterfallItem(110, Colors.indigo),
],
),
);
}
Widget _buildWaterfallItem(double height, Color color) {
return Container(
height: height,
color: color,
child: Center(
child: Text(
'${height.toInt()}px',
style: const TextStyle(color: Colors.white),
),
),
);
}
}
What's happening here? - Variable-height items in grid - Optimal column placement - Custom spacing control - Pinterest-style layout
Best Practices
Use RenderObject Efficiently
// Good - Cache calculations
class CustomRenderObject extends RenderBox {
List<Offset>? _cachedPositions;
@override
void performLayout() {
if (_cachedPositions == null) {
_cachedPositions = _calculatePositions();
}
// Use cached positions
}
}
Minimize Layout Work
// Good - Only relayout when needed
set spacing(double value) {
if (_spacing == value) return;
_spacing = value;
markNeedsLayout(); // Only when value changes
}
Use Mixins for Common Functionality
// Good - Use ContainerRenderObjectMixin
class CustomRenderObject extends RenderBox
with ContainerRenderObjectMixin<RenderBox, BoxParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, BoxParentData> {
// Gets many methods for free
}
Summary
Custom layouts give you complete control over child positioning and sizing. Use MultiChildRenderObjectWidget and RenderObject to implement your own layout algorithms. This enables specialized layouts like masonry, radial, waterfall, and any custom arrangement you can imagine.
Next Steps
Did You Know?
- Custom layouts use RenderObject
- ContainerRenderObjectMixin manages children
- performLayout handles sizing and positioning
- paint handles visual rendering
- markNeedsLayout triggers relayout
- Custom layouts can optimize performance
- Layout algorithms can be complex
- Custom layouts enable unique designs