
Chào các "coder hệ gen Z"! Hôm nay, "giáo sư Creyt" sẽ cùng các bạn "đào sâu" một khái niệm nghe thì đơn giản nhưng lại cực kỳ quan trọng trong Flutter: Window. Nghe tên thì giống cái cửa sổ bạn hay mở trên máy tính đúng không? Nhưng trong Flutter, nó lại mang một ý nghĩa "deep" hơn nhiều, và thường thì bạn sẽ không "đụng chạm" trực tiếp vào nó đâu. Hãy cùng bật đèn pin và khám phá nhé!
1. Window trong Flutter là gì? Để làm gì? (Giải thích kiểu Gen Z)
Nói một cách dễ hiểu, Window trong Flutter (cụ thể là đối tượng Window từ thư viện dart:ui) giống như cái "khung canvas" hay "khung hình chiếu" mà ứng dụng của bạn đang "được vẽ" lên vậy. Nó không phải là một Widget mà bạn "kéo thả" hay nhìn thấy rõ ràng trên màn hình. Nó là cái bề mặt vật lý mà hệ điều hành cấp cho ứng dụng của bạn để hiển thị mọi thứ.
Tưởng tượng: Ứng dụng của bạn là một bộ phim hoạt hình "siêu cấp cute". Window chính là cái màn hình chiếu phim khổng lồ mà bộ phim đó đang được trình chiếu. Nó cung cấp những thông tin "thô ráp" nhất về cái màn hình đó: kích thước thực tế (tính bằng pixel), mật độ điểm ảnh (devicePixelRatio), hay những khu vực bị "chiếm đóng" bởi thanh trạng thái, thanh điều hướng của hệ điều hành (padding, viewInsets).
Vậy nó để làm gì? Nó là nguồn dữ liệu gốc, cung cấp cho Flutter biết "khung cảnh" mà nó đang hoạt động trông như thế nào. Từ những dữ liệu "thô" này, Flutter mới có thể tính toán và vẽ các Widget của bạn một cách chính xác. Tuy nhiên, ít khi bạn tương tác trực tiếp với nó, bởi vì Flutter đã có một "phiên dịch viên" cực kỳ thân thiện và thông minh mang tên MediaQuery rồi!
MediaQuery giống như một "hướng dẫn viên du lịch" cực kỳ nhiệt tình. Thay vì bạn phải tự mình đọc bản đồ kỹ thuật chi tiết (Window) với hàng tá thông số pixel lằng nhằng, MediaQuery sẽ "dịch" những thông tin đó sang một ngôn ngữ dễ hiểu hơn, dễ dùng hơn cho các Widget của bạn. Nó còn "tự động cập nhật" khi màn hình xoay, bàn phím bật lên, hay có bất kỳ thay đổi nào về "khung cảnh" đó nữa chứ!
2. Code Ví Dụ Minh Họa Rõ Ràng
Để bạn thấy sự khác biệt giữa việc "đụng" trực tiếp Window và dùng MediaQuery, hãy xem ví dụ này:
import 'package:flutter/material.dart';
import 'dart:ui' as ui; // Import dart:ui để truy cập đối tượng Window
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Window Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
// Khai báo để có thể theo dõi sự kiện thay đổi của Window
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
// Phương thức này sẽ được gọi khi có sự thay đổi về cấu hình (ví dụ: xoay màn hình, bàn phím hiện lên)
@override
void didChangeMetrics() {
setState(() {
// Rebuild UI để cập nhật thông tin Window
});
}
@override
Widget build(BuildContext context) {
// --- Lấy thông tin từ MediaQuery (cách phổ biến và được khuyến nghị) ---
final mediaQueryData = MediaQuery.of(context);
final screenWidthLogical = mediaQueryData.size.width;
final screenHeightLogical = mediaQueryData.size.height;
final safeAreaTop = mediaQueryData.padding.top;
final safeAreaBottom = mediaQueryData.padding.bottom;
final viewInsetsBottom = mediaQueryData.viewInsets.bottom; // Thường là chiều cao bàn phím
// --- Lấy thông tin từ Window (cách thấp cấp, ít dùng trực tiếp) ---
final ui.Window window = WidgetsBinding.instance.window;
final screenWidthPixels = window.physicalSize.width;
final screenHeightPixels = window.physicalSize.height;
final devicePixelRatio = window.devicePixelRatio;
final windowPaddingTop = window.padding.top; // Raw pixels
final windowViewInsetsBottom = window.viewInsets.bottom; // Raw pixels
return Scaffold(
appBar: AppBar(
title: const Text('Window vs. MediaQuery'),
),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Thông tin từ MediaQuery (Logical Pixels):',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text('Chiều rộng màn hình: ${screenWidthLogical.toStringAsFixed(2)} dp'),
Text('Chiều cao màn hình: ${screenHeightLogical.toStringAsFixed(2)} dp'),
Text('Vùng an toàn trên (notch): ${safeAreaTop.toStringAsFixed(2)} dp'),
Text('Vùng an toàn dưới: ${safeAreaBottom.toStringAsFixed(2)} dp'),
Text('Chiều cao bàn phím (viewInsets.bottom): ${viewInsetsBottom.toStringAsFixed(2)} dp'),
const Divider(height: 32),
Text(
'Thông tin từ Window (Raw Physical Pixels):',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text('Chiều rộng vật lý: ${screenWidthPixels.toStringAsFixed(2)} px'),
Text('Chiều cao vật lý: ${screenHeightPixels.toStringAsFixed(2)} px'),
Text('Device Pixel Ratio: ${devicePixelRatio.toStringAsFixed(2)}'),
Text('Vùng an toàn trên (raw): ${windowPaddingTop.toStringAsFixed(2)} px'),
Text('Chiều cao bàn phím (raw): ${windowViewInsetsBottom.toStringAsFixed(2)} px'),
const Divider(height: 32),
Text(
'Lưu ý: Bạn sẽ thấy giá trị từ Window (px) = giá trị từ MediaQuery (dp) * devicePixelRatio',
style: const TextStyle(fontStyle: FontStyle.italic),
),
],
),
),
),
);
}
}
Giải thích:
MediaQuery.of(context): Đây là cách chuẩn để lấy thông tin về "khung hình" của bạn. Nó trả vềMediaQueryDatavới các giá trị đã được tính toán ở đơn vị logical pixels (dp), tự động điều chỉnh theodevicePixelRatiođể UI của bạn trông nhất quán trên mọi thiết bị. Nó còn tự động "lắng nghe" các thay đổi (như xoay màn hình, bàn phím bật lên) và kích hoạt rebuild Widget để UI của bạn luôn được cập nhật.WidgetsBinding.instance.window: Đây là cách bạn "chạm" vào đối tượngWindowgốc. Nó cung cấp các giá trị ở đơn vị physical pixels (px) và không tự động kích hoạt rebuild Widget khi có thay đổi. Bạn phải tự implementWidgetsBindingObservervàdidChangeMetrics()để lắng nghe sự kiện thay đổi, giống như trong ví dụ. Thấy "rắc rối" hơn hẳn đúng không?

