- Hiểu mental model: "Constraints flow down, Sizes flow up, Parent positions".
- Sử dụng
Row,ColumnvớiMainAxisAlignment,CrossAxisAlignment. - Sử dụng
Expanded,Flexible,Spacercho responsive. - Sử dụng
Stack+Positionedcho overlap. - Container, Padding, SizedBox, Align, Center.
- ConstrainedBox, LimitedBox, FractionallySizedBox.
- Debug layout với
debugPaintSizeEnabled = truevà Flutter Inspector. - Hiểu khi nào widget bị "unbounded constraint" lỗi.
1. Mental model — Constraints flow
Quy tắc 3 dòng — luật vật lý của Flutter layout:
- Parent pass constraint xuống child.
- Child pick size trong constraint, trả lên parent.
- Parent position child theo logic của nó.
Center(
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
)
Flow:
- Screen pass constraint (vd
BoxConstraints(0..390, 0..844)trên iPhone) xuốngCenter. Centerpass cùng constraint xuốngContainer.Containerpick size(100, 100)(vì cówidth+height), trả lênCenter.Centerposition container vào giữa screen size.
2. BoxConstraints
BoxConstraints(
minWidth: 0,
maxWidth: 390,
minHeight: 0,
maxHeight: 844,
)
// Tight — min == max (fixed size, no choice)
BoxConstraints.tight(Size(100, 100));
// → minWidth: 100, maxWidth: 100, minHeight: 100, maxHeight: 100
// Loose — min == 0 (có lựa chọn, max là giới hạn)
BoxConstraints.loose(Size(100, 100));
// → minWidth: 0, maxWidth: 100, minHeight: 0, maxHeight: 100
Phân loại:
| Loại | Định nghĩa | Ví dụ widget cấp |
|---|---|---|
| Tight | min == max — child không có lựa chọn | Screen root, SizedBox với cả w+h |
| Loose | min == 0 — child tự do trong max | Center, Align (loosen child) |
| Bounded | max là finite | Đa số case |
| Unbounded | max = infinity | ListView, SingleChildScrollView (theo axis cuộn) |
3. Row và Column
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, // trục chính (ngang)
crossAxisAlignment: CrossAxisAlignment.center, // trục phụ (dọc)
children: [
const Icon(Icons.menu),
const Text('Title'),
const Icon(Icons.search),
],
)
Column(
mainAxisAlignment: MainAxisAlignment.center, // trục chính (dọc)
crossAxisAlignment: CrossAxisAlignment.stretch, // trục phụ (ngang)
children: [/*...*/],
)
MainAxisAlignment values
start— dồn về đầu trục chính.end— dồn về cuối.center— dồn vào giữa.spaceBetween— child đầu/cuối sát mép, giữa chia đều.spaceAround— child có khoảng cách bằng nhau quanh.spaceEvenly— khoảng cách giữa các child + mép đều nhau.
CrossAxisAlignment values
start,end,center— căn theo trục phụ.stretch— child fill toàn bộ trục phụ.baseline— căn theo text baseline (cầntextBaseline).
4. Expanded & Flexible
Dùng trong Row/Column/Flex để chia phần space còn lại.
Row(
children: [
Expanded(
flex: 1,
child: Container(height: 50, color: Colors.red),
),
Expanded(
flex: 2,
child: Container(height: 50, color: Colors.green),
),
Expanded(
flex: 1,
child: Container(height: 50, color: Colors.blue),
),
],
)
// Chia tỷ lệ 1:2:1 — red 25%, green 50%, blue 25%
Expanded = Flexible(fit: FlexFit.tight). Khác biệt:
- Expanded — child BUỘC fill phần space chia (tight).
- Flexible — child có thể nhỏ hơn (loose, fit:
loose).
Spacer
Row(
children: [
const Text('Left'),
const Spacer(), // = Expanded(child: SizedBox())
const Text('Right'),
],
)
// Left | (space) | Right
5. Stack & Positioned
Overlap children — child sau vẽ đè child trước.
Stack(
alignment: Alignment.center, // align cho non-positioned children
children: [
// Background
Container(height: 200, color: Colors.amber),
// Centered text (non-positioned — dùng stack alignment)
const Text('Title', style: TextStyle(fontSize: 24)),
// Positioned
const Positioned(
top: 10,
right: 10,
child: Icon(Icons.favorite, color: Colors.red),
),
// Positioned.fill
Positioned.fill(
bottom: 0,
top: null,
child: Container(height: 30, color: Colors.black54),
),
],
)
6. Container — composite widget
Container là kitchen sink — combine Padding + ColoredBox + DecoratedBox + ConstrainedBox + Align + Transform.
Dùng nhanh nhưng đôi khi nên tách primitive cho performance.
Container(
width: 200,
height: 100,
margin: const EdgeInsets.all(8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey),
boxShadow: [
BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 2)),
],
gradient: const LinearGradient(
colors: [Colors.amber, Colors.orange],
),
),
child: const Text('Card'),
)
Container chỉ cần padding 8 → dùng Padding trực tiếp. Chỉ cần color → ColoredBox.
Primitive nhẹ hơn Container (Container compose nhiều thứ).
7. SizedBox + Padding + Center/Align
SizedBox
const SizedBox(width: 100, height: 50)
const SizedBox(height: 16) // spacer dọc
const SizedBox(width: 8) // spacer ngang
const SizedBox.shrink() // (0, 0) — invisible spacer
const SizedBox.expand() // fill cha
Padding & EdgeInsets
EdgeInsets.all(16)
EdgeInsets.symmetric(horizontal: 16, vertical: 8)
EdgeInsets.only(left: 8, top: 4)
EdgeInsets.fromLTRB(8, 4, 8, 16) // left, top, right, bottom
Center & Align
const Center(child: Text('centered'))
// Center là Align với alignment center
const Align(
alignment: Alignment.bottomRight,
child: Text('bottom right'),
)
// Alignment values: 9 preset (-1..1, -1..1)
Alignment.topLeft
Alignment.topCenter
Alignment.topRight
Alignment.centerLeft
Alignment.center
Alignment.centerRight
Alignment.bottomLeft
Alignment.bottomCenter
Alignment.bottomRight
// Custom
Alignment(0.5, -0.7) // 50% right of center, 70% up from center
8. ConstrainedBox / LimitedBox / FractionallySizedBox
// ConstrainedBox — apply constraint thêm cho child
ConstrainedBox(
constraints: const BoxConstraints(minHeight: 50, maxHeight: 200),
child: Text(longText),
)
// LimitedBox — chỉ apply khi parent unbounded
// VD trong ListView, cha cho child unbounded height
LimitedBox(
maxHeight: 200,
child: SomeWidget(),
)
// FractionallySizedBox — size theo % parent
FractionallySizedBox(
widthFactor: 0.5, // 50% width của parent
heightFactor: 0.3,
child: Container(color: Colors.red),
)
9. AspectRatio
AspectRatio(
aspectRatio: 16 / 9, // w/h
child: Container(color: Colors.amber),
)
// Hữu ích cho video player, image, card
AspectRatio(
aspectRatio: 1,
child: Image.network(url), // vuông 1:1
)
10. 3 lỗi phổ biến + fix
10.1 "RenderFlex overflowed by X pixels"
// ❌ Lỗi — text dài quá row
Row(
children: [
const Icon(Icons.info),
Text('Lorem ipsum dolor sit amet consectetur adipiscing elit ...'),
],
)
// ✅ Fix: wrap Expanded
Row(
children: [
const Icon(Icons.info),
Expanded(child: Text('Lorem ipsum ...')),
],
)
// ✅ Fix khác: SingleChildScrollView horizontal
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(children: [/*long row*/]),
)
// ✅ Fix khác: FittedBox shrink
FittedBox(
fit: BoxFit.scaleDown,
child: Row(children: [/*long*/]),
)
10.2 "Vertical viewport was given unbounded height"
// ❌ Lỗi — ListView trong Column không có constraint
Column(
children: [
const Text('Header'),
ListView(children: [/*...*/]), // ListView muốn height infinity
],
)
// ✅ Fix 1: Expanded
Column(
children: [
const Text('Header'),
Expanded(child: ListView(children: [/*...*/])),
],
)
// ✅ Fix 2: SizedBox fixed
Column(
children: [
const Text('Header'),
SizedBox(
height: 300,
child: ListView(children: [/*...*/]),
),
],
)
// ✅ Fix 3: shrinkWrap (cẩn thận performance, ListView không lazy nữa)
Column(
children: [
const Text('Header'),
ListView(shrinkWrap: true, children: [/*...*/]),
],
)
10.3 "Expanded must be a descendant of a Flex"
// ❌ Lỗi — Expanded chỉ dùng trong Row/Column/Flex
Container(
child: Expanded(child: Text('...')), // crash
)
// ✅ Fix: wrap Row/Column trước, hoặc dùng widget khác
SizedBox(
width: double.infinity,
child: Container(child: Text('...')),
)
11. Debug layout
// Trong main() — vẽ border quanh mọi widget
import 'package:flutter/rendering.dart';
void main() {
debugPaintSizeEnabled = true;
runApp(const MyApp());
}
Flag debug khác:
debugPaintBaselinesEnabled— vẽ text baseline.debugPaintPointersEnabled— highlight tap area.debugRepaintRainbowEnabled— màu thay đổi mỗi repaint (chương 13).
Flutter Inspector trong DevTools — bật "Select Widget Mode" → tap widget trên app → xem constraint, size, vị trí. Là tool quan trọng nhất khi debug layout.
12. Bài tập
Twitter card layout
Recreate Twitter post layout: avatar + name + handle + text content + action row (4 icon). Chỉ widget chương 3.
💡 Gợi ý đáp án
Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const CircleAvatar(radius: 24, backgroundColor: Colors.amber),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: const [
Text('Việt', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(width: 6),
Text('@viet · 2h', style: TextStyle(color: Colors.grey)),
],
),
const SizedBox(height: 4),
const Text('Đang học Flutter với IT Basic. Layout thật là phức tạp!'),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
_ActionIcon(icon: Icons.chat_bubble_outline, label: '12'),
_ActionIcon(icon: Icons.repeat, label: '5'),
_ActionIcon(icon: Icons.favorite_border, label: '42'),
_ActionIcon(icon: Icons.share, label: ''),
],
),
],
),
),
],
),
)
Responsive 2-column với LayoutBuilder
Build responsive layout: nếu screen width > 600 thì 2 column ngang 1:1, ngược lại 1 column dọc.
Dùng LayoutBuilder.
💡 Gợi ý đáp án
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 600) {
return Row(
children: [
Expanded(child: _ColumnA()),
Expanded(child: _ColumnB()),
],
);
}
return Column(
children: [_ColumnA(), _ColumnB()],
);
},
)
LayoutBuilder cho phép build dựa trên constraint của parent — useful cho responsive.
Stack profile header
Tạo profile header với background gradient phía sau, avatar tròn center bottom (50% chìm dưới header).
Dùng Stack + Positioned.
💡 Gợi ý đáp án
Stack(
clipBehavior: Clip.none,
alignment: Alignment.bottomCenter,
children: [
Container(
height: 200,
decoration: const BoxDecoration(
gradient: LinearGradient(colors: [Colors.purple, Colors.pink]),
),
),
Positioned(
bottom: -50, // 50% radius (radius=50, full=100)
child: Container(
width: 100, height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
border: Border.all(color: Colors.white, width: 4),
),
),
),
],
)
clipBehavior: Clip.none cho phép child overflow ra ngoài Stack.
Debug overflow 3 cách fix
Cho code có "RenderFlex overflowed":
Row(
children: [
const Icon(Icons.info),
Text('A very very long text that overflows the row width'),
],
)
Fix bằng 3 cách: Expanded, SingleChildScrollView, FittedBox. Trong mỗi cách giải thích trade-off.
💡 Gợi ý đáp án
Expanded: text auto wrap xuống nhiều dòng.
SingleChildScrollView: user scroll ngang, không wrap. UX có thể không tối ưu.
FittedBox(fit: BoxFit.scaleDown): scale down toàn row cho vừa, có thể text nhỏ khó đọc.
Pricing 3 plan side-by-side
Build pricing card 3 plan (Free / Pro / Enterprise) hiển thị side-by-side với Row + Expanded.
💡 Gợi ý đáp án
Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
Expanded(child: _PlanCard(name: 'Free', price: '0')),
SizedBox(width: 12),
Expanded(child: _PlanCard(name: 'Pro', price: '99', highlighted: true)),
SizedBox(width: 12),
Expanded(child: _PlanCard(name: 'Enterprise', price: '299')),
],
)
crossAxisAlignment: stretch để mọi card cao bằng nhau.
13. Quiz
Expanded dùng ngoài Row/Column được không?
Xem đáp án
Đáp án: Không. Lỗi runtime "Expanded widgets must be placed inside Flex widgets". Chỉ dùng trong Row/Column/Flex.
Row mà children tổng > viewport — chuyện gì?
Xem đáp án
Đáp án: "RenderFlex overflowed" error + yellow/black stripe. Fix: Expanded/Flexible cho 1 child, hoặc wrap scrollable, hoặc FittedBox.
mainAxisAlignment của Row là trục nào?
Xem đáp án
Đáp án: Trục ngang (horizontal) — vì main axis của Row là ngang. Của Column thì main là dọc.
Container(width: 200) trong parent constraint tight 100 — kết quả width?
Xem đáp án
Đáp án: 100. Parent constraint thắng — Container không có lựa chọn vì constraint tight (min == max).
Stack default alignment là gì?
Xem đáp án
Đáp án: AlignmentDirectional.topStart (top-left ở LTR locale). Áp dụng cho non-positioned children.
Padding vs Container(padding: ...) — khác performance?
Xem đáp án
Đáp án: Padding đơn lẻ rẻ hơn. Container compose nhiều thứ (Padding + ColoredBox + DecoratedBox + ...) — nặng hơn nếu chỉ cần padding.
Flexible(flex: 2) và Expanded(flex: 2) khác gì?
Xem đáp án
Đáp án: Expanded = Flexible(fit: FlexFit.tight). Flexible mặc định loose — child có thể nhỏ hơn space cấp.
Lỗi "Vertical viewport was given unbounded height" — nguyên nhân?
Xem đáp án
Đáp án: Đặt scrollable (ListView, GridView) trong Column không có constraint height. Fix: Expanded, SizedBox fixed height, hoặc shrinkWrap: true.
14. Tổng kết
- ✅ Mental model: Constraints flow down, sizes flow up, parent positions.
- ✅ BoxConstraints: tight / loose, bounded / unbounded.
- ✅ Row / Column + mainAxis / crossAxis alignment.
- ✅ Expanded (tight) / Flexible (loose) / Spacer — trong Flex.
- ✅ Stack + Positioned cho overlap.
- ✅ Container kitchen sink — biết khi tách primitive.
- ✅ Padding, SizedBox, Center, Align, EdgeInsets variants.
- ✅ ConstrainedBox, LimitedBox, FractionallySizedBox, AspectRatio.
- ✅ 3 lỗi phổ biến + fix: overflow, unbounded height, Expanded outside Flex.
- ✅ Debug:
debugPaintSizeEnabled+ Flutter Inspector.
15. Kết nối
- Chương 4 — Theming: apply theme cho layout (color, text, spacing).
- Chương 5 — Navigation: layout per page.
- Chương 9 — Forms: form layout patterns.
- Chương 13 — Custom Paint: RenderObject + constraint deep dive.