Flutter Layout & Responsive UI

Responsive Design: definition and examples | Uxcel

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.

Bài viết khác

StatelessWidget vs StatefulWidget

StatelessWidget vs StatefulWidget 1. Khái niệm Trong Flutter, StatelessWidget và StatefulWidget là hai loại Widget cơ bản dùng để xây dựng giao diện. Điểm khác biệt quan trọng nhất nằm ở State (trạng thái). StatelessWidget: Widget không có State nội bộ có thể thay đổi. StatefulWidget: Widget có State nội bộ có thể thay đổi trong quá […]

Flutter Widget Fundamentals

Flutter Widget Fundamentals 1. Widget Trong Flutter, Widget là thành phần cơ bản dùng để xây dựng giao diện người dùng (UI). Có thể hiểu đơn giản: Mọi thành phần xuất hiện trên giao diện Flutter đều được xây dựng từ Widget. Ví dụ: Text → hiển thị văn bản. Image → hiển thị hình […]

Isolate & Concurrency

Isolate & Concurrency Isolate và Concurrency (đồng thời) là cơ chế trong Dart dùng để thực hiện nhiều công việc mà không làm một công việc phải chờ hoàn thành hoàn toàn mới có thể xử lý công việc khác. Lý thuyết 1. Concurrency Concurrency (xử lý đồng thời) là khả năng chương trình quản […]

Null Safety trong Dart

Null Safety là cơ chế phân biệt rõ ràng giữa kiểu dữ liệu có thể chứa null (Nullable) và kiểu dữ liệu không thể chứa null (Non-nullable) ngay ở mức ngôn ngữ lập trình, giúp trình biên dịch phát hiện và bắt lỗi liên quan đến null ngay trong quá trình viết mã thay vì […]

Test Scenario(Kịch bản kiểm thử)

1. Lời mở đầu và vị trí chiến lược của Test Scenario trong quy trình kiểm thử: Trong bức tranh tổng thể của vòng đời kiểm thử phần mềm (STLC), sau khi đã hoàn thành việc phân tích yêu cầu và lập kế hoạch kiểm thử (Test Plan), công việc tiếp theo mang tính chất […]

Dart OOP

OOP là từ viết tắt của cụm từ Object Oriented Programming. Nó có nghĩa là lập trình hướng đối tượng. Đây là một phương pháp lập trình dựa trên những khái niệm về đối tượng và lớp. OOP thường tập trung vào những đối tượng thao tác hơn là tập trung vào logic để có […]

Leave a Reply

Your email address will not be published. Required fields are marked *