3. Mẹo (Best Practices) để ghi nhớ hoặc dùng thực tế
-
"Bạn bè" của bạn là
MediaQuery, không phảiWindow: Hầu hết 99.9% thời gian, bạn nên dùngMediaQuery.of(context)để lấy thông tin về kích thước màn hình, vùng an toàn, hay trạng thái bàn phím. Nó thân thiện, dễ dùng, và quan trọng nhất là reactive (tự động cập nhật UI khi có thay đổi).
-
Windowchỉ dành cho "hacker" cấp cao: Chỉ khi bạn đang làm những thứ rất "low-level" như tạo một custom render engine, hay cần những giá trị pixel thô để tính toán một cách cực kỳ chính xác màMediaQuerykhông đáp ứng được, bạn mới nghĩ đếnWindow. Còn không, "tránh xa" nó ra cho lành! -
Hiểu về
dpvàpx:MediaQuerycho bạn giá trịdp(density-independent pixels), là đơn vị mà bạn nên dùng để thiết kế UI.Windowcho bạn giá trịpx(physical pixels), là số điểm ảnh thực tế trên màn hình. Mối quan hệ làpx = dp * devicePixelRatio. -
Sử dụng
MediaQuery.removePadding/MediaQuery.removeViewInsets: Đôi khi bạn muốn Widget của mình "tràn" ra cả vùng an toàn (ví dụ, một tấm ảnh nền). Bạn có thể bọc Widget đó trong mộtMediaQuerymới với các giá trịpaddinghoặcviewInsetsbằng 0 để bỏ qua các vùng này.// Ví dụ bỏ qua padding trên cùng (thanh trạng thái) MediaQuery.removePadding( context: context, removeTop: true, child: ListView( // Nội dung của bạn sẽ tràn lên cả vùng thanh trạng thái ), )
4. Ứng dụng thực tế các ứng dụng/website đã ứng dụng
- Thiết kế Responsive (Mọi ứng dụng Flutter): Bất kỳ ứng dụng Flutter nào cũng dùng
MediaQueryđể điều chỉnh layout cho phù hợp với kích thước màn hình khác nhau (điện thoại, tablet, web, desktop). Ví dụ, một ứng dụng chat sẽ hiển thị danh sách cuộc trò chuyện toàn màn hình trên điện thoại, nhưng trên tablet nó có thể chia đôi màn hình: danh sách bên trái, nội dung chat bên phải. - Xử lý vùng an toàn (Safe Area) (Instagram, TikTok): Các ứng dụng có giao diện tràn viền trên các điện thoại có "tai thỏ" (notch) hoặc "đục lỗ" đều phải dùng
MediaQueryđể đảm bảo nội dung không bị che khuất bởi các phần cứng này.SafeAreaWidget chính là một "sản phẩm" củaMediaQuery. - Xử lý bàn phím ảo (Zalo, Messenger): Khi bàn phím ảo hiện lên,
MediaQuery.of(context).viewInsets.bottomsẽ cho bạn biết chiều cao của bàn phím. Các ứng dụng chat thường dùng thông tin này để đẩy khung nhập liệu lên trên, tránh bị bàn phím che mất. - Game hoặc ứng dụng đồ họa chuyên sâu: Một số game hoặc ứng dụng cần kiểm soát pixel cực kỳ chính xác (ví dụ, vẽ trực tiếp lên canvas) có thể sẽ phải "đụng" đến
Windowđể lấy kích thước pixel thô, nhưng trường hợp này rất hiếm trong phát triển ứng dụng thông thường.
5. Thử nghiệm đã từng và hướng dẫn nên dùng cho case nào
Với kinh nghiệm của Creyt, tôi đã từng "nghịch" với Window trực tiếp khi muốn làm một số hiệu ứng đồ họa "khó nhằn" đòi hỏi sự chính xác tuyệt đối về pixel. Nhưng tin tôi đi, đó là một hành trình "đau khổ" và không cần thiết cho 99% các dự án Flutter thông thường.
Bạn NÊN dùng MediaQuery khi:
- Bạn muốn ứng dụng của mình "responsive": Tức là nó "tự động đẹp" trên mọi kích thước màn hình, từ điện thoại nhỏ đến tablet lớn, hay cả trên web.
- Bạn cần biết kích thước màn hình hiện tại (logical pixels): Dùng
MediaQuery.of(context).size. - Bạn cần biết về vùng an toàn (safe area): Để tránh nội dung bị cắt bởi notch, thanh trạng thái, thanh điều hướng. Dùng
MediaQuery.of(context).paddinghoặc đơn giản hơn là bọc Widget trongSafeArea. - Bạn muốn điều chỉnh UI khi bàn phím ảo hiện lên/ẩn đi: Dùng
MediaQuery.of(context).viewInsets.bottom. - Bạn cần biết mật độ điểm ảnh của thiết bị (
devicePixelRatio): DùngMediaQuery.of(context).devicePixelRatio(mặc dù cái này cũng có trongWindow, nhưngMediaQuerytiện hơn).
Bạn CHỈ NÊN dùng Window (từ dart:ui) khi:
- Bạn đang phát triển một thư viện rất thấp cấp hoặc một render engine tùy chỉnh.
- Bạn cần truy cập các giá trị pixel thô mà không muốn qua lớp trừu tượng của
MediaQuery. (Rất hiếm!) - Bạn muốn lắng nghe các sự kiện thay đổi của Window một cách thủ công và tự xử lý việc rebuild UI. (Thường thì không ai muốn làm vậy cả,
MediaQueryđã làm hộ rồi).
Kết luận: Hãy xem MediaQuery là người bạn thân, còn Window là một "người anh lớn" trầm tính, ít khi xuất hiện nhưng lại là nền tảng cho mọi thứ. Hiểu được cả hai sẽ giúp bạn làm chủ "khung hình" của ứng dụng Flutter một cách "pro" nhất. Chúc các bạn code vui vẻ và luôn "on top"!
Thuộc Series: Flutter
Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!