Module 5: Layout Widgets
Your Toolkit for Arranging Things on Screen
Every screen you've ever admired was built by someone arranging invisible boxes inside other invisible boxes.
Teach: How Flutter's layout system works through composition -- nesting widgets inside other widgets to create any arrangement. See: Row, Column, Expanded, Container, Stack, and friends building real screen layouts from scratch. Feel: Confidence that you can look at any mobile screen and mentally decompose it into Flutter layout widgets.
Welcome to the module where Flutter starts feeling real. Up until now, you've been learning about widgets as individual pieces. Today, you learn how to arrange them. Layout in Flutter is done entirely with widgets -- there's no separate CSS file, no XML layout, no drag-and-drop editor. You literally nest widgets inside other widgets to build your screen. Sound tedious? It's actually one of Flutter's greatest strengths, because what you see in your code is exactly what appears on screen. No magic, no indirection. Let's get our hands dirty.
Row and Column: The Dynamic Duo
Teach: Row arranges children horizontally, Column arranges them vertically, and together they handle the vast majority of layout needs. See: Code examples showing children lining up in both directions, with alignment options controlling spacing. Feel: That Row and Column are your bread and butter -- you'll use them on every single screen.
If you only learn two layout widgets today, make them Row and Column. These are the workhorses of Flutter layout. Almost every screen you'll ever build starts with a Column of things stacked top to bottom, and inside that column, Rows of things arranged left to right. Sound simple? That's the whole idea.
Think of Column as a vertical stack of pancakes. Each child widget sits on top of the next, going down the screen. Row is the same thing, but sideways -- children line up from left to right.
Column(
children: [
Text('First'),
Text('Second'),
Text('Third'),
],
)
That's it. Three text widgets, stacked vertically. Now flip it sideways:
Row(
children: [
Icon(Icons.home),
Icon(Icons.search),
Icon(Icons.settings),
],
)
Three icons, side by side.
MainAxisAlignment: Controlling the Main Direction
Here's where it gets interesting. Both Row and Column have a main axis and a cross axis. For a Column, the main axis runs vertically (top to bottom). For a Row, it runs horizontally (left to right).
MainAxisAlignment controls how children are distributed along the main axis:
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('First'),
Text('Second'),
Text('Third'),
],
)
The options:
| Value | What It Does |
|---|---|
start |
Pack children at the beginning (default) |
end |
Pack children at the end |
center |
Center children along the axis |
spaceBetween |
Equal space between children, none at edges |
spaceAround |
Equal space around each child |
spaceEvenly |
Equal space between children AND at edges |
CrossAxisAlignment: Controlling the Other Direction
CrossAxisAlignment controls alignment on the perpendicular axis. For a Column, that's horizontal. For a Row, that's vertical.
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Short'),
Text('A much longer piece of text'),
Text('Medium text'),
],
)
The options: start, end, center, stretch (forces children to fill the cross axis).
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(Icons.home),
Icon(Icons.search),
Icon(Icons.settings),
],
)
This pushes the icons to the left edge, center, and right edge of the row.
The axis trick: For Column, main = vertical, cross = horizontal. For Row, main = horizontal, cross = vertical. Memorize this and layout puzzles get much easier.
Expanded and Flexible: Sharing Space
Teach: Expanded forces a child to fill remaining space, Flexible lets a child take up to its share, and the flex property divides space proportionally. See: Red and blue containers dividing a Row by different ratios, and the visible difference between Expanded (must fill) and Flexible (can shrink). Feel: That responsive sizing is just a matter of wrapping children in the right widget.
Here's a question that comes up immediately when you start using Row and Column: what happens when you want a child to stretch and fill all the remaining space? That's where Expanded comes in. And its cousin Flexible gives you even finer control. Think of Expanded as a greedy child that says "I'll take whatever room is left" and Flexible as a polite child that says "I'll take up to this much, but I'm fine with less."
Expanded forces a child to fill all remaining space along the main axis. Wrap any child in Expanded and it gobbles up leftover room:
Row(
children: [
Expanded(
child: Container(color: Colors.red, height: 50),
),
Container(width: 100, height: 50, color: Colors.blue),
],
)
Here, the blue container is exactly 100 pixels wide. The red container expands to fill everything else.
The flex Property
When multiple children are Expanded, the flex property controls how they divide the space:
Row(
children: [
Expanded(
flex: 2,
child: Container(color: Colors.red, height: 50),
),
Expanded(
flex: 1,
child: Container(color: Colors.blue, height: 50),
),
],
)
// Red gets 2/3 of the width, Blue gets 1/3
Think of flex as shares. Red has 2 shares, blue has 1 share, total is 3 shares. Red gets 2/3, blue gets 1/3.
Flexible vs. Expanded
Flexible is like Expanded with a relaxed attitude. An Expanded widget MUST fill its allocated space. A Flexible widget CAN fill up to its allocated space, but is happy to be smaller:
Row(
children: [
Flexible(
flex: 2,
child: Container(color: Colors.red, height: 50, width: 50),
),
Flexible(
flex: 1,
child: Container(color: Colors.blue, height: 50, width: 50),
),
],
)
Both containers are only 50 pixels wide, even though they have room to grow. Expanded would force them to fill the space.
Container: The Swiss Army Knife
Teach: Container combines padding, margin, color, size constraints, and decoration into a single versatile widget -- but simpler alternatives exist for single-purpose use. See: A Container with rounded corners, shadow, and padding all configured in one place, plus the EdgeInsets patterns you'll use daily. Feel: That Container is powerful but knowing when NOT to use it is just as important.
If Row and Column are the bread and butter, Container is the Swiss Army knife. Need padding? Container. Need a background color? Container. Need rounded corners? Container. Need a fixed size? Container. It does a lot of things, and knowing when to use it -- and when NOT to use it -- is a skill you'll develop over time.
Container is the most versatile single-child layout widget. It can do padding, margin, color, size constraints, and decoration all in one:
Container(
width: 200,
height: 100,
padding: EdgeInsets.all(16),
margin: EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 4,
offset: Offset(2, 2),
),
],
),
child: Text(
'Hello!',
style: TextStyle(color: Colors.white, fontSize: 18),
),
)
The Golden Rule of Container
Here's something that trips up beginners: if you're ONLY using Container for padding, use the Padding widget instead. If you're ONLY using it for a fixed size, use SizedBox. Container is great when you need several things at once, but using it for a single purpose is like using a Swiss Army knife to open a letter -- technically works, but there's a better tool.
EdgeInsets Patterns
You'll use EdgeInsets constantly for padding and margin:
EdgeInsets.all(16) // Same on all sides
EdgeInsets.symmetric(horizontal: 16, vertical: 8) // H and V
EdgeInsets.only(left: 8, top: 16) // Specific sides
EdgeInsets.fromLTRB(8, 16, 8, 0) // Left, Top, Right, Bottom
Padding, SizedBox, and Spacer
Teach: Padding adds space around a widget, SizedBox creates fixed-size gaps, and Spacer fills remaining space -- these small widgets are the whitespace of your UI. See: Spacing between text items using SizedBox, a Spacer pushing elements apart, and Padding giving a widget breathing room. Feel: That controlling whitespace is easy and these three widgets cover every spacing need.
These three widgets seem tiny and unimportant compared to the big layout guns like Row and Column. But you'll actually use them more than anything else. They're the whitespace of your UI. Without them, everything would be crammed together like sardines in a can.
Padding
Adds space AROUND its child widget:
Padding(
padding: EdgeInsets.all(16),
child: Text('I have breathing room!'),
)
Use Padding when padding is the only thing you need. Use Container when you need padding PLUS other stuff.
SizedBox
Creates a fixed-size invisible box. Most commonly used as a spacer:
Column(
children: [
Text('Title'),
SizedBox(height: 16), // 16 pixels of vertical space
Text('Subtitle'),
SizedBox(height: 8), // 8 pixels of vertical space
Text('Body text'),
],
)
In a Row, use SizedBox(width: 16) for horizontal spacing.
Spacer
A flexible space that expands to fill remaining room inside a Row or Column:
Row(
children: [
Text('Left'),
Spacer(), // Pushes "Right" to the far edge
Text('Right'),
],
)
Spacer is essentially Expanded(child: SizedBox.shrink()). It's just more readable.
The spacing hierarchy: SizedBox for fixed spacing. Spacer for flexible spacing. Padding when you need space around a specific widget.
Stack and Positioned: Layered Layouts
Teach: Stack lets you layer widgets on top of each other, and Positioned gives precise control over where each layer sits. See: Widgets overlapping like cards on a table, with badges and labels positioned in corners. Feel: Excitement about building notification badges, overlay text on images, and floating UI elements.
Everything we've done so far has been linear -- things going left to right, or top to bottom. But what about when you need to put a notification badge on top of an icon? Or overlay text on an image? Or stack cards on top of each other? That's where Stack comes in. Think of Stack as a pile of transparent sheets on an overhead projector. Each child is a new sheet laid on top of the previous one.
Stack layers its children on top of each other:
Stack(
children: [
Container(width: 200, height: 200, color: Colors.blue),
Container(width: 150, height: 150, color: Colors.green),
Container(width: 100, height: 100, color: Colors.red),
],
)
This creates three nested squares, with blue on the bottom and red on top.
Positioned
Inside a Stack, Positioned lets you place a child at exact coordinates:
Stack(
children: [
Container(width: 200, height: 200, color: Colors.blue),
Positioned(
top: 10,
right: 10,
child: Icon(Icons.star, color: Colors.white),
),
Positioned(
bottom: 10,
left: 10,
child: Text(
'Hello!',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
],
)
The star sits 10 pixels from the top-right corner. The text sits 10 pixels from the bottom-left.
Real-World Example: Notification Badge
Here's a pattern you'll see in real apps:
Stack(
clipBehavior: Clip.none,
children: [
Icon(Icons.mail, size: 32),
Positioned(
top: -4,
right: -4,
child: Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: Text(
'3',
style: TextStyle(color: Colors.white, fontSize: 10),
),
),
),
],
)
That little red circle with a number on a mail icon? Stack and Positioned.
There Are No Dumb Questions
Teach: Answers to the most common layout questions beginners ask, clearing up confusion before it becomes a habit. See: Concise answers about Container vs. Padding, nesting Rows and Columns, overflow errors, and Stack alignment. Feel: Relief that your "dumb" questions are universal and have simple answers.
Q: When should I use Container vs. Padding vs. SizedBox?
A: Use the simplest widget that does what you need. Need only padding? Use Padding. Need only a fixed size? Use SizedBox. Need padding AND a background color AND rounded corners? That's Container territory. Flutter performance is better when you use simple, single-purpose widgets.
Q: Can I nest Rows inside Columns and vice versa?
A: Absolutely, and you'll do it constantly. A typical screen is a Column containing several Rows. Some of those Rows might contain Columns. It's turtles all the way down.
Q: What happens if I put too many widgets in a Row and they overflow?
A: You'll get the dreaded yellow-and-black overflow stripes -- Flutter's way of telling you your widgets don't fit. Fix it with Expanded, Flexible, or by wrapping the Row in a SingleChildScrollView with scrollDirection: Axis.horizontal.
Q: Why does Stack need Positioned? Can't I just use alignment?
A: Stack has an alignment property that controls where non-Positioned children go (default is top-left). But Positioned gives you pixel-level control. Use alignment for simple cases and Positioned when you need precision.
Q: What's the difference between Spacer and an Expanded SizedBox?
A: Nothing. Spacer is literally just an Expanded wrapping a SizedBox.shrink(). It's a convenience widget that makes your code more readable.
The overflow question deserves a moment. Those yellow-and-black stripes mean you've asked Flutter to fit more content than the screen allows. Three common fixes, in order: wrap the child in Expanded to let it shrink, wrap the parent in a SingleChildScrollView if scrolling is acceptable, or reduce the content. The worst mistake is ignoring the warning and hoping it goes away. It won't. A production app with overflow stripes is a bug you can see from across the room.
Sharpen Your Pencil: Calculator Layout
Teach: How to combine Row, Column, Expanded, and Container to build a realistic calculator screen layout from scratch. See: A calculator UI with a display area and a grid of buttons, all built entirely with layout widgets. Feel: Accomplishment that a complex-looking screen is really just nested Rows and Columns.
Time to put these layout widgets to work. You'll build a calculator screen that looks like the one on your phone -- but don't worry, it doesn't need to actually calculate anything. This is purely a layout exercise.
Setup
flutter create layout_widgets
Replace lib/main.dart with an app shell:
import 'package:flutter/material.dart';
import 'calculator_screen.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Layout Widgets',
debugShowCheckedModeBanner: false,
home: const CalculatorScreen(),
);
}
}
Build lib/calculator_screen.dart
Create a calculator layout with these requirements:
-
A
Scaffoldwith anAppBartitled'Calculator'and a dark background (backgroundColor: Colors.black). -
The body is a
Columncontaining: - An
Expandedwidget (flex: 2) for the display area -- aContainerwithAlignment.bottomRight, padding of 24, showing'0'in white atfontSize: 48 -
An
Expandedwidget (flex: 5) for the button grid -
Create a helper method
Widget buildButton(String label, {Color color = Colors.grey})that returns anExpandedchild with aContainer(margin of 1, background color) containing a centeredText(white, fontSize 24). -
Build 5 rows of buttons:
- Row 1:
C,+/-,%,/(orange) - Row 2:
7,8,9,x(orange) - Row 3:
4,5,6,-(orange) - Row 4:
1,2,3,+(orange) - Row 5:
0(flex: 2),.,=(orange)
Where this fits: This exercise combines Row, Column, Expanded, Container, and color -- nearly everything from this module in a single screen. If you can build this, you understand Flutter layout fundamentals.
Build the calculator one row at a time. Don't try to nail every button at once — get one row working, pat yourself on the back, then copy-paste and tweak for the next row. You'll see immediately why Expanded matters. If you forget it, buttons collapse to their natural size and leave gaps. If you wrap each button in Expanded, they share available space evenly. The last row with the zero button at flex 2 is the trick shot — it shows why flex values, not fixed widths, make layouts responsive.
Sharpen Your Pencil: Card Grid
Teach: How to use Expanded with flex ratios, MainAxisAlignment spacing, and Stack with Positioned to build a variety of grid-like arrangements. See: Equal-width cards, proportional cards, spaced-out cards, and layered cards using Stack. Feel: Confidence that you can build any card-based layout by choosing the right combination of widgets.
Build lib/card_grid_screen.dart
Create a StatelessWidget called CardGridScreen with these sections:
-
Row 1 -- Three equal-width cards using
Expanded. Each is a coloredContainer(height: 100, borderRadius: 8) with a centered white letter (A, B, C). Colors: red, green, blue. -
Row 2 -- Three cards with
flex: 1,flex: 2,flex: 1. Colors: orange, purple, teal. -
Row 3 -- Use
MainAxisAlignment.spaceBetweenwith three fixed-width cards (SizedBox(width: 80, height: 80)). -
Row 4 -- A
Stack(height: 200, width:double.infinity): - Base: grey container filling the whole space
Positioned(top: 10, left: 10): blue 80x80 containerPositioned(bottom: 10, right: 10): red 80x80 containerCenter: green 60x60 container
This exercise is where you develop your intuition for flex ratios. Row 1 with three equal-flex Expandeds gives you thirds. Row 2 with flex 1, 2, 1 gives you a 25-50-25 split. Suddenly, every "how do I make this take up two-thirds of the screen" question has an obvious answer. The Stack row is equally important. You just saw how Row and Column arrange things along an axis. Now see how Stack arranges things perpendicular to the screen — layers on top of each other. Flutter layouts are one of these two patterns, nested as deeply as you need.
Sharpen Your Pencil: Alignment Showcase
Teach: How each of the six MainAxisAlignment values distributes children differently within a Row. See: Side-by-side visual comparison of start, center, end, spaceBetween, spaceAround, and spaceEvenly. Feel: That alignment is no longer guesswork -- you can pick the right value on the first try.
Build lib/alignment_screen.dart
Create a StatelessWidget called AlignmentScreen that demonstrates all six MainAxisAlignment values:
- Wrap the body in
SingleChildScrollViewwithPaddingof 16 - For each of the six alignment values (
start,center,end,spaceBetween,spaceAround,spaceEvenly): - Add a
Textlabel showing the alignment name - Add a
Container(height: 50, light background) containing aRowwith that alignment and three small colored boxes (30x30)
Update main.dart so you can navigate between all three screens (use DefaultTabController with TabBar and TabBarView, or just change the home property and add comments).
Take screenshots of each screen.
Of all the MainAxisAlignment values, spaceBetween and spaceEvenly confuse people most. spaceBetween puts no space before the first child or after the last — all the spacing goes between children. spaceEvenly puts equal space before, between, and after every child. spaceAround splits the difference — half-sized gutters at the ends, full gutters between. Seeing all three side by side on one screen is worth a hundred diagrams. After this exercise, the alignment value you pick in any real layout will be a one-second decision.
📝 Module Quiz
Test what you learned. 45 seconds per question. Passing score is set per module. Your best attempt is saved in your browser so you can track progress -- nothing is sent to a server.
Wrapping Up
Teach: A summary of the complete layout widget toolkit and how all the pieces fit together. See: The full picture: Row, Column, Expanded, Flexible, Container, Padding, SizedBox, Spacer, Stack, and Positioned working in concert. Feel: Confidence that you can decompose any mobile screen into Flutter layout widgets.
You now have the complete layout toolkit. Row and Column for linear arrangements. Expanded and Flexible for responsive sizing. Container for decoration and constraints. Padding, SizedBox, and Spacer for breathing room. Stack and Positioned for layered effects. Every screen you'll ever build in Flutter is some combination of these widgets. The calculator exercise proves it -- a complex-looking layout is really just Rows inside a Column with some Expanded widgets doing the heavy lifting.
Layout in Flutter is composition. You don't configure a layout engine -- you compose small, single-purpose widgets into the arrangement you want. When in doubt, start with a Column, put Rows inside it, and adjust from there.