Flutter Layouts I: Row & Column
This is Part 5 of the Flutter Fundamentals series. Now we make widgets sit where we want them. The vast majority of every Flutter screen is built from two widgets — Row and Column — plus a handful of helpers that control how children share space. Master these and you can lay out almost anything.
This part covers flex layout (arranging children in a line). Part 6 covers overlapping with Stack and the underlying constraints model that explains why layouts behave as they do. Here we focus on the practical 90%.
Row and Column: children in a line
Rowarranges its children horizontally (left to right).Columnarranges its children vertically (top to bottom).
They're the same widget conceptually (both are "flex" layouts) — just rotated 90°.
Column(
children: const [
Text('First'),
Text('Second'),
Text('Third'),
],
)
Row(
children: const [
Icon(Icons.star),
Text('Rating'),
Icon(Icons.star),
],
)
Everything else about flex layout is just how the children are aligned and how they share the leftover space. And that hinges on one concept: axes.
The most important idea: main axis vs cross axis
Every Row/Column has two axes:
- Main axis — the direction children are laid out along.
- Cross axis — perpendicular to it.
ROW COLUMN
main axis ───────────► main axis
[A] [B] [C] │
▲ ▼
cross axis (vertical) [A] ◄─ cross axis (horizontal)
[B]
[C]
| | Main axis | Cross axis | | --- | --- | --- | | Row | horizontal | vertical | | Column | vertical | horizontal |
Why this matters: the two alignment properties you'll use constantly are named after these axes, and beginners mix them up because they forget which axis is which for a Column. Burn this in: for a Column, the main axis is vertical. So mainAxisAlignment on a Column moves children up/down; crossAxisAlignment moves them left/right. For a Row, it's the opposite.
mainAxisAlignment: spacing along the main axis
mainAxisAlignment controls how children are positioned along the main axis (and how leftover space is distributed):
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [Icon(Icons.home), Icon(Icons.search), Icon(Icons.person)],
)
The options, visualized for a Row (| = edges, ▪ = children):
start | ▪▪▪ |
end | ▪▪▪ |
center | ▪▪▪ |
spaceBetween | ▪ ▪ ▪ | (no space at the ends)
spaceAround | ▪ ▪ ▪ | (half-space at the ends)
spaceEvenly | ▪ ▪ ▪ | (equal space everywhere)
start/end/center— pack children together at one position.spaceBetween— equal gaps between children, none at the ends. (Great for a top bar.)spaceAround— equal gaps around each child (half-gaps at the ends).spaceEvenly— equal gaps everywhere, including the ends.
crossAxisAlignment: alignment along the cross axis
crossAxisAlignment controls positioning perpendicular to the main axis:
Column(
crossAxisAlignment: CrossAxisAlignment.start, // left-align the children
children: const [Text('Title'), Text('A longer subtitle here')],
)
Common values: start, center, end, and stretch (forces children to fill the cross axis). stretch is how you make every child in a Column span the full width:
Column(
crossAxisAlignment: CrossAxisAlignment.stretch, // children fill the width
children: [
Container(color: Colors.red, height: 40),
Container(color: Colors.blue, height: 40),
],
)
mainAxisSize: how big should the flex itself be?
By default, a Row/Column tries to be as big as possible along its main axis (MainAxisSize.max) — a Row takes the full available width, a Column the full available height. Set MainAxisSize.min to make it shrink to just wrap its children:
Column(
mainAxisSize: MainAxisSize.min, // only as tall as its children need
children: const [Text('compact'), Text('column')],
)
This matters for
mainAxisAlignment: properties likecenter/spaceBetweenonly have a visible effect if the flex is bigger than its children (i.e. there's leftover space to distribute). WithMainAxisSize.minthere's no extra space, so they do nothing.
Sharing leftover space: Expanded, Flexible, Spacer
Here's the part that makes flex layouts powerful. What if you want a child to grow and fill the remaining room? Wrap it in Expanded.
Row(
children: [
const Icon(Icons.menu), // takes its natural width
Expanded(child: Container(color: Colors.amber)), // grabs ALL leftover width
const Icon(Icons.more_vert), // natural width
],
)
Expanded tells the child: "ignore your natural size — take all the remaining space along the main axis." With multiple Expanded children, space is divided by their flex factor:
Row(
children: [
Expanded(flex: 2, child: Container(color: Colors.red)), // 2/3 of width
Expanded(flex: 1, child: Container(color: Colors.blue)), // 1/3 of width
],
)
Expanded vs Flexible
They're closely related — in fact Expanded is just Flexible with fit: FlexFit.tight:
| | Expanded | Flexible |
| --- | --- | --- |
| Fit | tight — must fill all its share | loose (default) — fills up to its share |
| Child size | forced to the allotted space | natural size, capped at the allotted space |
| Use when | child should stretch to fill | child should grow only if it needs to |
// Flexible: the text takes only what it needs, but won't overflow —
// it shrinks/wraps within its share instead of forcing full width.
Row(children: [
Flexible(child: Text('A potentially very long piece of text...')),
Icon(Icons.check),
])
Spacer — flexible empty space
Spacer is a shortcut for Expanded(child: SizedBox()) — an invisible flexible gap. Great for pushing things apart:
Row(children: const [
Text('Left'),
Spacer(), // pushes the two apart, filling the middle
Text('Right'),
])
The dreaded overflow (and how to fix it)
You will see this — the yellow-and-black "RenderFlex overflowed by 42 pixels" stripes. It means a Row/Column's children wanted more space along the main axis than was available. Flutter doesn't shrink them automatically; it overflows and warns you.
Classic cause — long text in a Row:
// ❌ If the text is long, it overflows the Row's width.
Row(children: [
Icon(Icons.message),
Text('A very very very very very long message that does not fit'),
])
The fix is to tell the flexible child to take the remaining space and wrap/ellipsize within it — wrap it in Expanded or Flexible:
// ✅ The text now lives within the leftover width and wraps/ellipsizes.
Row(children: [
const Icon(Icons.message),
Expanded(
child: Text(
'A very very very long message that now fits',
overflow: TextOverflow.ellipsis,
),
),
])
Other overflow fixes depending on intent:
- Scrolling content → wrap in a
SingleChildScrollView(or useListView). - Too many items in a Column on small screens → make it scrollable.
- Need wrapping onto multiple lines → use a
Wrapinstead of aRow.
Rule of thumb: overflow in a flex means "a child wants more main-axis space than exists." Decide who should give — usually wrap the greedy child in
Expanded/Flexible, or make the area scrollable.
Putting it together: a list tile
A realistic row combining everything — a leading icon, a flexible two-line text block, and a trailing action:
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.account_circle, size: 40),
const SizedBox(width: 12), // fixed gap
Expanded( // text block takes the remaining width
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('Vivek Kumar', style: TextStyle(fontWeight: FontWeight.bold)),
Text('Tap to view profile', overflow: TextOverflow.ellipsis),
],
),
),
const Icon(Icons.chevron_right),
],
)
Notice the toolkit at work: a Row for the horizontal arrangement, a fixed SizedBox gap, Expanded so the middle flexes, a nested Column (MainAxisSize.min, crossAxisAlignment.start) for the stacked text, and ellipsis to avoid overflow. This pattern is 80% of real UI.
Practice Challenges
Challenge 1 — Axis check. For a Column, which direction does mainAxisAlignment.center move the children, and which does crossAxisAlignment.start move them?
Show solution
For a Column, the main axis is vertical, so mainAxisAlignment.center centers children vertically. The cross axis is horizontal, so crossAxisAlignment.start aligns them to the left (the leading edge).
Challenge 2 — Push apart. Make a Row with "Back" on the far left and "Next" on the far right.
Show solution
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [Text('Back'), Text('Next')],
)
// or: Row(children: const [Text('Back'), Spacer(), Text('Next')])
Challenge 3 — Proportional split. Make a Row of two colored boxes where the first is twice as wide as the second, filling the width.
Show solution
Row(children: [
Expanded(flex: 2, child: Container(color: Colors.red, height: 50)),
Expanded(flex: 1, child: Container(color: Colors.blue, height: 50)),
])
flex: 2 and flex: 1 split the space 2:1.
Challenge 4 — Kill the overflow. This Row overflows on long names. Fix it so the name ellipsizes.
Row(children: [Icon(Icons.person), Text(longName), Icon(Icons.star)])
Show solution
Row(children: [
const Icon(Icons.person),
Expanded(child: Text(longName, overflow: TextOverflow.ellipsis)),
const Icon(Icons.star),
])
Wrapping the text in Expanded confines it to the leftover width, and ellipsis truncates instead of overflowing.
Challenge 5 — Expanded vs Flexible. When would you choose Flexible over Expanded?
Show solution
Use Flexible (loose fit) when the child should take only as much space as it needs, up to its share — but not be forced to fill it. Use Expanded (tight fit) when the child should stretch to fill its entire allotted space regardless of its natural size. E.g. a text label that shouldn't be stretched to full width → Flexible; a background panel that should fill → Expanded.
Questions to test yourself
Q1 (basic). What's the difference between Row and Column?
Show answer
Row lays its children out horizontally (along a horizontal main axis); Column lays them out vertically. They're the same flex concept rotated 90°.
Q2 (basic). For a Row, which axis does mainAxisAlignment control, and which does crossAxisAlignment control?
Show answer
For a Row, mainAxisAlignment controls the horizontal axis (positioning/spacing children left-to-right), and crossAxisAlignment controls the vertical axis. (For a Column it's swapped.)
Q3 (intermediate). What does wrapping a child in Expanded do, and how is space split between multiple Expanded children?
Show answer
Expanded forces the child to fill all remaining space along the main axis (ignoring its natural size). With multiple Expanded children, the leftover space is divided in proportion to their flex factors (e.g. flex: 2 and flex: 1 split it 2:1).
Q4 (intermediate). What's the difference between Expanded and Flexible?
Show answer
Expanded is Flexible with fit: tight — the child must fill its entire allotted share. Flexible (default fit: loose) lets the child take only as much as it needs, capped at its share. Use Expanded to stretch-fill, Flexible to grow-only-if-needed.
Q5 (intermediate). What does a "RenderFlex overflowed" error mean, and name two fixes?
Show answer
It means a Row/Column's children wanted more main-axis space than was available, so Flutter overflowed instead of silently shrinking them. Fixes: wrap the greedy child in Expanded/Flexible (often with TextOverflow.ellipsis), make the area scrollable (SingleChildScrollView/ListView), or use a Wrap to flow onto multiple lines.
Q6 (advanced). Why does mainAxisAlignment: center sometimes appear to do nothing, and how does mainAxisSize relate?
Show answer
mainAxisAlignment only has a visible effect when the flex is larger than its children — i.e. there's leftover main-axis space to distribute. If mainAxisSize is min, the Row/Column shrinks to exactly wrap its children, leaving no extra space, so center/spaceBetween etc. do nothing. To see alignment effects, the flex needs to be bigger than its content (MainAxisSize.max, the default, within a bounded parent).
Wrapping up
Row and Column are the workhorses of Flutter layout:
- They arrange children along a main axis, with a perpendicular cross axis — and the alignment properties are named after those axes (mind the swap between
RowandColumn). mainAxisAlignmentdistributes children along the main axis;crossAxisAlignmentaligns across it (stretchfills it).mainAxisSizedecides whether the flex fills or wraps; alignment needs leftover space to matter.Expanded(tight) stretch-fills the remaining space byflexratio;Flexible(loose) grows only as needed;Spaceris flexible empty space.- Overflow = a child wanted more main-axis space than existed — fix with
Expanded/Flexible, scrolling, orWrap.
But Row/Column only place things side by side. What about overlapping widgets — a badge on an avatar, text over an image? And why does a Container(width: 100) sometimes fill the whole screen? Both answers live in Part 6: Stack, Positioned & the constraints model — the part that finally makes Flutter layout fully click.