Layout System, Constraints & Flex
Constraints Go Down, Sizes Go Up — Master This or Fight Flutter Forever
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Flutter's layout system sends BoxConstraints down the tree and receives Sizes back up. Parents then position children with Offsets. Row and Column use a flex algorithm to distribute space. Understanding tight vs loose vs unbounded constraints explains every layout bug you'll encounter. LayoutBuilder enables constraint-aware responsive layouts.
Real-world relevance
In an offline-first field app, a job details screen uses a Column with the job header (fixed height), an Expanded ListView of task items (fills remaining space), and a fixed bottom action bar. LayoutBuilder on the action bar adapts between a single-column button layout (phone) and a row of buttons (tablet). This avoids RenderFlex overflow on small screens.
Key points
- The Golden Rule: Constraints Down, Sizes Up, Parent Positions — Every layout in Flutter follows this: (1) Parent passes BoxConstraints to child. (2) Child determines its own size within those constraints. (3) Parent decides where to place the child. This is NOT like CSS where children can affect parent size arbitrarily. Understanding this one rule prevents 99% of layout bugs.
- BoxConstraints — The Four Values — BoxConstraints has minWidth, maxWidth, minHeight, maxHeight. 'Tight constraint': min == max (child MUST be exactly this size). 'Loose constraint': min is 0 (child can be smaller). 'Unbounded constraint': max is double.infinity (child picks its own size). Unbounded height in a Column causes 'RenderFlex overflowed' errors.
- Expanded vs Flexible — Expanded: takes ALL remaining space in the main axis (flex: 1 by default). Equivalent to Flexible(fit: FlexFit.tight). Flexible: takes UP TO its allotted space but can be smaller (fit: FlexFit.loose). Interview: What's the difference between Expanded and Flexible? Expanded forces max size, Flexible allows smaller.
- flex Parameter — In a Row or Column, Expanded(flex: 2) gets twice the space of Expanded(flex: 1). Remaining space after non-flex children are laid out is divided proportionally by flex values. Interview: You have a Row with three children. Two are flex:1 and one is flex:2. How is space distributed? 25%, 25%, 50%.
- Unbounded Constraints — The Common Crash — ListView inside a Column without a height — Column gives ListView infinite height AND ListView gives its children infinite height → 'Vertical viewport was given unbounded height'. Fix: wrap ListView in Expanded (inside Column) or give it a fixed height with SizedBox. Never put ListView directly in Column without a bounded parent.
- LayoutBuilder — Constraints-Aware Widgets — LayoutBuilder exposes the parent's BoxConstraints to the builder function. Use it for responsive layouts: if constraints.maxWidth > 600 show desktop layout, else mobile. Interview: How do you make a widget responsive? Use LayoutBuilder and branch on constraints.
- Intrinsic Sizing — Use Sparingly — IntrinsicHeight and IntrinsicWidth force a double layout pass — expensive. They measure children at their natural size first, then constrain all children to the largest. Use only when needed. For most cases, CrossAxisAlignment.stretch is faster and achieves similar results.
- RenderFlex Overflowed — Diagnosis — 'A RenderFlex overflowed by X pixels' means a Row/Column child is wider/taller than the available space. Diagnosis: (1) Wrap the overflowing child in Expanded/Flexible. (2) Wrap in SingleChildScrollView. (3) Use Overflow.clip or FittedBox. Interview: How do you debug layout overflow in Flutter?
- CustomMultiChildLayout — Precise Control — For pixel-perfect custom layouts, CustomMultiChildLayout and MultiChildLayoutDelegate give you full control over each child's constraints and position. Used in complex dashboards, custom card layouts, and game UIs. Each child is identified by a layoutId.
- SizedBox, ConstrainedBox, FractionallySizedBox — SizedBox(width: 200): tight constraint to 200px. ConstrainedBox(constraints: BoxConstraints(maxWidth: 300)): adds constraints on top of parent's. FractionallySizedBox(widthFactor: 0.5): 50% of parent width. These are the layout primitives for controlling child sizes.
Code example
// Flutter Layout System, Constraints & Flex
import 'package:flutter/material.dart';
// --- 1. CONSTRAINTS DOWN, SIZES UP demo ---
class ConstraintDemo extends StatelessWidget {
const ConstraintDemo({super.key});
@override
Widget build(BuildContext context) {
return LayoutBuilder( // Exposes parent's constraints
builder: (context, constraints) {
debugPrint('Available: ${constraints.maxWidth}x${constraints.maxHeight}');
// Branch on available width — responsive layout
if (constraints.maxWidth > 600) {
return const _DesktopLayout();
}
return const _MobileLayout();
},
);
}
}
// --- 2. Expanded vs Flexible ---
class FlexDemo extends StatelessWidget {
const FlexDemo({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// ROW: non-flex child first, then flex children share remaining space
Row(
children: [
// Non-flex: takes its natural size (64px icon)
const Icon(Icons.account_circle, size: 64),
// Expanded: takes ALL remaining space after the icon
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('John Doe', style: TextStyle(fontWeight: FontWeight.bold)),
Text('Senior Engineer'),
],
),
),
// Expanded(flex:1): gets half as much as flex:2
Expanded(
flex: 1,
child: ElevatedButton(
onPressed: () {},
child: const Text('Edit'),
),
),
],
),
const SizedBox(height: 16),
// Flexible vs Expanded difference
Row(
children: [
// Expanded: MUST fill its share (200px if total is 400px)
Expanded(
child: Container(color: Colors.blue, height: 50),
),
// Flexible: CAN be smaller than its share
Flexible(
child: Container(
color: Colors.green,
height: 50,
width: 80, // Takes only 80px even if more is available
),
),
],
),
],
);
}
}
// --- 3. FIX: ListView inside Column (common crash) ---
class JobDetailScreen extends StatelessWidget {
const JobDetailScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
// Fixed header
const _JobHeader(),
// BAD: ListView in Column → "Vertical viewport unbounded height"
// ListView(children: [...]),
// GOOD: Wrap in Expanded — Column gives ListView its remaining space
Expanded(
child: ListView.builder(
itemCount: 20,
itemBuilder: (context, index) => ListTile(
title: Text('Task #$index'),
),
),
),
// Fixed bottom bar
const _ActionBar(),
],
),
);
}
}
// --- 4. ConstrainedBox, SizedBox, FractionallySizedBox ---
class SizingDemo extends StatelessWidget {
const SizingDemo({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// Tight: exactly 200x50
SizedBox(
width: 200,
height: 50,
child: ElevatedButton(onPressed: () {}, child: const Text('Login')),
),
// At most 300px wide
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 300),
child: const TextField(),
),
// 80% of parent width
FractionallySizedBox(
widthFactor: 0.8,
child: ElevatedButton(onPressed: () {}, child: const Text('Full Action')),
),
],
);
}
}
// --- 5. CustomMultiChildLayout ---
class BadgeOverlay extends StatelessWidget {
final Widget child;
final Widget badge;
const BadgeOverlay({super.key, required this.child, required this.badge});
@override
Widget build(BuildContext context) {
return CustomMultiChildLayout(
delegate: _BadgeDelegate(),
children: [
LayoutId(id: 'child', child: child),
LayoutId(id: 'badge', child: badge),
],
);
}
}
class _BadgeDelegate extends MultiChildLayoutDelegate {
@override
void performLayout(Size size) {
final childSize = layoutChild('child', BoxConstraints.loose(size));
positionChild('child', Offset.zero);
final badgeSize = layoutChild(
'badge', BoxConstraints.loose(Size(size.width / 3, size.height / 3)));
positionChild(
'badge',
Offset(childSize.width - badgeSize.width,
childSize.height - badgeSize.height));
}
@override
bool shouldRelayout(_BadgeDelegate oldDelegate) => false;
}
class _JobHeader extends StatelessWidget {
const _JobHeader();
@override
Widget build(BuildContext context) =>
Container(height: 120, color: Colors.blueGrey, child: const Center(child: Text('Job Header')));
}
class _ActionBar extends StatelessWidget {
const _ActionBar();
@override
Widget build(BuildContext context) =>
Container(height: 64, color: Colors.blue, child: const Center(child: Text('Actions')));
}
class _DesktopLayout extends StatelessWidget {
const _DesktopLayout();
@override
Widget build(BuildContext context) => const Row(children: [Text('Desktop')]);
}
class _MobileLayout extends StatelessWidget {
const _MobileLayout();
@override
Widget build(BuildContext context) => const Column(children: [Text('Mobile')]);
}Line-by-line walkthrough
- 1. LayoutBuilder exposes parent BoxConstraints — used for responsive branching
- 2. constraints.maxWidth > 600 → desktop layout, else mobile
- 3. Row with Icon (natural size) + Expanded(flex:2) + Expanded(flex:1) — 0%, 67%, 33% of remaining space
- 4. flex:2 child gets twice the space of flex:1 child
- 5. Flexible with explicit width — takes only 80px even if more is available
- 6. Column + Expanded wrapping ListView — Column's remaining space given to ListView
- 7. SizedBox creates tight constraints — child MUST be exactly that size
- 8. ConstrainedBox adds maxWidth — child can be smaller but not larger
- 9. FractionallySizedBox uses parent's width as reference — 80% of parent
- 10. CustomMultiChildLayout: layoutChild gives constraints, positionChild places result
Spot the bug
Widget build(BuildContext context) {
return Row(
children: [
Column(
children: [
Text('Name'),
Text('email@example.com'),
],
),
ElevatedButton(onPressed: () {}, child: Text('Edit')),
],
);
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Flutter Layout (Flutter Official)
- Understanding Constraints (Flutter Official)
- BoxConstraints (Flutter API)
- Decoding Flutter: Unbounded Height (Flutter YouTube)