Flutter Layout — Complete Guide
1. The Core Mental Model
Flutter layout runs on one rule, repeated at every level of the widget tree:
Constraints go down. Sizes go up. Position is set by the parent.
- A parent passes constraints (min/max width, min/max height) down to its child.
- The child looks at those constraints, decides its own size (within them), and passes that size back up.
- The parent then decides where to place the child (its position), based on its own layout logic (Row, Column, Stack, etc.).
- A widget cannot know or decide its own position — only its size, and only within the constraints given.
- A parent cannot just tell a child "be this exact size" without constraints — it does so by passing tight constraints.
This single-pass, top-down/bottom-up system is why Flutter layout is fast (no repeated relayout passes like some browser engines) but also why certain patterns (like "make me the same size as my sibling") require special widgets to solve.
Constraints (BoxConstraints)
BoxConstraints(
minWidth: 0, maxWidth: 300,
minHeight: 0, maxHeight: 150,
)
-
Tight constraints: min == max (e.g.,
BoxConstraints.tight(Size(100,100))). The child has no choice — it must be that exact size. - Loose constraints: min is 0, max is some value. The child can be anywhere from 0 up to that max.
-
Unbounded constraints: max is
double.infinity. This is where the infamous "RenderBox was not laid out" or "BoxConstraints forces an infinite width" errors come from — usually caused by putting an unconstrained widget (likeListVieworColumn) inside another unconstrained widget (like inside aRowwithoutExpanded, or inside a scrollable without a fixed height).
2. Everything Is a Widget
Widgets are configuration — immutable blueprints. Three widget trees exist under the hood:
- Widget tree — what you write (immutable, cheap to rebuild).
- Element tree — the "live" instantiation, persists across rebuilds, manages state.
- RenderObject tree — does the actual layout, painting, and hit-testing math.
For layout purposes, what matters is the RenderObject tree — every widget that affects layout ultimately creates or configures a RenderBox that participates in the constraints-down/sizes-up protocol.
3. Single-Child Layout Widgets
These take one child and adjust size/position/constraints.
Container
The Swiss-army-knife. Combines padding, margin, alignment, decoration, and constraints.
Container(
width: 200,
height: 100,
padding: EdgeInsets.all(16),
margin: EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
),
child: Text('Hello'),
)
Behavior depends on what's set:
- If
width/heightgiven → tightens constraints to that size (if the parent allows). - If no size given but has a child → sizes to fit the child (adds padding).
- If no size and no child → tries to expand to fill available space.
Padding
Adds space around a child, shrinking the constraints passed down by the insets.
Padding(padding: EdgeInsets.all(16), child: Text('Hi'))
Center / Align
Align positions a child within itself according to Alignment (e.g., Alignment.topRight), and by default sizes itself to fill available space. Center is just Align with Alignment.center.
Align(alignment: Alignment.bottomRight, child: Icon(Icons.star))
SizedBox
Forces exact width/height (tight constraints), or use SizedBox.expand() / SizedBox.shrink(). Also commonly used as a spacer:
SizedBox(height: 20) // vertical gap between widgets
ConstrainedBox / UnconstrainedBox
ConstrainedBox adds additional constraints (it intersects with what the parent gives, doesn't override).
ConstrainedBox(
constraints: BoxConstraints(minWidth: 100, maxWidth: 300),
child: Text('Flexible width text'),
)
FittedBox
Scales/fits a child within itself using BoxFit (cover, contain, fill, etc.) — useful for text or images that must shrink to fit.
AspectRatio
Forces the child into a specific width/height ratio, respecting incoming constraints as much as possible.
IntrinsicWidth / IntrinsicHeight
Sizes a child based on its "intrinsic" (natural/preferred) dimensions rather than the parent's constraints. Expensive (extra layout passes) — use sparingly, e.g., making buttons in a Row all match the height of the tallest one.
Transform
Applies a matrix transform (rotate, scale, skew, translate) at paint time — does not affect layout/constraints, only how it's drawn (so it can visually overflow its bounds without affecting siblings).
4. Multi-Child Layout Widgets
Row & Column (Flex)
Both are Flex under the hood — Row lays out horizontally, Column vertically.
- Main axis: the direction of layout (horizontal for Row, vertical for Column).
- Cross axis: perpendicular to that.
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [Widget1(), Widget2(), Widget3()],
)
MainAxisAlignment (spacing along main axis):
start, end, center, spaceBetween, spaceAround, spaceEvenly
CrossAxisAlignment (alignment along cross axis):
start, end, center, stretch, baseline
MainAxisSize:
-
max(default) — Row/Column takes all available space on the main axis. -
min— shrinks to fit just its children.
Key gotcha: A Row has unbounded width if placed inside something that gives infinite width (like a horizontally scrolling parent without explicit sizes) — this causes overflow errors. Non-flexible children in a Row/Column must fit within the available space, or you get the classic yellow-and-black striped overflow warning.
Expanded & Flexible
Used only inside Row/Column/Flex to control how children share leftover space along the main axis.
Row(
children: [
Expanded(flex: 2, child: Container(color: Colors.red)),
Expanded(flex: 1, child: Container(color: Colors.blue)),
],
)
-
Expanded=FlexiblewithFlexFit.tight— the child is forced to fill its share of space exactly. -
FlexiblewithFlexFit.loose(default) — the child can be up to its share of space, but isn't forced to fill it. -
flex— the relative weight (default 1) determining how leftover space is divided among flexible children.
Without Expanded/Flexible, children in a Row/Column take only their natural (intrinsic) size — if that combined size exceeds available space, you get an overflow.
Stack & Positioned
Layers children on top of each other (z-axis), like absolute positioning in CSS.
Stack(
alignment: Alignment.center,
fit: StackFit.loose,
children: [
Container(color: Colors.grey), // sized by non-positioned children or Stack itself
Positioned(top: 10, right: 10, child: Icon(Icons.close)),
Positioned.fill(child: Center(child: Text('Overlay'))),
],
)
- Children without
Positionedare laid out first ("non-positioned"), sized according tofit(loose= as small as possible,expand= fill the Stack) and placed peralignment. -
Positionedchildren are placed usingtop/bottom/left/right/width/heightrelative to the Stack's own bounds — these are not in the constraints-down flow the same way; the Stack determines its own size from non-positioned children (or fills available space if given tight constraints), then positions thePositionedones absolutely within that.
Wrap
Like Row/Column, but overflows onto new lines/columns when out of space, instead of clipping/erroring.
Wrap(
spacing: 8, runSpacing: 4,
children: List.generate(10, (i) => Chip(label: Text('Item $i'))),
)
ListView, GridView, and other scrollables
These are unbounded on the scroll axis internally — they build children lazily (ListView.builder) and give each child unbounded space along the scroll direction but bounded on the cross axis. This is why putting a ListView inside a Column without wrapping it in Expanded or giving it a fixed height throws a layout error (a Column gives unbounded height to its children by default when it's inside something scrollable, and ListView also wants unbounded height — two unbounded things fighting).
Column(
children: [
Text('Header'),
Expanded(child: ListView(children: [...])), // now ListView gets bounded height
],
)
GridView works similarly but lays children in a 2D grid — controlled via gridDelegate:
GridView.count(crossAxisCount: 3, children: [...])
// or
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemBuilder: (context, i) => Card(),
itemCount: 20,
)
CustomMultiChildLayout / CustomSingleChildLayout
Escape hatches for fully custom layout logic — you implement a LayoutDelegate/SingleChildLayoutDelegate and manually compute constraints/positions for each labeled child. Rarely needed unless building complex custom widgets.
5. Slivers (for scroll-heavy layouts)
CustomScrollView + slivers give fine control over scroll behavior — collapsing app bars, mixed lists/grids, sticky headers.
CustomScrollView(
slivers: [
SliverAppBar(expandedHeight: 200, pinned: true, flexibleSpace: FlexibleSpaceBar(title: Text('Title'))),
SliverList(delegate: SliverChildBuilderDelegate((c, i) => ListTile(title: Text('$i')))),
SliverGrid(gridDelegate: ..., delegate: ...),
],
)
Slivers use a different protocol than boxes (SliverConstraints/SliverGeometry instead of BoxConstraints/Size), because they need to describe how much of themselves is currently visible within a scrolling viewport, not just a fixed size.
6. Responsive & Adaptive Layout
MediaQuery
Gives you the size/properties of the whole screen (or nearest ancestor MediaQuery):
final size = MediaQuery.of(context).size;
final isTablet = size.width > 600;
Rebuilds the widget whenever the queried value changes (e.g., rotation), so avoid over-querying deep in performance-critical trees.
LayoutBuilder
Gives you the incoming constraints at that point in the tree (more precise than MediaQuery, which is screen-wide) — essential for building widgets that adapt to their parent's size, not the whole screen:
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 600) {
return WideLayout();
}
return NarrowLayout();
},
)
OrientationBuilder
Rebuilds based on portrait/landscape orientation specifically.
FractionallySizedBox
Sizes a child as a fraction of the parent's available space:
FractionallySizedBox(widthFactor: 0.8, child: Text('80% width'))
7. Text Layout Specifics
Text has its own layout quirks:
-
Textwraps automatically within its constrained width;overflow: TextOverflow.ellipsiscombined withmaxLinesprevents overflow errors. -
softWrap,textAlign,textScaleFactor(deprecated in favor ofTextScaler) all affect how much space text actually needs. - Wrapping
TextinExpandedorFlexibleinside a Row is often required, otherwise a long string can force the Row to overflow horizontally.
8. Common Errors & How to Read Them
| Error | Typical Cause | Fix |
|---|---|---|
| "RenderFlex overflowed by X pixels" | Children's natural size exceeds Row/Column space | Wrap overflowing child in Expanded/Flexible, or use Wrap, or scroll |
| "BoxConstraints forces an infinite width/height" | Unbounded widget (ListView, Column) inside another unbounded context | Give explicit size, or wrap in Expanded/SizedBox/ConstrainedBox
|
| "Vertical viewport was given unbounded height" |
ListView/Column with scrollable inside another scrollable/Column without constraints |
Wrap the scrollable in Expanded or set shrinkWrap: true (careful: perf cost) |
| "A RenderFlex overflowed" only in debug (yellow/black stripes) | Same as above — it's a visual warning, not a crash | Same fixes |
Debugging tools:
-
debugPaintSizeEnabled = true— visualizes all box boundaries. - Flutter DevTools Layout Explorer — inspect constraints/flex factors visually per widget.
-
Widget Inspector— click any widget on screen to see its render tree info.
9. Performance Notes
- Layout is generally a single pass per frame (parent asks child to lay out once) — but widgets like
IntrinsicWidth/IntrinsicHeightforce extra passes and are more expensive. - Rebuilds (
setState) don't necessarily mean relayout — Flutter only recomputes layout/paint for subtrees whose constraints or geometry actually changed (RenderObject.markNeedsLayoutis only called when necessary). - Keys (
ValueKey,GlobalKey) matter for preserving state and identity across rebuilds when list order or widget type changes, but they don't directly affect the layout algorithm itself.
10. Quick Reference Cheat Sheet
-
Center something:
CenterorAlign -
Fixed size:
SizedBox -
Extra space around:
Padding -
Share space along a Row/Column:
Expanded/Flexible -
Overlap widgets:
Stack+Positioned -
Wrap onto new lines:
Wrap -
Scrollable list:
ListView.builder -
Adapt to parent size:
LayoutBuilder -
Adapt to screen size:
MediaQuery -
Percentage sizing:
FractionallySizedBox -
Force aspect ratio:
AspectRatio -
Fit content to box (image/text):
FittedBox
Top comments (0)