Flutter: Layout System
Flutter layout is like an assembly line — parent passes constraints, child reports size, parent determines position.
📋 Prerequisites: You need to have completed the following first
- Lesson 3: Widget Fundamentals
1. What You Will Learn
- Box model: Constraints → Size → Position constraint passing protocol
- Linear layout: Row/Column with Main/CrossAxisAlignment alignment
- Elastic layout: Flex/Expanded/Flexible proportional distribution
- Layered layout: Stack/Positioned for overlays and badges
- ShopApp product grid layout: GridView.count and waterfall flow
2. A Story from a UI Designer Transitioning to Development
(1) The Pain: The Yellow-Black Overflow Stripe Nightmare
Bob received the ShopApp UI design specs — product cards needed an image on the left and info on the right, with a price row at the bottom. He nested a Row inside a Column, only to see yellow-black striped overflow warnings across the screen. After adjusting padding and margin for ages, either text was truncated or images were distorted. 1000 products, 10 screen sizes, layout problems everywhere.
(2) The Constraint Passing Solution
Flutter's layout has one core rule: constraints pass down, sizes report up, positions are determined by the parent. Understanding this rule, Bob could predict every Widget's behavior.
import 'package:flutter/material.dart';
// Parent gives tight constraints: exactly 200x100
SizedBox(
width: 200,
height: 100,
child: Text('I will be 200x100'), // Text forced to 200x100
)
// Parent gives loose constraints: 0 to maxWidth
Row(
children: [
Expanded(child: Text('I take remaining space')),
SizedBox(width: 80, child: Text('I am 80 wide')),
],
)
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
(3) The Result: Get Layouts Right the First Time
After mastering constraint passing, Bob's first-time layout accuracy improved from 30% to 90%, and overflow warnings disappeared entirely.
3. Box Model and Constraint Passing
graph TD
subgraph Constraint Passing
P[Parent Constraints] --> C[Child Layout]
C --> S[Child Size]
S --> P
end
subgraph Common Layouts
R[Row] --> EX1[Expanded]
CO[Column] --> EX2[Flexible]
ST[Stack] --> POS[Positioned]
end
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
(1) Constraint Types
| Type | Description | Example |
|---|---|---|
| Tight | min = max, fixed size | SizedBox(width: 100) |
| Loose | min = 0, max = parent max | Child inside Center |
| Unbounded | max = infinity | Column inside ListView |
| Bounded | Both min and max are finite | Column inside a Page |
▶ Example
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
: Constraint passing demonstration
import 'package:flutter/material.dart';
// Demo: constraint passing in action
class ConstraintDemo extends StatelessWidget {
const ConstraintDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
// Tight constraint: exactly 200 height
Container(
height: 200,
color: Colors.blue[100],
child: const Center(child: Text('Height: 200 (tight)')),
),
// Loose constraint: takes remaining space
Expanded(
child: Container(
color: Colors.green[100],
child: const Center(child: Text('Expanded (loose, fills remaining)')),
),
),
],
),
);
}
}
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
4. Linear Layout: Row and Column
(1) Alignment Properties
| Property | Direction | Row Effect | Column Effect |
|---|---|---|---|
mainAxisAlignment |
Main axis | Horizontal alignment | Vertical alignment |
crossAxisAlignment |
Cross axis | Vertical alignment | Horizontal alignment |
mainAxisSize |
Main axis size | Width | Height |
▶ Example
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
: Product info row (Row)
import 'package:flutter/material.dart';
// Product info row: image + details
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Fixed-size image
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
'https://cdn.shopapp.com/laptop.jpg',
width: 100,
height: 100,
fit: BoxFit.cover,
),
),
const SizedBox(width: 12),
// Flexible details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Pro Laptop 16"',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text('Electronics > Laptops',
style: TextStyle(fontSize: 12, color: Colors.grey[600])),
const SizedBox(height: 8),
Row(
children: [
const Text('\$1,299.99',
style: TextStyle(fontSize: 18, color: Colors.green)),
const SizedBox(width: 8),
Text('\$1,599.99',
style: TextStyle(
fontSize: 14,
decoration: TextDecoration.lineThrough,
color: Colors.grey[400],
)),
],
),
],
),
),
],
)
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
(2) Common Alignment Values Comparison
| MainAxisAlignment | Effect |
|---|---|
start |
Align to start (default) |
center |
Center |
end |
Align to end |
spaceBetween |
First/last at edges, equal spacing between |
spaceAround |
Equal spacing on both sides of each item |
spaceEvenly |
All spacing equal |
5. Elastic Layout: Expanded and Flexible
▶ Example
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
: Price and button elastic layout
import 'package:flutter/material.dart';
// Bottom bar: price (expanded) + button (fixed)
Container(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Expanded(
flex: 2, // Takes 2/3 of space
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Total', style: TextStyle(fontSize: 12)),
Text('\$${total.toStringAsFixed(2)}',
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
],
),
),
Expanded(
flex: 1, // Takes 1/3 of space
child: ElevatedButton(
onPressed: onCheckout,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16)),
child: const Text('Checkout'),
),
),
],
),
)
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
| Widget | Behavior | Remaining Space | Use Case |
|---|---|---|---|
Expanded |
Force fill | Distributed proportionally | Must fill all space |
Flexible |
Optionally fill | Distributed proportionally, can be less | Flexible but not forced |
▶ Example
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
: Expanded vs Flexible
import 'package:flutter/material.dart';
Row(
children: [
// Expanded: must fill allocated space
Expanded(
flex: 1,
child: Container(color: Colors.red, child: const Text('Expanded')),
),
// Flexible: can be smaller than allocated space
Flexible(
flex: 1,
child: Container(
color: Colors.blue,
child: const Text('Flexible'), // May not fill all space
),
),
// Fixed width
SizedBox(width: 80, child: Container(color: Colors.green, child: const Text('Fixed'))),
],
)
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
6. Layered Layout: Stack and Positioned
Stack allows children to overlap in paint order, and Positioned controls absolute positioning.
▶ Example
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
: Product image badges
import 'package:flutter/material.dart';
// Simplified class definition
class Product {
final String imageUrl;
const Product({required this.imageUrl});
}
// Product image with discount badge and favorite button
Stack(
children: [
// Base image
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
product.imageUrl,
height: 200,
width: double.infinity,
fit: BoxFit.cover,
),
),
// Discount badge (top-left)
Positioned(
top: 8,
left: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(4),
),
child: Text('${discountPercent}% OFF',
style: const TextStyle(color: Colors.white, fontSize: 12)),
),
),
// Favorite button (top-right)
Positioned(
top: 8,
right: 8,
child: CircleAvatar(
backgroundColor: Colors.white.withOpacity(0.8),
child: const Icon(Icons.favorite_border, size: 18),
),
),
// Stock indicator (bottom)
Positioned(
bottom: 8,
left: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(4),
),
child: const Text('In Stock', style: TextStyle(color: Colors.white, fontSize: 10)),
),
),
],
)
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
| Stack Property | Description | Default |
|---|---|---|
alignment |
Alignment for non-Positioned children | AlignmentDirectional.topStart |
fit |
How children adapt | StackFit.loose |
clipBehavior |
Overflow clipping | Clip.hardEdge |
7. GridView Grid Layout
▶ Example
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
: ShopApp product grid
import 'package:flutter/material.dart';
// Simplified class definition
class Product {
final int id;
final String name;
final double price;
const Product({required this.id, required this.name, required this.price});
}
// Two-column product grid
GridView.count(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 0.75, // height = width / 0.75
padding: const EdgeInsets.all(8),
children: products.map((product) => ProductCard(
key: ValueKey(product.id),
product: product,
onAddToCart: () => addToCart(product),
)).toList(),
)
// Responsive grid with LayoutBuilder
LayoutBuilder(
builder: (context, constraints) {
int columns = (constraints.maxWidth / 180).floor().clamp(2, 4);
return GridView.count(
crossAxisCount: columns,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 0.75,
children: productWidgets,
);
},
)
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
| GridView Property | Description |
|---|---|
crossAxisCount |
Number of columns per row |
mainAxisSpacing |
Main axis spacing |
crossAxisSpacing |
Cross axis spacing |
childAspectRatio |
Child item width-to-height ratio |
padding |
Inner padding |
8. Complete Example: ShopApp Home Page Layout
import 'package:flutter/material.dart';
class ShopHomePage extends StatelessWidget {
const ShopHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ShopApp'),
actions: [
Stack(
children: [
IconButton(icon: const Icon(Icons.shopping_cart), onPressed: () {}),
Positioned(right: 4, top: 4,
child: CircleAvatar(radius: 8, backgroundColor: Colors.red,
child: const Text('5', style: TextStyle(fontSize: 10, color: Colors.white)))),
],
),
],
),
body: CustomScrollView(
slivers: [
// Banner section
SliverToBoxAdapter(
child: Container(
height: 180,
color: Colors.blue[50],
child: const Center(child: Text('Flash Sale - Up to 50% OFF',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold))),
),
),
// Category chips
SliverToBoxAdapter(
child: SizedBox(
height: 50,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
children: ['All', 'Electronics', 'Clothing', 'Books', 'Home']
.map((c) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Chip(label: Text(c)),
)).toList(),
),
),
),
// Product grid
SliverPadding(
padding: const EdgeInsets.all(8),
sliver: SliverGrid.count(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 0.7,
children: List.generate(20, (i) => _buildProductCard(i)),
),
),
],
),
);
}
Widget _buildProductCard(int index) {
return Card(
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 3,
child: Container(
color: Colors.grey[200],
child: const Icon(Icons.image, size: 48, color: Colors.grey),
),
),
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Product ${index + 1}', maxLines: 1, overflow: TextOverflow.ellipsis),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('\$${(19.99 + index * 10).toStringAsFixed(2)}',
style: const TextStyle(color: Colors.green)),
const Icon(Icons.add_circle, size: 20),
],
),
],
),
),
),
],
),
);
}
}
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
❓ FAQ
📖 Summary
- Flutter layout core: constraints pass down, sizes report up, positions determined by parent
- Row/Column are linear layouts, controlled via Main/CrossAxisAlignment
- Expanded forces fill, Flexible adapts elastically, flex controls proportions
- Stack + Positioned creates layered effects (badges, overlays)
- GridView.count builds product grids, LayoutBuilder enables responsive column counts
📝 Exercises
- Basic (difficulty ⭐): Create a horizontally scrolling category tag bar (Chip + ListView horizontal direction).
- Intermediate (difficulty ⭐⭐): Implement a product detail page layout: top large image + middle info section + bottom price/buy button bar, using Column + Expanded.
- Challenge (difficulty ⭐⭐⭐): Use LayoutBuilder to implement a responsive product grid: 2 columns on phone, 3 on tablet, 4 on desktop, switching in real-time on window resize.