Flutter Layouts Made Easy: Row, Column, Expanded, Flexible and Stack
Understand how Flutter lays out widgets, fix the famous "RenderFlex overflowed" error, and build a real course card step by step using Row, Column, Expanded, Flexible and Stack.
CodeOrbit Learn TeamPublished 4 min read
Most Flutter screens are built from just a handful of layout widgets. Once you understand how Flutter decides sizes, layouts stop feeling like guesswork and the yellow-black "overflow" stripes disappear.
The one rule behind every layout
Keep this sentence in mind; every layout problem below is an example of it.
Row and Column
Row places children horizontally, Column vertically. Two properties control alignment:
- mainAxisAlignment: along the direction of the widget (horizontal for Row, vertical for Column).
- crossAxisAlignment: across it.
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: const [
Icon(Icons.menu_book),
Text('Chapter 4: Trees'),
Icon(Icons.chevron_right),
],
)mainAxisAlignment | Effect |
|---|---|
start | Children packed at the start |
center | Packed in the middle |
spaceBetween | First and last at the edges, equal gaps between |
spaceAround | Equal space around each child |
spaceEvenly | Equal gaps everywhere, including edges |
The famous overflow error
Row(
children: [
const Icon(Icons.menu_book),
Text('Data Structures and Algorithms: complete revision notes for exams'),
],
)On a phone this shows "A RenderFlex overflowed by 87 pixels on the right". Why? A Row gives its children unbounded width, so the Text asks for its full natural width on one line, which is wider than the screen.
The fix is to tell the text how much space it may take, using Expanded:
Row(
children: [
const Icon(Icons.menu_book),
const SizedBox(width: 8),
Expanded(
child: Text(
'Data Structures and Algorithms: complete revision notes for exams',
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
)Expanded vs Flexible
Both share the leftover space of a Row or Column by flex factor.
Expandedforces the child to fill its share.Flexiblelets the child be smaller than its share if it wants.
Row(
children: [
Expanded(flex: 2, child: Container(height: 40, color: Colors.blue)), // 2/3 of width
Expanded(flex: 1, child: Container(height: 40, color: Colors.green)), // 1/3 of width
],
)Use Flexible for things like a chip with short text that should not stretch, but must shrink if space runs out.
Stack: put widgets on top of each other
Stack layers children. Use Positioned to pin one to an edge, for example a "FREE" badge on a course image:
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(course.imageUrl, height: 140, width: double.infinity, fit: BoxFit.cover),
),
Positioned(
top: 8,
right: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(color: Colors.green, borderRadius: BorderRadius.circular(20)),
child: const Text('FREE', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
),
),
],
)Putting it together: a course card
class CourseCard extends StatelessWidget {
const CourseCard({super.key, required this.course});
final Course course;
@override
Widget build(BuildContext context) {
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(children: [/* image + FREE badge from above */]),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(course.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Row(
children: [
const Icon(Icons.timer_outlined, size: 16),
const SizedBox(width: 4),
Text('${course.minutes} min'),
const Spacer(), // pushes the button right
FilledButton(onPressed: () {}, child: const Text('Start')),
],
),
],
),
),
],
),
);
}
}Notice Spacer(): it is simply an Expanded with an empty child, a handy way to push widgets apart.
Lists inside Columns
Another classic error: putting a ListView directly inside a Column gives "Vertical viewport was given unbounded height". The Column doesn't limit its children's height, and a list wants to be infinitely tall. Wrap the list in Expanded:
Column(
children: [
const Text('All chapters'),
Expanded(child: ListView.builder(itemCount: 20, itemBuilder: (_, i) => ListTile(title: Text('Chapter ${i + 1}')))),
],
)Quick debugging checklist
- Overflow in a Row? Wrap the long child in
ExpandedorFlexible. - List in a Column? Wrap the list in
Expanded. - Something not centred? Check both main and cross axis alignment.
- Turn on Flutter Inspector → Show guidelines to see every box's size.
Frequently asked questions
What is the difference between SizedBox and Container?
SizedBox only sets a size (or adds space). Container can also add padding, colour, borders and decoration. Prefer SizedBox for spacing: it is lighter.
When should I use Wrap instead of Row?
Use Wrap when items (like tags or chips) should move to the next line when they run out of space instead of overflowing.
Why does my Column take the full height?
A Column's mainAxisSize is max by default. Set mainAxisSize: MainAxisSize.min to make it only as tall as its children.
Related posts
Flutter
Flutter State Management: setState vs Provider vs Riverpod (with One Example App)
Build the same quiz-score screen three ways in Flutter, with setState, Provider and Riverpod, and learn exactly when each approach is the right choice.
5 min read