Làm chủ bố cục Flutter với Row, Column, Expanded và Stack

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Phần lớn thời gian làm giao diện Flutter là sắp xếp các widget vào đúng chỗ. Chỉ cần nắm vững Row, Column, Expanded và Stack, bạn đã dựng được đa số màn hình thường gặp. Hãy cùng xem từng thành phần cùng quy tắc ràng buộc kích thước phía sau chúng.

Row và Column

Row xếp các con theo chiều ngang, Column xếp theo chiều dọc. Hai widget này có hai trục: trục chính (main axis) là chiều xếp, trục phụ (cross axis) vuông góc với nó.

Column(
  mainAxisAlignment: MainAxisAlignment.center,
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    const Text('Tiêu đề'),
    const SizedBox(height: 8),
    const Text('Mô tả ngắn'),
    Row(
      children: [
        const Icon(Icons.star),
        const SizedBox(width: 4),
        const Text('4.5'),
      ],
    ),
  ],
)
  • mainAxisAlignment điều khiển cách phân bố dọc theo trục chính (start, center, spaceBetween...).
  • crossAxisAlignment điều khiển căn chỉnh theo trục phụ.
  • SizedBox là cách đơn giản để tạo khoảng cách giữa các widget.

Expanded và Flexible

Khi muốn một widget chiếm phần không gian còn lại trong Row hoặc Column, hãy bọc nó bằng Expanded. Nếu có nhiều Expanded, tham số flex quyết định tỉ lệ.

Row(
  children: [
    Expanded(flex: 2, child: Container(color: Colors.blue, height: 50)),
    Expanded(flex: 1, child: Container(color: Colors.orange, height: 50)),
  ],
)

Ở đây khối xanh chiếm hai phần, khối cam một phần. Flexible tương tự nhưng cho phép widget nhỏ hơn không gian được cấp, còn Expanded ép nó lấp đầy.

Stack: xếp chồng lên nhau

Stack đặt các widget chồng lên nhau, rất hữu ích cho huy hiệu số lượng trên biểu tượng, chữ đè lên ảnh hoặc nút nổi. Dùng Positioned để đặt con ở vị trí cụ thể so với Stack.

Stack(
  children: [
    Image.network('https://example.com/cover.jpg'),
    Positioned(
      right: 8,
      bottom: 8,
      child: Container(
        padding: const EdgeInsets.all(6),
        color: Colors.black54,
        child: const Text('Mới', style: TextStyle(color: Colors.white)),
      ),
    ),
  ],
)

Quy tắc ràng buộc kích thước

Flutter tuân theo nguyên tắc: ràng buộc đi xuống, kích thước đi lên, cha quyết định vị trí. Nghĩa là widget cha đưa ra khoảng kích thước cho phép, widget con chọn kích thước trong khoảng đó rồi báo lên. Hiểu quy tắc này giúp bạn giải thích được phần lớn lỗi bố cục.

Lỗi bố cục thường gặp

  1. RenderFlex overflowed: nội dung vượt quá không gian. Giải pháp: bọc bằng Expanded, dùng SingleChildScrollView hoặc rút ngắn nội dung.
  2. Expanded trong Column không giới hạn chiều cao: thường do Column nằm trong ListView. Hãy cho nó chiều cao cụ thể hoặc dùng shrinkWrap hợp lý.
  3. Text bị tràn: dùng overflow: TextOverflow.ellipsis và maxLines.

Mẹo thực hành

  • Dùng Flutter DevTools để bật Widget Inspector và xem cây widget trực quan.
  • Tách widget lớn thành các widget nhỏ có tên rõ ràng.
  • Dùng Padding, SizedBox thay cho việc lồng Container chỉ để tạo khoảng cách.
  • Thử xoay màn hình và đổi cỡ chữ hệ thống để kiểm tra bố cục.

Kết luận

Row, Column, Expanded và Stack là bộ bốn cơ bản. Khi bạn đọc được thông báo lỗi tràn và hiểu cách ràng buộc truyền xuống, việc dựng giao diện sẽ trở nên nhẹ nhàng hơn nhiều. Hãy thử sao chép bố cục của một ứng dụng quen thuộc để luyện tập.

Bài tập: dựng một thẻ sản phẩm

Hãy luyện bằng cách dựng thẻ sản phẩm gồm ảnh bên trái, tên và giá bên phải, kèm biểu tượng yêu thích ở góc. Bài tập này dùng đủ Row, Column, Expanded và Stack:

Card(
  child: Padding(
    padding: const EdgeInsets.all(12),
    child: Row(
      children: [
        Stack(
          children: [
            Container(width: 80, height: 80, color: Colors.grey.shade300),
            const Positioned(
              top: 4,
              right: 4,
              child: Icon(Icons.favorite_border, size: 18),
            ),
          ],
        ),
        const SizedBox(width: 12),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: const [
              Text('Tên sản phẩm khá dài sẽ được cắt bớt',
                  maxLines: 2, overflow: TextOverflow.ellipsis),
              SizedBox(height: 4),
              Text('199.000 đ'),
            ],
          ),
        ),
      ],
    ),
  ),
)

Hãy thử thu nhỏ màn hình, đổi cỡ chữ hệ thống lớn lên và xem bố cục có tràn không. Nếu tràn, bạn vừa có cơ hội thực hành đúng kỹ năng đọc ràng buộc kích thước đã nói ở trên. Sau đó thử biến thẻ này thành một widget riêng có tham số để tái sử dụng trong ListView.

Khoảng cách, căn lề và những widget hỗ trợ

Ngoài bốn widget chính, có vài widget nhỏ giúp bố cục gọn hơn. Padding thêm khoảng đệm bên trong, SizedBox tạo khoảng trống cố định hoặc ép kích thước, Align và Center căn vị trí con trong vùng cha, Spacer chiếm phần trống linh hoạt giữa các con của Row hoặc Column. Wrap hữu ích khi các phần tử cần tự xuống dòng, ví dụ nhóm thẻ nhãn có độ dài khác nhau. ConstrainedBox đặt chiều rộng tối đa để nội dung không giãn quá mức trên màn hình lớn.

Một lời khuyên là hạn chế dùng Container cho mọi thứ. Container tiện nhưng gói nhiều chức năng; nếu chỉ cần khoảng đệm hãy dùng Padding, nếu chỉ cần kích thước hãy dùng SizedBox. Code sẽ rõ ý hơn và người đọc biết ngay mục đích của từng lớp bao.

Để luyện bố cục qua dự án thực tế có hướng dẫn, bạn có thể xem các khóa học trên codebem.com.

author_photo
Nguyễn Đình Cường

0 comment