Một màn hình desktop rộng nhưng card đang nằm trong sidebar hẹp. Nếu card dùng chiều rộng cửa sổ để quyết định hiển thị hai cột, nó có thể chọn layout không vừa chỗ được cấp. Vấn đề nằm ở câu hỏi đang đo: toàn vùng app hay vùng cha dành cho component.
Bài đối chiếu Flutter API ngày 29/10/2026. Ví dụ dùng Material, LayoutBuilder và MediaQuery.sizeOf; cần SDK có các API này. Code được rà theo tài liệu, chưa chạy Flutter build, widget test hoặc thiết bị. Các tình huống bên dưới là checklist để bạn kiểm tra trên dự án của mình.
Hai phép đo phục vụ hai phạm vi
MediaQuery.sizeOf(context) lấy size từ MediaQuery gần context nhất. Trong cây app thông thường, nó hữu ích khi chọn cấu trúc theo kích thước cửa sổ. Nó không hứa trả chiều rộng thực tế của một card sau Padding, sidebar hoặc SizedBox.
LayoutBuilder cung cấp constraints do cha truyền vào lúc layout. Với một component cần quyết định theo chỗ còn lại, constraints.maxWidth thường là dữ kiện phù hợp hơn. Constraints là giới hạn được phép, chưa phải phép đo kích thước cuối cùng của mọi widget con.
Hướng dẫn adaptive của Flutter phân biệt phạm vi cửa sổ và phạm vi cục bộ này. Hãy xác định component có thể được đặt ở đâu trước khi chọn API.
Tạo một vùng hẹp có chủ đích
Thay nội dung lib/main.dart trong project thử nghiệm bằng ví dụ sau. Ngưỡng 600 là lựa chọn minh họa cho hai panel, không phải quy tắc bắt buộc cho mọi tablet hoặc mọi ứng dụng.
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: DemoPage()));
class DemoPage extends StatelessWidget {
const DemoPage({super.key});
@override
Widget build(BuildContext context) {
final windowWidth = MediaQuery.sizeOf(context).width;
return Scaffold(
appBar: AppBar(title: const Text('Layout theo vùng cha')),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Cửa sổ: ${windowWidth.toStringAsFixed(0)}'),
const SizedBox(height: 12),
const Align(
alignment: Alignment.centerLeft,
child: SizedBox(width: 360, child: SummaryPanels()),
),
],
),
),
),
);
}
}
class SummaryPanels extends StatelessWidget {
const SummaryPanels({super.key});
Widget panel(String title) => Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(title),
),
);
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
final wide = constraints.maxWidth >= 600;
final first = panel('Thông tin đơn hàng');
final second = panel('Ghi chú giao hàng có thể dài hơn dự kiến');
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Vùng cha: ${constraints.maxWidth.toStringAsFixed(0)}'),
if (wide)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: first),
const SizedBox(width: 12),
Expanded(child: second),
],
)
else ...[
first,
const SizedBox(height: 12),
second,
],
],
);
});
}
}
Ở cửa sổ rộng, SizedBox vẫn chỉ đề nghị rộng 360 và chịu giới hạn từ cha. SummaryPanels vì vậy có vùng hẹp để chọn một cột. Trong code, MediaQuery chỉ dùng để hiển thị thông tin so sánh, không điều khiển nhánh layout của panel.
Kiểm tra sự khác biệt
Đầu tiên, mở cửa sổ rộng hơn 600 và giữ nguyên wrapper 360. Hai con số hiển thị cần được đọc như hai phạm vi khác nhau; đừng mặc định chúng phải bằng nhau.
Tiếp theo, thay toàn bộ widget Align trong children bằng const SummaryPanels(). Column ngoài đang stretch theo chiều ngang nên component nhận vùng rộng của phần nội dung, sau padding. Khi vùng ấy đạt 600, nhánh hai panel theo hàng mới được chọn.
Cuối cùng, thu hẹp cửa sổ để vùng nội dung xuống dưới 600. Kiểm tra panel quay về một cột, chữ dài vẫn đọc được, và có thể cuộn khi chiều cao không đủ. Thử sát hai phía của ngưỡng để phát hiện việc đặt breakpoint trên sai lớp Padding.
Constraints cần có giới hạn phù hợp
Ví dụ dựa vào chiều ngang hữu hạn do màn hình truyền xuống. Nếu đưa SummaryPanels vào vùng cuộn ngang không ràng buộc chiều rộng, maxWidth có thể vô hạn; so với 600 lúc ấy không còn giải quyết được bố cục. Cần cấp một chiều rộng hữu hạn ở cha trước, thay vì thêm Expanded ngẫu nhiên để dập lỗi.
Theo LayoutBuilder API, builder chạy khi các điều kiện layout hoặc dependency liên quan thay đổi; không nên dùng nó để ghi database hay phát request mỗi lần dựng cây. Giữ quyết định layout là phép tính từ constraints và dữ liệu hiện có.
Lỗi thường gặp và giới hạn
- Dùng chiều rộng cửa sổ để đo card lồng trong vùng nhỏ.
- Gọi maxWidth là chiều rộng cuối cùng đã render của mọi con.
- Đặt ngưỡng 600 mà không thử nội dung thật, chữ lớn hoặc bản dịch dài.
- Chỉ kiểm tra điện thoại toàn màn hình, bỏ qua cửa sổ nhỏ và bố cục chia cột.
MediaQuery.sizeOf API vẫn phù hợp khi quyết định thực sự thuộc về toàn vùng MediaQuery. Với component tái sử dụng, bắt đầu từ phần không gian cha cấp sẽ giúp nó thích nghi đúng khi chuyển sang sidebar, dialog hoặc vùng nội dung khác.

