Layout #
Flutter’s layout system is designed with one very consistent architectural principle that applies absolutely to all interface elements: Constraints go down, sizes go up, parent sets position. Understanding how this fundamental law interacts with widgets like Row, Column, and Stack, and how to handle the fatal unbounded constraints error, is a mandatory skill that separates beginner Flutter developers from professionals. We’ll break down in depth the constraints system, linear layouts, remaining space distribution, and screen overflow troubleshooting techniques often encountered in production apps.
Flutter’s Rules of Layout #
The entire visual layout process in Flutter is governed by three golden rules executed sequentially on the Render Tree:
- Constraints Go Down: The parent widget sends a
BoxConstraintsobject to its child widget. These constraints determine the minimum and maximum width and height values the child widget is allowed to have. - Sizes Go Up: The child widget reads those constraints, then calculates its own size dimensions (
Size) within the allowed boundary range. After its size is determined, the child widget reports that size back to the parent. - Parent Sets Position: After receiving the size report from the child, the parent is responsible for setting the child’s relative position coordinates on screen using the
Offset(x, y)coordinate. The child widget has absolutely no power to choose its own position on screen.
This layout instruction flow can be visualized through the following flow diagram:
flowchart TD
Parent["Parent Widget"] -->|"1. Send BoxConstraints (min/max)"| Child["Child Widget"]
Child -->|"2. Calculate & Report Size (width/height)"| Parent
Parent -->|"3. Determine Offset Coordinates (x, y)"| ChildTight Constraints vs Loose Constraints #
Flutter divides BoxConstraints into two main behavior categories:
- Tight Constraints: The condition where the minimum and maximum boundary values are set exactly the same. This forces the child widget to have the same size as those constraints, ignoring custom size properties written in the child widget.
Example:
BoxConstraints.tight(Size(300, 300)). If you put aContainer(width: 50)inside it, the Container is still forced to expand to 300x300. - Loose Constraints: The condition where the minimum boundary value is set to zero. This frees the child widget to determine its own size dynamically, as long as its size doesn’t exceed the allowed maximum boundary.
Example:
BoxConstraints(minWidth: 0, maxWidth: 300).
Column and Row — Flexible Linear Layouts #
Column (vertical) and Row (horizontal) are the two most frequently used linear layout widgets in Flutter. Both are derived from the same base class Flex.
Every flexible layout has two main axes determining the flow direction and alignment of child widgets:
Row (Horizontal Flow):
Main Axis: Horizontal (Left to Right)
Cross Axis: Vertical (Top to Bottom)
Column (Vertical Flow):
Main Axis: Vertical (Top to Bottom)
Cross Axis: Horizontal (Left to Right)
1. MainAxisAlignment (Main Axis Distribution) #
Used to arrange the spread and spacing between child widgets along the main axis.
MainAxisAlignment.start: Clusters all child widgets at the beginning of the axis.MainAxisAlignment.end: Clusters all child widgets at the end of the axis.MainAxisAlignment.center: Groups child widgets exactly in the middle.MainAxisAlignment.spaceBetween: Divides empty space evenly between child widgets, making the first and last widgets hug the container edges tightly.MainAxisAlignment.spaceAround: Divides empty space evenly, where the empty space at the start and end edges is half the space between widgets.MainAxisAlignment.spaceEvenly: Divides all empty space equally across all gaps, including at the start and end edges.
2. CrossAxisAlignment (Cross Axis Alignment) #
Used to position child widgets perpendicular to the main axis flow.
CrossAxisAlignment.start: Aligns child widgets at the start edge of the cross axis.CrossAxisAlignment.end: Aligns child widgets at the end edge of the cross axis.CrossAxisAlignment.center: Aligns child widgets exactly in the middle of the cross axis (static default).CrossAxisAlignment.stretch: Forces all child widgets to stretch fully to fill the cross axis width/height.
3. MainAxisSize (Main Axis Size) #
By default, Row and Column try to take up as much empty space as possible along the main axis (MainAxisSize.max). If you want the container to shrink to fit the total size of all child widgets inside, change the setting to MainAxisSize.min (similar to wrap-content behavior).
Expanded, Flexible, and Spacer — Remaining Space Distribution #
When arranging elements inside a Row or Column, there’s often leftover empty space on the main axis. You can control how child widgets divide and fill that remaining space using three special widgets:
Expanded #
Expanded forces its child widget to fill all the available remaining space on the main axis. This widget implicitly sets FlexFit.tight behavior.
Row(
children: [
const Icon(Icons.star), // Fixed size (24px)
Expanded(
child: Container(color: Colors.blue), // Takes ALL remaining screen width
),
],
)
If you have several Expanded widgets in one row, you can use the flex property to divide the remaining space proportionally:
Row(
children: [
Expanded(
flex: 1, // Takes 1/3 of the remaining space
child: Container(color: Colors.red),
),
Expanded(
flex: 2, // Takes 2/3 of the remaining space (twice as wide)
child: Container(color: Colors.blue),
),
],
)
Flexible #
Unlike Expanded, Flexible gives the child widget freedom to fill the remaining space, but doesn’t force the child to fill it if the child’s intrinsic size is smaller (FlexFit.loose).
Row(
children: [
Flexible(
child: Container(
width: 50.0, // Even though there's 200px remaining, the Container stays at 50px
color: Colors.green,
),
),
],
)
Spacer #
Spacer is an empty non-visual widget acting as a pusher. Under the hood, Spacer is just shorthand for Expanded(child: SizedBox.shrink()).
Row(
children: [
const Text('Left Menu'),
const Spacer(), // Pushes 'Right Menu' to the far right edge of the screen
const Text('Right Menu'),
],
)
Stack — Managing Overlapping Elements #
Stack is used to layer multiple child widgets on top of each other (based on the Z-index axis). Elements written first in the children list are placed on the bottom layer, while elements written last are stacked on the top layer.
Stack(
alignment: Alignment.bottomRight, // Alignment for non-positioned children
children: [
// Layer 1: Bottom
Image.network('https://example.com/card_bg.png'),
// Layer 2: Acts as a dark overlay
Positioned.fill(
child: Container(color: Colors.black.withOpacity(0.4)),
),
// Layer 3: Topmost with absolute position
const Positioned(
top: 16.0,
left: 16.0,
child: Text('Premium Card', style: TextStyle(color: Colors.white)),
),
],
)
Stack Sizing (fit) #
The fit property on Stack determines how the parent’s size constraints are forwarded to children not marked with Positioned:
StackFit.loose: The Stack follows the size of the largest non-positioned child.StackFit.expand: The Stack is forced to stretch fully following the parent’s maximum constraints, forcing all non-positioned children to fill the Stack’s size too.
Handling the Fatal Error: Unbounded Constraints #
The most common fatal errors encountered by beginner Flutter developers in the debugging console are:
A RenderFlex overflowed by X pixels on the bottom/right.(Dashed yellow-black lines on screen).Vertical viewport was given unbounded height.(App crashes immediately with a white/red screen).
This problem occurs because of a collision of Unbounded Constraints. This is the condition where a widget requests unlimited space, placed inside a container that also doesn’t limit its size.
Let’s study the two classic error scenarios and their fixes:
Case 1: Putting a ListView inside a Column #
By default, a vertical ListView tries to take as much height as possible (greedy height). Meanwhile, a Column also doesn’t limit its children’s maximum height. Combining them without protection triggers an immediate crash.
// ANTI-PATTERN: ListView inside Column immediately triggers an Unbounded Height Crash!
Widget build(BuildContext context) {
return Column(
children: [
const Text('Product List:'),
ListView.builder(
itemCount: 5,
itemBuilder: (context, index) => Text('Product $index'),
),
],
);
}
Solution 1: Wrapping with Expanded #
Wrap the ListView with Expanded so the Column limits the ListView’s height to only the safely available remaining screen space.
// CORRECT: Limiting height using Expanded
Widget build(BuildContext context) {
return Column(
children: [
const Text('Product List:'),
Expanded(
child: ListView.builder(
itemCount: 5,
itemBuilder: (context, index) => Text('Product $index'),
),
),
],
);
}
Solution 2: Using shrinkWrap and NeverScrollableScrollPhysics #
If you want the ListView to shrink its size to only the height of its elements (so it can scroll together with the main Column), enable the shrinkWrap property and disable its internal scroll function:
// CORRECT: Using shrinkWrap to shrink the ListView height
Widget build(BuildContext context) {
return Column(
children: [
const Text('Product List:'),
ListView.builder(
shrinkWrap: true, // Instructs the ListView to only take its elements' total height
physics: const NeverScrollableScrollPhysics(), // Disables the ListView's internal scrolling
itemCount: 5,
itemBuilder: (context, index) => Text('Product $index'),
),
],
);
}
Advanced Dimension Control Widgets #
For precise layout design, Flutter provides several advanced size control widgets:
1. ConstrainedBox #
Used when you want to inject additional custom size constraints on top of the constraints received from the parent.
ConstrainedBox(
// Forces the button to have a minimum width of 200px even if the text inside is short
constraints: const BoxConstraints(
minWidth: 200.0,
maxWidth: 300.0,
minHeight: 48.0,
),
child: ElevatedButton(
onPressed: () {},
child: const Text('Submit'),
),
)
2. IntrinsicHeight & IntrinsicWidth #
Classic scenario: you create a Row containing several Containers with texts of different lengths. You want all Containers in that row to have the same height, following the Container with the longest text.
// CORRECT: Dynamically equalizing element heights using IntrinsicHeight
IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch, // Stretches children's height following the container
children: [
Container(color: Colors.red, child: const Text('Short')),
Container(color: Colors.blue, child: const Text('Text\nIs\nVery\nLong')),
Container(color: Colors.green, child: const Text('Medium')), // Follows the tallest height
],
),
)
Performance Warning: UseIntrinsicHeightandIntrinsicWidthonly if there’s no other alternative solution. Both widgets are very expensive because they force Flutter to do a two-pass layout process: first measuring children’s intrinsic heights, and second physically rendering. Using too many of these widgets will immediately trigger performance degradation (frame drops).
Responsive Layout: LayoutBuilder vs MediaQuery #
When building apps that run on various screen sizes (smartphones, tablets, desktop), you need a mechanism to respond to display space size changes dynamically. Flutter provides two main tools for this need: MediaQuery and LayoutBuilder. Both have different purposes and working methods:
1. MediaQuery (Global Screen Information) #
MediaQuery is used to get the overall physical device screen size information (viewport window) and screen orientation.
- Characteristics: Based on a global inherited widget. App window size changes (e.g., when resizing the window on desktop/web or rotating the screen) trigger a rebuild of all widgets below the context using it.
- Ideal Use: Determining macro layouts (e.g., if the screen width is > 600px use two columns, otherwise use one column).
Widget build(BuildContext context) {
final double screenWidth = MediaQuery.of(context).size.width;
if (screenWidth > 600) {
return const WideLayoutWidget(); // Tablet/desktop display
} else {
return const NarrowLayoutWidget(); // Phone display
}
}
2. LayoutBuilder (Local Constraint Information) #
Unlike MediaQuery, which looks at the screen size globally, LayoutBuilder measures the size constraints passed down by the parent to that widget specifically at its position in the widget tree.
- Characteristics: Has a builder callback returning a
BoxConstraintsobject. This lets you adjust child layouts based on the real space available to that widget itself, not the overall screen size. - Ideal Use: Micro component design (e.g., a card widget displaying an image on the left if it’s wide enough, but on top if space is narrow).
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
if (constraints.maxWidth > 400) {
return Row(
children: [
const Icon(Icons.thumbnail),
Expanded(child: Text('Detail Content')),
],
);
} else {
return Column(
children: [
const Icon(Icons.thumbnail),
Text('Detail Content'),
],
);
}
},
);
}
Custom Layout with CustomMultiChildLayout #
For very complex layout scenarios where the child arrangement can’t be achieved using only combinations of Row, Column, or Stack, Flutter provides CustomMultiChildLayout. This widget gives you full control to programmatically set each child’s position and size using a custom layout delegate (MultiChildLayoutDelegate).
Using CustomMultiChildLayout, you must do two main steps:
- Identify each child with a unique ID using the
LayoutIdwidget. - Create a subclass of
MultiChildLayoutDelegateand implement theperformLayoutandshouldRelayoutmethods.
Here’s an example of a custom layout delegate implementation that positions the second child widget below the first child with exactly the same width:
class UnderneathLayoutDelegate extends MultiChildLayoutDelegate {
@override
void performLayout(Size size) {
// 1. Check whether both children with the expected IDs exist in the layout tree
if (hasChild('header') && hasChild('body')) {
// 2. Determine the header size by passing down constraints from the parent
final Size headerSize = layoutChild(
'header',
BoxConstraints.loose(size),
);
// 3. Position the header at coordinates (0, 0)
positionChild('header', Offset.zero);
// 4. Constrain the body width to match the header, with height adjusting to remaining space
final Size bodySize = layoutChild(
'body',
BoxConstraints(
minWidth: headerSize.width,
maxWidth: headerSize.width,
minHeight: 0,
maxHeight: size.height - headerSize.height,
),
);
// 5. Position the body exactly below the header
positionChild('body', Offset(0, headerSize.height));
}
}
@override
bool shouldRelayout(covariant UnderneathLayoutDelegate oldDelegate) {
return false;
}
}
Using this custom delegate in your widget tree is very simple:
CustomMultiChildLayout(
delegate: UnderneathLayoutDelegate(),
children: [
LayoutId(
id: 'header',
child: Container(
color: Colors.teal,
padding: const EdgeInsets.all(16.0),
child: const Text('Custom Header'),
),
),
LayoutId(
id: 'body',
child: Container(
color: Colors.grey[200],
padding: const EdgeInsets.all(16.0),
child: const Text('Main content placed exactly below the header with the same width.'),
),
),
],
)
Although CustomMultiChildLayout requires a bit more boilerplate code, it’s much more efficient in terms of performance than stacking unnecessary nested widgets, because all position and size calculations are completed in a single layout cycle.
Summary #
- The Main Layout Law: Always remember the principle Constraints go down, sizes go up, parent sets position. Constraints flow down, sizes are reported up, position coordinates are set by the parent.
- Flex Layout: Row and Column manage the Main Axis and Cross Axis flows. Use
MainAxisSize.minto shrink linear container sizes.- Expanded vs Flexible:
Expandedforces children to fill all remaining space (FlexFit.tight), whileFlexibleallows children to be smaller than the remaining space (FlexFit.loose).- Stack & Positioned: Arranges widgets in stacked layers based on the Z-index axis. Use
Positionedfor absolute coordinate placement.- Unbounded Constraints: A constraint collision occurring when placing greedy widgets inside an unbounded container. Solve it using
Expandedor theshrinkWrap: trueproperty.- Intrinsic Widgets: Useful for dynamically equalizing child height/width sizes, but with a fairly expensive performance cost (two-pass layout).