Flutter Layout & Responsive UI

1. Flutter Layout
Layout trong Flutter là quá trình xác định kích thước và vị trí của các Widget trên màn hình.
Flutter sử dụng hệ thống Widget để xây dựng giao diện. Các Widget Layout quyết định:
- Widget nằm ở đâu.
- Widget có kích thước bao nhiêu.
- Các Widget được sắp xếp theo chiều ngang hay dọc.
- Khoảng cách giữa các Widget.
- Widget có được phép mở rộng hay không.
Ví dụ:
Column(
children: [
Text('Username'),
TextField(),
ElevatedButton(
onPressed: () {},
child: Text('Login'),
),
],
)
Kết quả:
Username
↓
TextField
↓
Login Button
Flutter sẽ dựa vào cấu trúc Widget Tree để tính toán layout.
2. Cách Flutter Layout hoạt động
Một nguyên tắc cực kỳ quan trọng trong Flutter là:
Constraints go down, sizes go up, parent sets position.
Có thể hiểu:
Parent
↓
Gửi Constraints cho Child
↓
Child xác định Size
↓
Size được trả về Parent
↓
Parent quyết định Position
Ví dụ:
Parent
↓
"Con được phép rộng tối đa 300px"
↓
Child
↓
"OK, tôi cần 200px"
↓
Parent
↓
"Vậy tôi đặt con ở vị trí này"
Đây là nền tảng để hiểu hầu hết các lỗi Layout trong Flutter.
3. Constraints
Constraints là các giới hạn về kích thước mà Widget cha truyền cho Widget con.
Constraints gồm:
minWidth
maxWidth
minHeight
maxHeight
Ví dụ:
minWidth = 0
maxWidth = 400
minHeight = 0
maxHeight = 800
Có nghĩa Widget con được phép có:
Width: 0 → 400
Height: 0 → 800
Widget con chọn kích thước phù hợp trong giới hạn đó.
4. Size
Sau khi nhận Constraints, Widget sẽ xác định Size của mình.
Ví dụ:
Constraints:
Width: 0 → 400
Height: 0 → 800
Widget chọn:
Width = 200
Height = 100
Kết quả Widget có kích thước:
200 × 100
5. Position
Sau khi Widget xác định kích thước, Widget cha quyết định vị trí của Widget.
Ví dụ:
Screen
┌─────────────────────────┐
│ │
│ ┌──────────┐ │
│ │ Button │ │
│ └──────────┘ │
│ │
└─────────────────────────┘
Widget Button có thể có:
Width = 150
Height = 50
Sau đó Center quyết định đặt Button ở chính giữa màn hình.
6. Row
Row dùng để sắp xếp các Widget theo chiều ngang.
Ví dụ:
Row(
children: [
Icon(Icons.home),
Text('Home'),
Icon(Icons.arrow_forward),
],
)
Kết quả:
Home →
Một số thuộc tính quan trọng:
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
...
],
)
7. Main Axis và Cross Axis
Đây là khái niệm rất quan trọng khi làm việc với Row và Column.
Row
Row có hướng chính là ngang.
Main Axis →
Cross Axis ↓
Main Axis
─────────────→
Widget Widget Widget
│
↓
Cross Axis
Column
Column có hướng chính là dọc.
Main Axis ↓
Cross Axis →
Cross Axis →
│
↓
Widget
↓
Widget
↓
Widget
Main Axis
Vì vậy:
Row
→ mainAxis = horizontal
→ crossAxis = vertical
Column
→ mainAxis = vertical
→ crossAxis = horizontal
8. MainAxisAlignment
MainAxisAlignment điều khiển cách các Widget được sắp xếp trên Main Axis.
Các giá trị phổ biến:
MainAxisAlignment.start
MainAxisAlignment.center
MainAxisAlignment.end
MainAxisAlignment.spaceBetween
MainAxisAlignment.spaceAround
MainAxisAlignment.spaceEvenly
Ví dụ:
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Left'),
Text('Right'),
],
)
Kết quả:
Left Right
Các giá trị
start
[A][B][C]
center
[A][B][C]
end
[A][B][C]
spaceBetween
[A] [B] [C]
spaceAround
[A] [B] [C]
spaceEvenly
[A] [B] [C]
9. CrossAxisAlignment
CrossAxisAlignment điều khiển vị trí Widget trên Cross Axis.
Ví dụ:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Username'),
Text('Password'),
],
)
Kết quả:
Username
Password
Các giá trị thường dùng:
start
center
end
stretch
10. Container
Container là Widget thường được sử dụng để:
• Thiết lập width.
• Thiết lập height.
• Padding.
• Margin.
• Background.
• Border.
• Border radius.
• Decoration.
Ví dụ:
Container(
width: 200,
height: 100,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'Hello Flutter',
),
)
Có thể hiểu:
Container
├── Size
├── Padding
├── Decoration
└── Child
11. Padding
Padding dùng để tạo khoảng cách bên trong một Widget.
Padding(
padding: const EdgeInsets.all(16),
child: Text('Hello'),
)
Có thể sử dụng:
EdgeInsets.all(16)
EdgeInsets.symmetric(horizontal: 16)
EdgeInsets.symmetric(vertical: 16)
EdgeInsets.only(
left: 10,
top: 20,
)
12. Margin
Flutter không có thuộc tính margin trực tiếp trong mọi Widget.
Thông thường sử dụng Container:
Container(
margin: const EdgeInsets.all(16),
child: const Text('Hello'),
)
Hoặc sử dụng Padding bên ngoài:
Padding(
padding: const EdgeInsets.all(16),
child: const Text('Hello'),
)
Điểm cần phân biệt:
Padding
→ khoảng cách giữa Widget và nội dung bên trong
Margin
→ khoảng cách bên ngoài Widget
13. SizedBox
SizedBox dùng để:
• Tạo kích thước cố định.
• Tạo khoảng cách.
Ví dụ:
SizedBox(
width: 200,
height: 100,
child: Text('Hello'),
)
Tạo khoảng cách:
Column(
children: [
Text('Username'),
SizedBox(height: 16),
TextField(),
],
)
14. Expanded
Expanded cho phép Widget con chiếm phần không gian còn lại trong Row hoặc Column.
Ví dụ:
Row(
children: [
Expanded(
child: Container(),
),
Expanded(
child: Container(),
),
],
)
Hai Widget sẽ chia không gian:
┌──────────────┬──────────────┐
│ │ │
│ Expanded │ Expanded │
│ │ │
└──────────────┴──────────────┘
Mặc định:
1 : 1
Có thể sử dụng flex:
Row(
children: [
Expanded(
flex: 2,
child: Container(),
),
Expanded(
flex: 1,
child: Container(),
),
],
)
Kết quả:
┌───────────────────┬─────────┐
│ │ │
│ flex: 2 │ flex: 1 │
│ │ │
└───────────────────┴─────────┘
Tỷ lệ:
2 : 1
15. Flexible
Flexible cũng cho phép Widget sử dụng không gian linh hoạt nhưng không bắt buộc Widget phải chiếm toàn bộ phần không gian còn lại.
So sánh:
Expanded
→ bắt buộc child sử dụng phần không gian được cấp
Flexible
→ child có thể nhỏ hơn phần không gian được cấp
Ví dụ:
Row(
children: [
Flexible(
child: Text(
'Một đoạn text rất dài...',
),
),
],
)
Flexible thường hữu ích khi xử lý Text hoặc nội dung có kích thước thay đổi.
16. Spacer
Spacer dùng để tạo khoảng trống linh hoạt trong Row hoặc Column.
Ví dụ:
Row(
children: [
Text('Logo'),
Spacer(),
Text('Profile'),
],
)
Kết quả:
Logo Profile
Spacer thực chất hoạt động dựa trên cơ chế flex.
17. Stack
Stack cho phép các Widget chồng lên nhau.
Ví dụ:
Stack(
children: [
Image.network('image-url'),
const Positioned(
bottom: 10,
left: 10,
child: Text(
'Product',
),
),
],
)
Kết quả:
┌──────────────────────┐
│ │
│ IMAGE │
│ │
│ Product │
└──────────────────────┘
Stack thường được dùng cho:
• Badge.
• Overlay.
• Text trên hình ảnh.
• Floating button.
• Avatar có icon trạng thái.
18. Positioned
Positioned dùng bên trong Stack để xác định vị trí của Widget.
Ví dụ:
Stack(
children: [
Container(),
Positioned(
top: 10,
right: 10,
child: Icon(Icons.favorite),
),
],
)
Có thể sử dụng:
top
bottom
left
right
19. Wrap
Wrap tương tự Row hoặc Column nhưng có thể tự động xuống dòng khi không đủ không gian.
Ví dụ:
Wrap(
spacing: 8,
runSpacing: 8,
children: [
Chip(label: Text('Flutter')),
Chip(label: Text('Dart')),
Chip(label: Text('Firebase')),
Chip(label: Text('UI')),
],
)
Khi không đủ chiều rộng:
Flutter Dart Firebase
UI
Thay vì bị overflow như Row.
20. ListView
ListView dùng để tạo danh sách có thể cuộn.
Ví dụ:
ListView(
children: [
ListTile(
title: Text('Product 1'),
),
ListTile(
title: Text('Product 2'),
),
ListTile(
title: Text('Product 3'),
),
],
)
Đây là Widget rất quan trọng trong ứng dụng thực tế.
Khi danh sách có nhiều dữ liệu, thường sử dụng:
ListView.builder()
Ví dụ:
ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
return ProductCard(
product: products[index],
);
},
)
21. GridView
GridView dùng để hiển thị dữ liệu theo dạng lưới.
Ví dụ:
┌────────┬────────┐
│ Product│ Product│
├────────┼────────┤
│ Product│ Product│
├────────┼────────┤
│ Product│ Product│
└────────┴────────┘
Ví dụ:
GridView.count(
crossAxisCount: 2,
children: [
ProductCard(),
ProductCard(),
ProductCard(),
ProductCard(),
],
)
22. SingleChildScrollView
SingleChildScrollView cho phép một Widget lớn hơn màn hình có thể cuộn.
Ví dụ:
SingleChildScrollView(
child: Column(
children: [
...
],
),
)
Thường sử dụng cho:
• Form.
• Trang thông tin.
• Profile.
• Nội dung có chiều cao thay đổi.
23. SafeArea
SafeArea giúp nội dung không bị che bởi các khu vực đặc biệt của thiết bị như:
• Notch.
• Dynamic Island.
• Status bar.
• Navigation bar.
Ví dụ:
SafeArea(
child: Scaffold(
body: ...
),
)
Trong thực tế cần sử dụng phù hợp với cấu trúc của ứng dụng.
24. Responsive UI
Responsive UI là giao diện có khả năng thích nghi với nhiều kích thước và tỷ lệ màn hình khác nhau.
Ví dụ cùng một ứng dụng chạy trên:
Điện thoại nhỏ
↓
Điện thoại lớn
↓
Tablet
↓
Desktop
Giao diện vẫn phải:
• Không bị Overflow.
• Nội dung không bị cắt.
• Layout phù hợp với kích thước màn hình.
• Khoảng cách hợp lý.
• Số lượng cột có thể thay đổi.
25. Vì sao cần Responsive UI?
Không phải thiết bị nào cũng có cùng kích thước màn hình.
Ví dụ:
Mobile
390 × 844
Tablet
1024 × 1366
Desktop
1920 × 1080
Nếu cố định:
Container(
width: 500,
)
thì trên màn hình nhỏ có thể gây Overflow.
Vì vậy giao diện nên dựa trên không gian có sẵn thay vì phụ thuộc hoàn toàn vào kích thước cố định.
26. MediaQuery
MediaQuery cho phép lấy thông tin về kích thước và đặc điểm của màn hình.
Ví dụ:
final size = MediaQuery.sizeOf(context);
Lấy:
size.width
size.height
Ví dụ:
Container(
width: MediaQuery.sizeOf(context).width * 0.8,
)
Widget có chiều rộng bằng 80% màn hình.
Có thể kiểm tra:
final width = MediaQuery.sizeOf(context).width;
if (width < 600) {
// Mobile
} else {
// Tablet/Desktop
}
27. LayoutBuilder
LayoutBuilder cho phép xây dựng UI dựa trên Constraints mà Parent cung cấp.
Ví dụ:
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 600) {
return MobileLayout();
}
return DesktopLayout();
},
)
Điểm khác biệt quan trọng:
MediaQuery
→ thông tin về toàn bộ màn hình
LayoutBuilder
→ Constraints của khu vực Widget đang nằm
Trong responsive layout phức tạp, LayoutBuilder thường hữu ích hơn việc chỉ dựa vào kích thước màn hình.
28. Breakpoint
Breakpoint là các mốc kích thước được sử dụng để quyết định thay đổi layout.
Ví dụ:
< 600px
→ Mobile
600px – 1024px
→ Tablet
> 1024px
→ Desktop
Ví dụ:
if (width < 600) {
return MobileLayout();
} else if (width < 1024) {
return TabletLayout();
} else {
return DesktopLayout();
}
Breakpoint không phải quy tắc bắt buộc của Flutter. Đây là cách tổ chức responsive UI tùy theo yêu cầu của ứng dụng.
29. Responsive Navigation
Một ứng dụng có thể thay đổi Navigation dựa trên kích thước màn hình.
Mobile:
┌─────────────────────┐
│ │
│ Content │
│ │
├─────────────────────┤
│ Home Search Profile │
└─────────────────────┘
Desktop:
┌──────────┬───────────────┐
│ │ │
│ Home │ │
│ Search │ Content │
│ Profile │ │
│ │ │
└──────────┴───────────────┘
Mobile có thể sử dụng BottomNavigationBar hoặc NavigationBar.
Desktop có thể sử dụng NavigationRail hoặc Sidebar.
30. Responsive Grid
Ví dụ ứng dụng bán hàng.
Mobile:
┌────────┬────────┐
│ Product│ Product│
├────────┼────────┤
│ Product│ Product│
└────────┴────────┘
Tablet:
┌──────┬──────┬──────┐
│ P1 │ P2 │ P3 │
├──────┼──────┼──────┤
│ P4 │ P5 │ P6 │
└──────┴──────┴──────┘
Desktop:
┌────┬────┬────┬────┬────┐
│ P1 │ P2 │ P3 │ P4 │ P5 │
├────┼────┼────┼────┼────┤
│ P6 │ P7 │ P8 │ P9 │ P10│
└────┴────┴────┴────┴────┘
Có thể thay đổi crossAxisCount dựa trên width.
31. Tránh sử dụng kích thước cố định quá nhiều
Không nên lạm dụng:
Container(
width: 350,
height: 700,
)
vì kích thước này có thể phù hợp trên một thiết bị nhưng không phù hợp trên thiết bị khác.
Thay vào đó nên sử dụng:
Expanded
Flexible
FractionallySizedBox
LayoutBuilder
MediaQuery
Wrap
Flexible Grid
tùy trường hợp.
32. Overflow
Overflow xảy ra khi nội dung lớn hơn không gian mà Parent cho phép.
Ví dụ:
Row(
children: [
Text(
'Một đoạn text cực kỳ dài...',
),
Text(
'Another text',
),
],
)
Nếu tổng chiều rộng lớn hơn màn hình, Flutter có thể báo:
A RenderFlex overflowed by XX pixels
Đây là lỗi rất phổ biến khi mới học Flutter.
Một cách xử lý:
Row(
children: [
Expanded(
child: Text(
'Một đoạn text rất dài...',
),
),
],
)
33. Các Widget Layout quan trọng cần nhớ
Container
→ Container và Decoration
Row
→ Layout ngang
Column
→ Layout dọc
Stack
→ Chồng Widget
Wrap
→ Tự động xuống dòng
Expanded
→ Chiếm không gian còn lại
Flexible
→ Kích thước linh hoạt
Spacer
→ Tạo khoảng trống linh hoạt
SizedBox
→ Kích thước / khoảng cách
Padding
→ Khoảng cách bên trong
ListView
→ Danh sách cuộn
GridView
→ Danh sách dạng lưới
SingleChildScrollView
→ Cuộn nội dung
SafeArea
→ Tránh vùng hệ thống
34. Quy tắc Layout quan trọng
Có thể ghi nhớ quy tắc cốt lõi của Flutter:
Constraints go down
↓
Sizes go up
↓
Parent sets position
Nghĩa là:
Parent
→ đưa Constraints xuống Child
Child
→ chọn Size trong Constraints
Parent
→ quyết định vị trí của Child
Nếu hiểu được quy tắc này thì việc debug các lỗi Row, Column, Expanded, ListView, Container và Overflow sẽ dễ hơn rất nhiều.
35. Tóm tắt
Flutter Layout là hệ thống dùng để xác định kích thước và vị trí của Widget.
Các Widget Layout quan trọng:
Row
→ ngang
Column
→ dọc
Stack
→ chồng
Wrap
→ xuống dòng
Expanded
→ chiếm phần còn lại
Flexible
→ linh hoạt
SizedBox
→ kích thước / khoảng cách
Padding
→ khoảng cách bên trong
ListView
→ danh sách cuộn
GridView
→ dạng lưới
Responsive UI là khả năng giao diện thích nghi với nhiều kích thước màn hình.
Các công cụ quan trọng:
MediaQuery
→ lấy thông tin màn hình
LayoutBuilder
→ xây dựng UI dựa trên Constraints
Breakpoint
→ thay đổi layout theo kích thước
Expanded / Flexible
→ sử dụng không gian linh hoạt
Wrap
→ tự động xuống dòng
GridView
→ thay đổi số cột theo kích thước
Điểm quan trọng nhất cần nhớ:
Flutter Layout hoạt động dựa trên Constraints → Size → Position.
Responsive UI không phải là làm một giao diện có kích thước cố định cho mọi thiết bị, mà là thiết kế UI có khả năng thích nghi với không gian hiển thị khác nhau.






Khoá học lập trình game con rắn cho trẻ em




