1. Widget Configuration và Element Reconciliation
1.1. Khái niệm Widget Configuration
Trong Flutter, Widget là đối tượng mô tả cấu hình giao diện người dùng. Nó xác định những thuộc tính như màu sắc, kích thước, nội dung văn bản, cách bố trí và hành vi của một thành phần giao diện.
Tuy nhiên, Widget không trực tiếp quản lý toàn bộ quá trình hiển thị giao diện trên màn hình. Flutter sử dụng thêm Element và RenderObject để duy trì trạng thái cấu trúc giao diện, xử lý bố cục và thực hiện quá trình vẽ.
Widget Configuration có thể hiểu là tập hợp các thông tin cấu hình được sử dụng để mô tả giao diện tại một thời điểm nhất định. Khi dữ liệu thay đổi, Flutter có thể tạo ra Widget mới với cấu hình mới rồi so sánh nó với cấu hình trước đó để quyết định cách cập nhật giao diện.
Ví dụ, một nút bấm được mô tả bằng Widget ElevatedButton:
ElevatedButton(
onPressed: () {},
child: const Text('Đăng nhập'),
)
Cấu hình của nút bao gồm:
-
Loại Widget được sử dụng.
-
Hàm xử lý sự kiện
onPressed. -
Widget con hiển thị nội dung.
-
Các thuộc tính giao diện như màu sắc, kiểu chữ, hình dạng và khoảng cách.
Khi nội dung nút chuyển từ Đăng nhập thành Đăng ký, Flutter có thể tạo ra một cấu hình Widget mới. Hệ thống sẽ kiểm tra cấu hình mới với cấu hình cũ để xác định cách cập nhật phù hợp.
Widget thường được thiết kế theo nguyên tắc bất biến (immutable). Điều này có nghĩa là sau khi một đối tượng Widget được tạo, các thuộc tính của nó không thể bị thay đổi trực tiếp. Thay vì sửa đổi Widget cũ, Flutter thường tạo Widget mới để mô tả trạng thái giao diện mới.
Cơ chế này giúp việc xây dựng giao diện trở nên dễ dự đoán, giảm nguy cơ thay đổi dữ liệu không kiểm soát và hỗ trợ Flutter tối ưu hóa quá trình cập nhật giao diện.
1.2. Ba thành phần cốt lõi trong hệ thống giao diện Flutter
Flutter sử dụng ba cấu trúc có quan hệ mật thiết với nhau: Widget Tree, Element Tree và RenderObject Tree.
Widget Tree
Widget Tree là cây cấu trúc được tạo thành từ các Widget. Mỗi Widget mô tả một thành phần giao diện hoặc một quy tắc bố trí giao diện.
Ví dụ:
MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Trang chủ'),
),
body: const Center(
child: Text('Xin chào Flutter'),
),
),
)
Cấu trúc Widget Tree tương ứng gồm:
-
MaterialAppquản lý cấu hình ứng dụng Material. -
Scaffoldcung cấp cấu trúc trang. -
AppBartạo thanh tiêu đề. -
Centercăn giữa Widget con. -
Texthiển thị nội dung văn bản.
Widget Tree mô tả giao diện mong muốn nhưng không trực tiếp lưu giữ toàn bộ trạng thái hoạt động lâu dài của giao diện.
Element Tree
Element Tree chứa các đối tượng Element tương ứng với những vị trí của Widget trong cấu trúc giao diện. Element đóng vai trò liên kết Widget với quá trình quản lý giao diện thực tế.
Element có thể tồn tại qua nhiều lần xây dựng lại giao diện, trong khi các Widget cấu hình tương ứng có thể được tạo mới.
Các nhiệm vụ quan trọng của Element bao gồm:
-
Duy trì vị trí của thành phần trong cây giao diện.
-
Liên kết Widget hiện tại với Element tương ứng.
-
Quản lý quan hệ cha – con.
-
Hỗ trợ cập nhật cấu hình Widget.
-
Quản lý vòng đời của
Stateđối vớiStatefulWidget. -
Phối hợp với hệ thống xây dựng lại giao diện.
Chính Element Tree giúp Flutter tránh việc phải hủy bỏ và tạo lại toàn bộ cấu trúc giao diện mỗi khi dữ liệu thay đổi.
RenderObject Tree
RenderObject Tree chịu trách nhiệm xử lý những vấn đề liên quan đến hình học và hiển thị giao diện.
RenderObject có thể tham gia vào các nhiệm vụ:
-
Đo đạc kích thước thành phần.
-
Xác định vị trí của thành phần.
-
Thực hiện bố cục.
-
Vẽ nội dung.
-
Quản lý vùng hiển thị và các thông tin phục vụ quá trình render.
Không phải mọi Widget đều tạo ra một RenderObject riêng. Một số Widget chỉ cung cấp cấu hình, tổ chức cây hoặc truyền thông tin xuống các thành phần khác.
Ví dụ, Padding có thể sử dụng RenderObject để tạo khoảng cách xung quanh nội dung, trong khi StatelessWidget tùy chỉnh thường chỉ tổ chức các Widget con và không trực tiếp tạo RenderObject riêng.
1.3. Cơ chế Element Reconciliation
Element Reconciliation là quá trình Flutter đối chiếu cấu hình Widget mới với cấu hình đang được sử dụng trong cây giao diện.
Khi một phương thức build() chạy lại, các Widget mới có thể được tạo ra. Tuy nhiên, việc tạo Widget mới không đồng nghĩa với việc tất cả Element và RenderObject cũ đều bị loại bỏ.
Flutter cần xác định:
-
Widget mới có tương thích với Element hiện tại hay không.
-
Element hiện tại có thể tiếp tục được sử dụng hay không.
-
Cấu hình Widget có cần cập nhật không.
-
Có cần thay thế Element hoặc xây dựng lại một phần cây giao diện không.
-
Trạng thái hiện tại có thể được giữ lại hay phải khởi tạo lại.
Một quy tắc quan trọng của Flutter được thể hiện qua phương thức Widget.canUpdate:
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType &&
oldWidget.key == newWidget.key;
}
Đoạn mã trên minh họa điều kiện cơ bản để Widget mới cập nhật Element đang gắn với Widget cũ: hai Widget phải có cùng runtimeType và cùng key.
Nếu điều kiện này được đáp ứng, Flutter thường có thể giữ lại Element và cập nhật cấu hình Widget. Nếu không, Element cũ có thể bị thay thế, tùy thuộc vào vị trí và quá trình cập nhật cây.
Vai trò của runtimeType
runtimeType cho biết kiểu thực tế của đối tượng Widget.
Ví dụ:
const Text('Xin chào');
const Icon(Icons.home);
Hai Widget trên có kiểu khác nhau. Text dùng để hiển thị văn bản, trong khi Icon hiển thị biểu tượng. Flutter không thể xem chúng là hai cấu hình tương đương để cập nhật cùng một Element theo quy tắc canUpdate.
Khi một Widget Text tại một vị trí được thay bằng Widget Icon, hệ thống có thể cần thay thế Element tương ứng và điều chỉnh cấu trúc bên dưới.
Vai trò của Key
Key giúp Flutter xác định danh tính của Widget trong cây giao diện, đặc biệt khi các Widget cùng loại được sắp xếp, thêm hoặc xóa.
Ví dụ:
Column(
children: const [
Text('Sản phẩm A', key: ValueKey('A')),
Text('Sản phẩm B', key: ValueKey('B')),
Text('Sản phẩm C', key: ValueKey('C')),
],
)
Mỗi Widget Text có một Key riêng. Khi danh sách thay đổi thứ tự, Key giúp Flutter nhận diện các phần tử theo danh tính thay vì chỉ dựa vào vị trí.
Một số loại Key phổ biến:
| Loại Key | Chức năng |
|---|---|
ValueKey |
Xác định Widget bằng một giá trị cụ thể |
ObjectKey |
Xác định Widget dựa trên danh tính của đối tượng |
UniqueKey |
Tạo Key duy nhất cho một lần khởi tạo |
GlobalKey |
Hỗ trợ nhận diện Widget trên phạm vi rộng hơn trong cây và truy cập một số thông tin liên quan |
GlobalKey có những quy tắc sử dụng riêng và không nên được dùng tùy tiện cho mọi Widget. Trong danh sách thông thường, ValueKey thường là lựa chọn đơn giản và phù hợp.
1.4. Ví dụ thực tế về quá trình đối chiếu Widget
Xét một ứng dụng hiển thị số lượt thích:
class LikeCounter extends StatefulWidget {
const LikeCounter({super.key});
@override
State<LikeCounter> createState() => _LikeCounterState();
}
class _LikeCounterState extends State<LikeCounter> {
int likes = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Lượt thích: $likes'),
ElevatedButton(
onPressed: () {
setState(() {
likes++;
});
},
child: const Text('Thích'),
),
],
);
}
}
Khi người dùng nhấn nút:
-
Hàm
onPressedđược gọi. -
setState()thông báo rằng trạng thái củaStateđã thay đổi. -
Flutter đánh dấu Element liên quan cần được xây dựng lại.
-
Phương thức
build()tạo cấu hình Widget mới. -
Flutter đối chiếu các Widget mới với cấu trúc hiện tại.
-
Những Element tương thích được cập nhật thay vì bị thay thế toàn bộ.
-
Các giai đoạn layout và paint cần thiết được thực hiện để phản ánh nội dung mới.
Giá trị likes vẫn được giữ lại vì nó nằm trong đối tượng State, không nằm trong cấu hình Widget bất biến.
1.5. Reconciliation trong danh sách có trạng thái
Giả sử một danh sách chứa các ô nhập liệu. Người dùng nhập nội dung vào ô thứ hai, sau đó ứng dụng chèn một phần tử mới lên đầu danh sách.
Nếu không sử dụng Key phù hợp, Flutter có thể đối chiếu các Widget theo vị trí và khiến trạng thái của một số ô bị gắn với phần tử không mong muốn, tùy thuộc vào cấu trúc danh sách.
Ví dụ sử dụng Key:
ListView(
children: products.map((product) {
return ProductTile(
key: ValueKey(product.id),
product: product,
);
}).toList(),
)
Ở đây, product.id là mã định danh ổn định của từng sản phẩm. Khi thứ tự danh sách thay đổi, Flutter có thể nhận diện Widget tương ứng với từng sản phẩm và duy trì trạng thái phù hợp.
Key không phải là công cụ tăng tốc mọi giao diện trong mọi tình huống. Giá trị chính của Key là hỗ trợ nhận diện và đối chiếu đúng các phần tử trong cây.
1.6. Ứng dụng trong phát triển Flutter
Widget Configuration và Element Reconciliation có ý nghĩa quan trọng trong các tình huống:
-
Xây dựng danh sách sản phẩm thương mại điện tử.
-
Cập nhật giao diện khi nhận dữ liệu từ API.
-
Duy trì trạng thái của biểu mẫu.
-
Tạo danh sách có thể thêm, xóa hoặc sắp xếp lại.
-
Xây dựng giao diện có nhiều Widget lồng nhau.
-
Tối ưu hóa quá trình cập nhật giao diện.
Lập trình viên nên ưu tiên Key ổn định cho các phần tử trong danh sách có trạng thái, tránh sử dụng UniqueKey() mới ở mỗi lần build() nếu mục tiêu là giữ lại trạng thái cũ.
Ngoài ra, không nên hiểu việc Flutter tái sử dụng Element là cam kết rằng toàn bộ quá trình render sẽ không diễn ra lại. Element được giữ lại vẫn có thể được cập nhật, layout lại hoặc paint lại nếu các thuộc tính thay đổi yêu cầu điều đó.
Kết luận: Widget Configuration và Element Reconciliation là nền tảng giúp Flutter cập nhật giao diện có chọn lọc. Việc nắm vững mối quan hệ giữa runtimeType, key, Widget Tree và Element Tree giúp lập trình viên quản lý trạng thái chính xác hơn, hạn chế lỗi trong danh sách động và xây dựng ứng dụng dễ bảo trì.
2. Flutter Rendering Pipeline
2.1. Tổng quan về Rendering Pipeline
Flutter Rendering Pipeline là chuỗi các giai đoạn xử lý giúp chuyển đổi cấu hình giao diện thành hình ảnh thực tế trên màn hình thiết bị.
Khi lập trình viên xây dựng giao diện bằng Dart, những đoạn mã đó chưa trực tiếp trở thành pixel trên màn hình. Flutter cần xử lý cấu trúc Widget, tính toán kích thước, xác định vị trí, vẽ các thành phần và phối hợp với hệ thống đồ họa để hiển thị kết quả.
Rendering Pipeline có thể được mô tả qua các giai đoạn chính:
-
Build.
-
Layout.
-
Paint.
-
Compositing.
-
Rasterization.
-
Semantics.
Trong đó, Semantics là quá trình xây dựng thông tin ngữ nghĩa phục vụ khả năng truy cập. Nó không đơn thuần là một bước bắt buộc nằm sau Rasterization trong mọi khung hình. Các giai đoạn có thể được kích hoạt độc lập tùy theo loại thay đổi.
2.2. Giai đoạn Build
Build là giai đoạn xây dựng hoặc cập nhật cấu hình Widget dựa trên dữ liệu và trạng thái hiện tại.
Flutter gọi phương thức build() của những thành phần cần được xây dựng lại để tạo ra cây Widget mới hoặc một phần cấu hình mới.
Ví dụ:
class WelcomePage extends StatefulWidget {
const WelcomePage({super.key});
@override
State<WelcomePage> createState() => _WelcomePageState();
}
class _WelcomePageState extends State<WelcomePage> {
String username = 'Tiến';
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('Xin chào, $username'),
),
);
}
}
Khi username thay đổi và trạng thái được thông báo đúng cách, Flutter có thể gọi lại build() để tạo nội dung mới.
Build không đồng nghĩa với việc vẽ toàn bộ giao diện. Nó chỉ tạo ra cấu hình Widget cần thiết cho giao diện mới.
Các nguyên nhân kích hoạt Build
-
Gọi
setState()trongState. -
Cập nhật dữ liệu từ một cơ chế quản lý trạng thái.
-
Một Widget cha được xây dựng lại và tạo cấu hình mới cho Widget con.
-
Thay đổi các phụ thuộc mà Widget đã đăng ký theo dõi, chẳng hạn dữ liệu từ
InheritedWidget. -
Những thay đổi trong cấu hình hoặc vòng đời ứng dụng khiến một phần cây cần được cập nhật.
Lập trình viên nên tránh đặt những tác vụ tốn thời gian như xử lý dữ liệu lớn, đọc tệp đồng bộ hoặc tính toán phức tạp trong build(). Phương thức này có thể được gọi nhiều lần và nên tập trung vào việc mô tả giao diện.
2.3. Giai đoạn Layout
Layout là giai đoạn tính toán kích thước và vị trí của các thành phần giao diện.
Trong Flutter, quá trình bố cục chủ yếu dựa trên hệ thống Constraints. Widget cha truyền các giới hạn kích thước xuống Widget con; Widget con chọn kích thước phù hợp trong phạm vi cho phép và báo kích thước của mình trở lại.
Nguyên tắc thường được diễn đạt như sau:
Constraints đi xuống, kích thước đi lên, vị trí được cha xác định.
Ví dụ:
Center(
child: Container(
width: 200,
height: 100,
color: Colors.blue,
),
)
Trong ví dụ trên, Center nhận các giới hạn từ Widget cha, xác định vị trí đặt Widget con và cho phép Container có kích thước phù hợp nếu giới hạn cho phép.
Nếu cha chỉ cho phép chiều rộng tối đa 150 pixel, Container không thể tùy ý hiển thị với chiều rộng 200 pixel vượt quá giới hạn đó.
BoxConstraints
BoxConstraints mô tả các giới hạn về chiều rộng và chiều cao của một thành phần.
Các thuộc tính quan trọng gồm:
-
minWidth: chiều rộng tối thiểu. -
maxWidth: chiều rộng tối đa. -
minHeight: chiều cao tối thiểu. -
maxHeight: chiều cao tối đa.
Ví dụ:
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 100,
maxWidth: 300,
minHeight: 50,
maxHeight: 150,
),
child: Container(
color: Colors.orange,
child: const Text('Nội dung'),
),
)
Container sẽ được bố trí trong phạm vi các giới hạn áp dụng, kết hợp với hành vi layout của Widget con và các ràng buộc từ Widget cha.
Layout lại
Khi kích thước hoặc các thuộc tính ảnh hưởng đến bố cục thay đổi, Flutter có thể đánh dấu RenderObject liên quan cần layout lại.
Quá trình layout thường có ảnh hưởng đến những thành phần phụ thuộc vào kích thước hoặc vị trí của thành phần đang thay đổi. Vì vậy, việc xây dựng cây Widget hiệu quả cần đi cùng với hiểu biết về cách bố cục lan truyền trong cây RenderObject.
2.4. Giai đoạn Paint
Paint là giai đoạn xác định cách nội dung của các RenderObject được vẽ.
Các hoạt động có thể bao gồm:
-
Vẽ màu nền.
-
Vẽ văn bản.
-
Vẽ đường viền.
-
Vẽ hình dạng.
-
Vẽ hình ảnh.
-
Vẽ bóng đổ.
-
Vẽ các thành phần trang trí.
Ví dụ:
Container(
width: 200,
height: 100,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(16),
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 8,
offset: Offset(0, 4),
),
],
),
)
Các thuộc tính như màu nền, đường viền và bóng đổ ảnh hưởng đến kết quả vẽ.
Nếu chỉ có thay đổi ảnh hưởng đến paint mà không ảnh hưởng đến kích thước hoặc vị trí, Flutter có thể chỉ cần thực hiện lại quá trình paint cho phần liên quan mà không phải layout lại toàn bộ cấu trúc.
Tuy nhiên, không phải mọi thay đổi màu sắc đều bảo đảm chỉ kích hoạt paint. Kết quả cụ thể phụ thuộc vào Widget, RenderObject và cách các thành phần phối hợp với nhau.
2.5. Giai đoạn Compositing
Compositing là quá trình kết hợp các lớp hoặc lớp hiển thị thành kết quả đồ họa cuối cùng.
Flutter có thể tổ chức nội dung thành các compositing layer. Các lớp này cho phép hệ thống quản lý những nội dung cần được kết hợp với nhau, đặc biệt khi có hiệu ứng biến đổi, cắt vùng hiển thị, độ trong suốt hoặc các thành phần có khả năng được xử lý độc lập.
Một số Widget có thể tạo hoặc ảnh hưởng đến ranh giới layer, chẳng hạn các Widget sử dụng cơ chế opacity, clipping hoặc hiệu ứng đặc biệt.
Tuy nhiên, không phải mọi Widget đều tạo ra một layer riêng. Việc tạo quá nhiều layer không cần thiết có thể làm tăng chi phí quản lý bộ nhớ và quá trình tổng hợp hình ảnh.
Ví dụ:
Opacity(
opacity: 0.5,
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
)
Widget Opacity giúp điều chỉnh độ trong suốt của nội dung con. Tùy thuộc vào trường hợp và cách tối ưu của Flutter, việc xử lý có thể liên quan đến compositing layer hoặc những phương pháp khác.
2.6. Giai đoạn Rasterization
Rasterization là quá trình chuyển các lệnh vẽ và nội dung đồ họa thành những pixel cụ thể để hiển thị.
Sau khi Flutter xử lý cấu trúc và thông tin vẽ, hệ thống đồ họa sẽ thực hiện các phép toán cần thiết để tạo hình ảnh. Quá trình này có thể liên quan đến GPU, tùy thuộc vào nền tảng và đường dẫn render đang sử dụng.
Các tác vụ có thể bao gồm:
-
Tô màu các vùng hình học.
-
Hiển thị chữ và hình ảnh.
-
Áp dụng hiệu ứng đồ họa.
-
Kết hợp nội dung từ các lớp.
-
Tạo khung hình được đưa lên màn hình.
Flutter sử dụng các thành phần của engine để thực hiện công việc đồ họa. Vì vậy, tối ưu giao diện không chỉ là giảm số lượng Widget mà còn phải quan tâm đến chi phí layout, paint, rasterization và các hiệu ứng đồ họa.
2.7. Giai đoạn Semantics
Semantics cung cấp thông tin ngữ nghĩa về giao diện cho những công nghệ hỗ trợ, chẳng hạn trình đọc màn hình.
Một giao diện có thể nhìn đẹp nhưng vẫn khó sử dụng với người khiếm thị nếu các biểu tượng, nút bấm hoặc trường nhập liệu không có thông tin mô tả phù hợp.
Ví dụ:
Semantics(
label: 'Thêm sản phẩm vào giỏ hàng',
button: true,
child: IconButton(
onPressed: () {},
icon: const Icon(Icons.add_shopping_cart),
),
)
Thông tin Semantics giúp công nghệ hỗ trợ nhận biết mục đích của thành phần giao diện.
Trong ứng dụng thực tế, cần cung cấp nhãn rõ ràng, trạng thái phù hợp và thứ tự điều hướng hợp lý. Semantics là một phần quan trọng của chất lượng ứng dụng, không chỉ là yêu cầu phụ về giao diện.
2.8. Mối quan hệ giữa các giai đoạn
| Giai đoạn | Nhiệm vụ chính | Ví dụ thay đổi |
|---|---|---|
| Build | Tạo cấu hình Widget | Thay đổi nội dung văn bản |
| Layout | Tính kích thước, vị trí | Thay đổi chiều rộng Container |
| Paint | Tạo lệnh vẽ | Thay đổi màu nền |
| Compositing | Kết hợp các lớp đồ họa | Thay đổi hiệu ứng lớp |
| Rasterization | Chuyển nội dung đồ họa thành pixel | Render khung hình |
| Semantics | Cập nhật thông tin trợ năng | Thay đổi nhãn nút bấm |
Các giai đoạn có mối quan hệ chặt chẽ nhưng không nhất thiết phải chạy lại toàn bộ mỗi khi giao diện thay đổi.
Ví dụ, thay đổi kích thước thường làm phát sinh layout và có thể kéo theo paint. Thay đổi thông tin trợ năng có thể yêu cầu cập nhật Semantics mà không nhất thiết phải vẽ lại toàn bộ nội dung.
2.9. Tối ưu hiệu năng dựa trên Rendering Pipeline
Một số nguyên tắc quan trọng:
-
Hạn chế tính toán nặng trong
build(). -
Sử dụng
constkhi có thể để giảm việc tạo cấu hình Widget không cần thiết. -
Sử dụng
ListView.buildercho danh sách dài và dữ liệu có thể tạo theo nhu cầu. -
Tránh tạo hiệu ứng đồ họa phức tạp không cần thiết.
-
Kiểm tra tình trạng layout lại và paint lại bằng Flutter DevTools.
-
Sử dụng công cụ Performance để phát hiện các khung hình bị chậm.
-
Đánh giá hiệu năng trên thiết bị thật hoặc chế độ gần với môi trường phát hành.
Trên màn hình 60 Hz, ngân sách thời gian cho một khung hình xấp xỉ 16,67 ms. Nếu quá trình xử lý vượt quá ngân sách này, người dùng có thể cảm nhận được hiện tượng giật, khựng hoặc hoạt ảnh thiếu mượt mà. Màn hình có tần số quét cao hơn sẽ có ngân sách thời gian mỗi khung hình ngắn hơn.
Kết luận: Flutter Rendering Pipeline giải thích cách cấu hình Widget được chuyển thành giao diện hiển thị. Việc hiểu rõ Build, Layout, Paint, Compositing, Rasterization và Semantics giúp lập trình viên xác định nguyên nhân gây chậm, tối ưu giao diện có cơ sở và cải thiện khả năng truy cập của ứng dụng.
3. InheritedWidget và cơ chế truyền dữ liệu xuống cây Widget
3.1. Tổng quan về InheritedWidget
InheritedWidget là một loại Widget đặc biệt của Flutter, cho phép cung cấp dữ liệu cho các Widget con nằm bên dưới nó trong cây giao diện.
Trong ứng dụng nhỏ, lập trình viên có thể truyền dữ liệu bằng constructor từ Widget cha xuống Widget con. Tuy nhiên, khi cây Widget có nhiều cấp, việc truyền cùng một dữ liệu qua hàng loạt Widget trung gian làm tăng độ phức tạp của mã nguồn.
Ví dụ, thông tin người dùng hiện tại có thể cần thiết ở nhiều vị trí:
-
Thanh tiêu đề hiển thị tên người dùng.
-
Trang cá nhân hiển thị ảnh đại diện.
-
Trang cài đặt hiển thị email.
-
Nút đăng xuất truy cập trạng thái đăng nhập.
Nếu phải truyền thông tin người dùng qua tất cả các Widget trung gian, nhiều Widget sẽ phải nhận các tham số mà bản thân chúng không trực tiếp sử dụng.
InheritedWidget cung cấp cơ chế chia sẻ dữ liệu theo phạm vi cây Widget, giúp các thành phần con có thể truy cập dữ liệu cần thiết mà không phải truyền tham số qua từng cấp.
3.2. Nguyên lý hoạt động
Một InheritedWidget cung cấp dữ liệu cho những Widget nằm trong phạm vi con của nó. Widget muốn sử dụng dữ liệu có thể tìm InheritedWidget thích hợp thông qua BuildContext.
Khi Widget đăng ký phụ thuộc vào InheritedWidget, Flutter ghi nhận mối quan hệ phụ thuộc giữa Element của Widget đó và Element cung cấp dữ liệu.
Nếu dữ liệu thay đổi và InheritedWidget thông báo rằng nội dung có ý nghĩa đối với các phụ thuộc đã thay đổi, Flutter có thể xây dựng lại những Widget phụ thuộc tương ứng.
Cơ chế này gồm ba thành phần:
-
Widget cung cấp dữ liệu.
-
Widget tiêu thụ dữ liệu.
-
Cơ chế theo dõi phụ thuộc và cập nhật giao diện.
Ví dụ, một ứng dụng có thể sử dụng InheritedWidget để cung cấp tên người dùng cho các thành phần trong một trang.
3.3. Xây dựng InheritedWidget tùy chỉnh
Đoạn mã sau minh họa cách tạo một InheritedWidget để chia sẻ tên người dùng.
import 'package:flutter/material.dart';
class UserScope extends InheritedWidget {
final String username;
const UserScope({
super.key,
required this.username,
required super.child,
});
static UserScope? maybeOf(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<UserScope>();
}
static UserScope of(BuildContext context) {
final scope = maybeOf(context);
assert(scope != null, 'Không tìm thấy UserScope trong cây Widget');
return scope!;
}
@override
bool updateShouldNotify(UserScope oldWidget) {
return username != oldWidget.username;
}
}
Trong đoạn mã trên:
-
usernamelà dữ liệu được chia sẻ. -
childlà Widget con nằm trong phạm vi cung cấp dữ liệu. -
maybeOf()tìmUserScopevà đăng ký phụ thuộc. -
of()cung cấp phương thức truy cập thuận tiện khiUserScopeđược kỳ vọng phải tồn tại. -
updateShouldNotify()xác định liệu các phụ thuộc có cần được thông báo về cấu hình mới hay không.
Điều kiện username != oldWidget.username giúp Flutter nhận biết rằng dữ liệu cần quan tâm đã thay đổi.
Nếu username giữ nguyên, updateShouldNotify() trả về false. Nếu tên người dùng thay đổi, phương thức trả về true.
Lưu ý rằng phương thức này quyết định việc thông báo cho các phụ thuộc của InheritedWidget; nó không có nghĩa là toàn bộ ứng dụng chỉ được phép cập nhật khi tên người dùng thay đổi.
3.4. Cung cấp dữ liệu trong cây Widget
class UserPage extends StatelessWidget {
const UserPage({super.key});
@override
Widget build(BuildContext context) {
return const UserScope(
username: 'Tien',
child: MaterialApp(
home: UserProfile(),
),
);
}
}
class UserProfile extends StatelessWidget {
const UserProfile({super.key});
@override
Widget build(BuildContext context) {
final username = UserScope.of(context).username;
return Scaffold(
appBar: AppBar(
title: Text('Xin chào $username'),
),
body: Center(
child: Text('Tên người dùng: $username'),
),
);
}
}
UserScope bao bọc MaterialApp và cung cấp dữ liệu cho cây Widget bên dưới. UserProfile truy cập dữ liệu thông qua UserScope.of(context).
Trong ví dụ này, UserProfile không cần nhận username thông qua constructor. Điều này giúp giảm sự phụ thuộc vào các Widget trung gian.
Trong ứng dụng lớn, vị trí đặt InheritedWidget cần được lựa chọn hợp lý. Nếu đặt quá cao, phạm vi dữ liệu có thể rộng hơn nhu cầu. Nếu đặt quá thấp, những Widget khác có thể không truy cập được dữ liệu.
3.5. dependOnInheritedWidgetOfExactType()
dependOnInheritedWidgetOfExactType<T>() là phương thức của BuildContext, được dùng để tìm InheritedWidget thuộc kiểu T gần nhất trong phạm vi tổ tiên và đăng ký phụ thuộc vào nó.
Ví dụ:
final scope =
context.dependOnInheritedWidgetOfExactType<UserScope>();
Khi phương thức được sử dụng trong một ngữ cảnh phù hợp, Element hiện tại sẽ theo dõi InheritedWidget được tìm thấy.
Nếu dữ liệu của UserScope thay đổi và updateShouldNotify() trả về true, những Element đã đăng ký phụ thuộc có thể được đánh dấu cần xây dựng lại.
Phương thức này phù hợp khi giao diện cần phản ứng với dữ liệu. Ví dụ:
-
Hiển thị tên người dùng.
-
Hiển thị trạng thái đăng nhập.
-
Thay đổi giao diện theo cấu hình.
-
Truy cập dữ liệu ngôn ngữ.
-
Đọc thông tin chủ đề giao diện.
Một điểm quan trọng là không nên gọi phương thức này trong những ngữ cảnh không phù hợp với vòng đời của BuildContext. Trong ứng dụng thông thường, việc truy cập từ phương thức build() là cách sử dụng phổ biến.
3.6. getInheritedWidgetOfExactType()
getInheritedWidgetOfExactType<T>() cũng tìm InheritedWidget thuộc kiểu T trong cây tổ tiên, nhưng không đăng ký phụ thuộc.
Ví dụ:
final scope =
context.getInheritedWidgetOfExactType<UserScope>();
Khác biệt cốt lõi nằm ở cơ chế theo dõi phụ thuộc.
| Đặc điểm | dependOnInheritedWidgetOfExactType |
getInheritedWidgetOfExactType |
|---|---|---|
| Tìm Widget tổ tiên | Có | Có |
| Đăng ký phụ thuộc | Có | Không |
| Tự động nhận thông báo thay đổi từ cơ chế phụ thuộc | Có | Không |
| Phù hợp để đọc dữ liệu phục vụ giao diện phản ứng | Có | Không phải lựa chọn mặc định |
Có thể trả về null |
Có | Có |
Nếu chỉ cần đọc một giá trị tại một thời điểm và không muốn đăng ký phụ thuộc, getInheritedWidgetOfExactType() có thể phù hợp.
Tuy nhiên, nếu giao diện cần tự động cập nhật khi dữ liệu thay đổi, phương thức đăng ký phụ thuộc thường là lựa chọn phù hợp hơn.
Không nên nhầm hai phương thức này với việc đọc dữ liệu từ một hệ thống quản lý trạng thái có cơ chế thông báo riêng. Mỗi thư viện có thể cung cấp thêm các API để lựa chọn giữa đọc dữ liệu một lần và theo dõi thay đổi.
3.7. updateShouldNotify và cơ chế cập nhật
updateShouldNotify() là phương thức quyết định cấu hình mới của InheritedWidget có cần thông báo đến những Element phụ thuộc hay không.
Ví dụ:
@override
bool updateShouldNotify(UserScope oldWidget) {
return username != oldWidget.username;
}
Phương thức so sánh dữ liệu của cấu hình mới với cấu hình cũ.
Nếu chỉ một phần dữ liệu thay đổi, việc so sánh chính xác giúp tránh thông báo không cần thiết. Với những đối tượng dữ liệu phức tạp, lập trình viên có thể cần so sánh các trường thực sự ảnh hưởng đến giao diện thay vì chỉ kiểm tra danh tính đối tượng.
Dù vậy, updateShouldNotify() không phải công cụ duy nhất kiểm soát toàn bộ số lần build() của ứng dụng. Widget cha vẫn có thể xây dựng lại các thành phần con theo cơ chế bình thường.
3.8. Quan hệ giữa InheritedWidget và các giải pháp quản lý trạng thái
InheritedWidget là một thành phần nền tảng trong hệ thống Flutter. Nhiều cơ chế quản lý trạng thái và chia sẻ dữ liệu cấp cao được xây dựng dựa trên nguyên lý tương tự.
Ví dụ:
-
Themecho phép các Widget truy cập cấu hình giao diện. -
MediaQuerycung cấp thông tin về môi trường hiển thị. -
Localizationscung cấp dữ liệu bản địa hóa. -
Providertận dụng cơ chế InheritedWidget để cung cấp đối tượng và quản lý phụ thuộc. -
Một số giải pháp như Riverpod cung cấp cách quản lý và theo dõi trạng thái ở cấp trừu tượng khác.
Việc hiểu InheritedWidget giúp lập trình viên hiểu rõ hơn cơ chế truy cập dữ liệu của Theme.of(context), MediaQuery.of(context) và nhiều API phổ biến khác.
Kết luận: InheritedWidget là nền tảng quan trọng trong cơ chế chia sẻ dữ liệu theo cây Widget. Việc nắm vững cách cung cấp dữ liệu, đăng ký phụ thuộc, sử dụng updateShouldNotify() và phân biệt hai phương thức truy cập giúp lập trình viên xây dựng giao diện linh hoạt, giảm truyền tham số qua nhiều cấp và hiểu sâu hơn các thư viện quản lý trạng thái của Flutter.
4. BuildOwner, Dirty Elements và Build Scheduling
4.1. Tổng quan về BuildOwner
BuildOwner là một thành phần nội bộ của Flutter Framework, có nhiệm vụ điều phối những hoạt động liên quan đến quá trình xây dựng cây Element.
Thay vì xây dựng lại toàn bộ giao diện ngay tại thời điểm mỗi dữ liệu thay đổi, Flutter có cơ chế đánh dấu các Element cần được xây dựng lại rồi xử lý chúng theo lịch phù hợp.
BuildOwner tham gia quản lý danh sách những Element cần build lại và phối hợp quá trình xây dựng lại cây giao diện. Nó cũng tham gia vào những công việc quản lý trạng thái và vòng đời Element trong quá trình cập nhật.
Trong các ứng dụng Flutter thông thường, lập trình viên hiếm khi cần tạo hoặc điều khiển BuildOwner trực tiếp. Tuy nhiên, hiểu thành phần này giúp giải thích cách Flutter xử lý nhiều thay đổi giao diện trong cùng một chu kỳ cập nhật.
4.2. Dirty Elements
Dirty Element là Element đã được đánh dấu cần xây dựng lại.
Khi một trạng thái thay đổi, Flutter không nhất thiết gọi ngay build() tại chính thời điểm thay đổi. Thay vào đó, Element liên quan được đánh dấu dirty và được đưa vào quy trình xử lý build.
Ví dụ:
setState(() {
counter++;
});
Trong một State đang hoạt động, việc gọi setState() thực hiện hàm cập nhật trạng thái đồng bộ, sau đó thông báo cho Flutter rằng giao diện liên quan cần được cập nhật.
Quá trình có thể được khái quát như sau:
-
Trạng thái thay đổi.
-
Element liên quan được đánh dấu cần build lại.
-
Hệ thống lên lịch xử lý.
-
Flutter xử lý các Element dirty trong giai đoạn build.
-
Cấu hình Widget mới được tạo.
-
Flutter đối chiếu cấu hình mới với cây hiện tại.
-
Những giai đoạn render cần thiết tiếp tục được thực hiện.
Dirty không có nghĩa là Widget đã bị hỏng hoặc dữ liệu không hợp lệ. Thuật ngữ này chỉ trạng thái cho biết Element cần được xử lý lại trong quy trình build.
4.3. Vai trò của BuildOwner trong quá trình cập nhật
BuildOwner điều phối các Element dirty thông qua cơ chế đánh dấu và lên lịch xử lý.
Các trách nhiệm liên quan bao gồm:
-
Quản lý danh sách Element cần rebuild.
-
Phối hợp việc xây dựng lại cây Element.
-
Xử lý những Element cần cập nhật theo thứ tự phù hợp.
-
Hỗ trợ quá trình quản lý vòng đời của Element.
-
Tham gia xử lý các bước cuối của một chu kỳ build.
Trong quá trình này, Flutter phải đảm bảo cây giao diện được cập nhật theo những quy tắc nhất quán. Ví dụ, các Element được đánh dấu dirty cần được xử lý trong bối cảnh phù hợp với cấu trúc cha – con.
Nếu một Widget cha thay đổi, việc xây dựng lại cha có thể tạo cấu hình mới cho các Widget con. Những Widget con tương thích có thể được cập nhật thông qua cơ chế reconciliation.
4.4. Build Scheduling
Build Scheduling là cơ chế lên lịch cho việc xây dựng lại giao diện.
Flutter sử dụng hệ thống scheduler để điều phối các công việc liên quan đến khung hình. Khi có thay đổi cần phản ánh lên giao diện, hệ thống có thể yêu cầu một frame mới được xử lý.
Việc lên lịch giúp Flutter gom nhiều thay đổi lại trong một chu kỳ xử lý phù hợp thay vì phải hoàn thành một lần render đầy đủ cho từng thao tác nhỏ.
Ví dụ:
setState(() {
counter++;
});
setState(() {
counter++;
});
Nếu hai lời gọi xảy ra trong cùng một đoạn xử lý đồng bộ và trước khi frame tiếp theo được xử lý, Flutter thường có thể gom các thay đổi liên quan vào cùng một lần build của Element đó.
Kết quả cuối cùng của biến counter tăng thêm hai đơn vị, nhưng điều đó không đồng nghĩa Flutter nhất thiết phải render hai khung hình riêng biệt cho hai lời gọi.
Cơ chế này giúp hạn chế công việc trùng lặp và duy trì khả năng cập nhật giao diện hiệu quả.
Tuy nhiên, không nên coi việc gom thay đổi là bảo đảm tuyệt đối trong mọi tình huống. Những thay đổi xảy ra ở các thời điểm khác nhau hoặc giữa các frame có thể dẫn đến những lần cập nhật riêng.
4.5. Mối liên hệ giữa setState và Dirty Elements
setState() là API thường được sử dụng để thông báo cho Flutter rằng trạng thái của một StatefulWidget đã thay đổi.
Ví dụ:
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int count = 0;
void increment() {
setState(() {
count++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('Giá trị: $count'),
),
floatingActionButton: FloatingActionButton(
onPressed: increment,
child: const Icon(Icons.add),
),
);
}
}
Khi người dùng nhấn nút, hàm increment() được gọi. setState() thực hiện thay đổi giá trị count và đánh dấu phần giao diện liên quan cần được xây dựng lại.
Một hiểu nhầm phổ biến là cho rằng setState() tự vẽ lại toàn bộ ứng dụng. Trên thực tế, setState() thông báo cho Flutter về thay đổi trạng thái. Quá trình build, reconciliation, layout và paint tiếp theo được điều phối bởi framework.
Phạm vi công việc thực tế phụ thuộc vào cấu trúc cây và những thành phần bị ảnh hưởng. Vì vậy, đặt setState() trong một Widget có phạm vi nhỏ thường giúp giới hạn vùng cần cập nhật, nhưng không tự động đảm bảo rằng mọi công việc render đều chỉ giới hạn trong Widget đó.
4.6. Phạm vi rebuild và thiết kế Widget
Một ứng dụng có thể chứa nhiều cấp Widget. Nếu trạng thái thay đổi được quản lý ở một Widget cha quá lớn, việc cập nhật trạng thái có thể khiến phần lớn cây con được xây dựng lại.
Ví dụ, nếu một trang chứa danh sách sản phẩm, thanh điều hướng, bộ lọc và giỏ hàng cùng được tổ chức trong một Widget lớn, mọi thay đổi trạng thái tại đó có thể làm phương thức build() của Widget lớn chạy lại.
Một hướng thiết kế phù hợp là tách các thành phần theo trách nhiệm:
class ProductPage extends StatelessWidget {
const ProductPage({super.key});
@override
Widget build(BuildContext context) {
return const Column(
children: [
ProductHeader(),
Expanded(
child: ProductList(),
),
CartSummary(),
],
);
}
}
class ProductHeader extends StatelessWidget {
const ProductHeader({super.key});
@override
Widget build(BuildContext context) {
return const Text('Danh sách sản phẩm');
}
}
class ProductList extends StatelessWidget {
const ProductList({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text('Danh sách sẽ hiển thị tại đây'),
);
}
}
class CartSummary extends StatelessWidget {
const CartSummary({super.key});
@override
Widget build(BuildContext context) {
return const Text('Giỏ hàng');
}
}
Việc tách Widget giúp cấu trúc mã nguồn rõ ràng hơn và tạo điều kiện để quản lý trạng thái theo phạm vi phù hợp.
Tuy nhiên, tách Widget quá mức cũng có thể làm tăng độ phức tạp trong tổ chức mã nguồn. Mục tiêu không phải tạo càng nhiều Widget càng tốt mà là phân chia theo chức năng và ranh giới trạng thái hợp lý.
4.7. Phân biệt rebuild, relayout và repaint
Ba khái niệm này liên quan nhưng không đồng nghĩa.
| Khái niệm | Ý nghĩa |
|---|---|
| Rebuild | Xây dựng lại cấu hình Widget hoặc một phần cây |
| Relayout | Tính toán lại kích thước và vị trí |
| Repaint | Thực hiện lại quá trình vẽ nội dung |
Một Widget có thể rebuild nhưng không nhất thiết khiến mọi RenderObject phải layout lại hoặc paint lại.
Ngược lại, một thay đổi liên quan đến RenderObject có thể làm phát sinh layout hoặc paint mà không yêu cầu xây dựng lại toàn bộ cây Widget.
Ví dụ, AnimatedContainer có thể thay đổi một số thuộc tính theo thời gian. Cơ chế hoạt ảnh và RenderObject có thể khiến các giai đoạn render cần thiết được kích hoạt theo từng frame.
Hiểu sự khác biệt này giúp lập trình viên tránh tối ưu sai mục tiêu. Nếu ứng dụng bị giật vì rasterization quá nặng, giảm số lần build() chưa chắc giải quyết được vấn đề. Cần xác định đúng giai đoạn gây chậm bằng công cụ đo hiệu năng.
4.8. Lưu ý khi xử lý trạng thái
Một số nguyên tắc cần tuân thủ:
-
Không gọi
setState()sau khi mộtStateđã bị dispose. -
Không gọi
setState()nếu trạng thái không thực sự cần thay đổi giao diện. -
Tránh đặt các tác vụ bất đồng bộ dài bên trong callback đồng bộ của
setState(). -
Chỉ thực hiện thay đổi trạng thái trong callback của
setState(), không dùng callback này để chờ tác vụ bất đồng bộ. -
Phân chia trạng thái theo phạm vi sử dụng.
-
Sử dụng Flutter DevTools để đo đạc hiệu năng thay vì chỉ dựa vào số lượng Widget.
Ví dụ không phù hợp:
// Không nên
setState(() async {
await loadData();
});
Cách xử lý phù hợp hơn:
Future<void> loadAndUpdate() async {
final result = await loadData();
if (!mounted) return;
setState(() {
data = result;
});
}
Trong đó, loadData() và biến data cần được định nghĩa phù hợp với ứng dụng. Kiểm tra mounted giúp tránh cập nhật trạng thái sau khi Widget đã bị loại khỏi cây.
Kết luận: BuildOwner, Dirty Elements và Build Scheduling giải thích cách Flutter điều phối việc cập nhật giao diện. Hiểu cơ chế này giúp lập trình viên nắm rõ vai trò của setState(), kiểm soát phạm vi rebuild và phân biệt chính xác giữa rebuild, relayout và repaint. Đây là cơ sở quan trọng để tối ưu hiệu năng và tổ chức trạng thái trong các ứng dụng Flutter lớn.
5. ParentDataWidget và dữ liệu layout
5.1. Tổng quan về ParentDataWidget
ParentDataWidget là một loại Widget đặc biệt trong Flutter, có nhiệm vụ truyền thông tin bố cục từ Widget con đến RenderObject của Widget cha phù hợp.
Thông thường, Widget cha quyết định cách bố trí các Widget con. Tuy nhiên, có những trường hợp Widget con cần cung cấp thêm thông tin để cha biết cách phân chia không gian hoặc đặt vị trí.
Ví dụ:
-
Expandedcho biết một phần tử trongRowhoặcColumncần sử dụng phần không gian còn lại. -
Flexiblecho phép phần tử tham gia phân chia không gian theo quy tắc linh hoạt. -
Positionedcung cấp thông tin vị trí cho phần tử bên trongStack.
Các Widget này không trực tiếp vẽ giao diện. Chúng cung cấp dữ liệu bố cục để RenderObject cha có thể thực hiện layout theo đúng quy tắc.
5.2. Cơ chế ParentData
Trong hệ thống RenderObject, một RenderObject cha có thể lưu trữ thông tin riêng cho từng RenderObject con thông qua cấu trúc ParentData.
Thông tin này có thể bao gồm:
-
Vị trí của phần tử.
-
Khoảng cách từ cạnh của vùng bố cục.
-
Hệ số phân chia không gian.
-
Cách phần tử tham gia vào thuật toán layout.
-
Những thông tin phụ thuộc vào loại RenderObject cha.
Ví dụ, RenderFlex được sử dụng trong các bố cục như Row và Column. RenderObject này có thể sử dụng thông tin FlexParentData để biết hệ số flex và những thông tin bố cục khác của từng phần tử con.
Tương tự, Stack sử dụng thông tin liên quan đến StackParentData để xác định vị trí và kích thước của những phần tử được định vị.
ParentData giúp giữ các quy tắc bố cục ở đúng nơi cần thiết. Widget con cung cấp thông tin, còn RenderObject cha sử dụng thông tin đó trong thuật toán layout.
5.3. Expanded
Expanded là một ParentDataWidget thường được sử dụng bên trong Row, Column hoặc các Widget thuộc hệ thống Flex.
Nó yêu cầu phần tử con tham gia sử dụng phần không gian còn lại theo hệ số flex được chỉ định.
Ví dụ:
Row(
children: [
Container(
width: 100,
height: 80,
color: Colors.red,
),
Expanded(
child: Container(
height: 80,
color: Colors.blue,
),
),
],
)
Trong ví dụ trên:
-
Container màu đỏ có chiều rộng cố định 100 pixel.
-
Expandedcho phép Container màu xanh sử dụng phần chiều rộng còn lại trong phạm vi màRowcó thể bố trí. -
Rowtính toán không gian đã sử dụng và phân chia phần không gian còn lại cho các phần tử flex.
Nếu Row có chiều rộng hữu hạn 400 pixel và phần tử đỏ chiếm 100 pixel, phần còn lại khoảng 300 pixel có thể được dành cho phần tử Expanded, giả sử không có padding, khoảng cách hoặc phần tử nào khác chiếm thêm không gian.
Expanded thường được sử dụng khi muốn một thành phần tự động chiếm không gian còn lại thay vì phải xác định kích thước bằng một con số cố định.
5.4. Flexible
Flexible cũng cho phép phần tử tham gia phân chia không gian trong bố cục Flex. Tuy nhiên, nó có những khác biệt quan trọng so với Expanded.
Ví dụ:
Row(
children: [
Flexible(
child: Container(
height: 80,
color: Colors.orange,
child: const Text('Nội dung có thể co giãn'),
),
),
const SizedBox(width: 16),
const Text('Thông tin bên cạnh'),
],
)
Flexible cho phép phần tử con tham gia sử dụng không gian theo quy tắc flex, nhưng không nhất thiết buộc phần tử phải lấp đầy toàn bộ phần không gian được phân bổ.
Sự khác biệt quan trọng nhất thường nằm ở thuộc tính fit:
| Đặc điểm | Expanded |
Flexible |
|---|---|---|
| Kiểu sử dụng | Mở rộng phần tử | Cho phép phần tử co giãn |
Giá trị fit mặc định |
FlexFit.tight |
FlexFit.loose |
| Mức độ bắt buộc sử dụng không gian được phân bổ | Bắt buộc theo ràng buộc flex | Có thể sử dụng ít hơn |
| Phạm vi sử dụng | Row, Column, Flex |
Row, Column, Flex |
Về mặt triển khai, Expanded là một dạng chuyên biệt của Flexible sử dụng FlexFit.tight.
Một lưu ý quan trọng là Expanded và Flexible cần được đặt trong cây bố cục có tổ tiên Flex phù hợp. Nếu đặt sai vị trí, Flutter có thể báo lỗi về ParentData hoặc ràng buộc layout.
5.5. Positioned
Positioned là một ParentDataWidget được sử dụng với Stack, cho phép xác định vị trí của phần tử con theo các cạnh của vùng bố cục.
Ví dụ:
Stack(
children: [
Container(
width: 300,
height: 200,
color: Colors.grey,
),
const Positioned(
top: 16,
right: 16,
child: Icon(
Icons.favorite,
color: Colors.red,
size: 32,
),
),
],
)
Trong ví dụ trên, biểu tượng trái tim được đặt cách cạnh trên và cạnh phải của Stack 16 pixel.
Positioned có thể sử dụng các thuộc tính:
-
top: khoảng cách từ cạnh trên. -
bottom: khoảng cách từ cạnh dưới. -
left: khoảng cách từ cạnh trái. -
right: khoảng cách từ cạnh phải. -
width: chiều rộng được chỉ định. -
height: chiều cao được chỉ định.
Những thuộc tính này được chuyển thành dữ liệu bố cục mà Stack sử dụng để xác định vị trí của phần tử.
Ví dụ:
Stack(
children: [
Container(
width: 300,
height: 200,
color: Colors.blueGrey,
),
const Positioned(
left: 20,
top: 20,
width: 120,
height: 50,
child: ColoredBox(
color: Colors.orange,
),
),
],
)
Container màu cam được bố trí theo thông tin vị trí và kích thước đã cung cấp.
Nếu chỉ định đồng thời những thuộc tính ràng buộc nhiều cạnh, chẳng hạn left, right và width, cần chú ý các quy tắc xác định kích thước của Stack. Một số tổ hợp có thể không hợp lệ hoặc khiến thuộc tính bị bỏ qua theo quy tắc layout của Flutter.
5.6. ParentDataWidget và thuật toán layout của cha
Điểm quan trọng của ParentDataWidget là dữ liệu được truyền đến RenderObject cha phải phù hợp với loại bố cục mà cha hỗ trợ.
Ví dụ, Expanded cung cấp thông tin dành cho bố cục Flex. Nếu sử dụng Expanded bên trong Stack mà không có một tổ tiên Flex phù hợp trong cấu trúc RenderObject liên quan, Flutter có thể phát sinh lỗi vì dữ liệu bố cục không tương thích.
Tương tự, Positioned cần một Stack phù hợp để diễn giải các thông tin như top, left hoặc right.
Có thể hình dung quy trình như sau:
-
Widget cha xác định thuật toán bố cục.
-
Widget con sử dụng
ParentDataWidgetđể cung cấp thông tin. -
Thông tin được ghi vào ParentData của RenderObject con phù hợp.
-
RenderObject cha đọc ParentData.
-
Thuật toán layout sử dụng dữ liệu để xác định kích thước và vị trí.
Đây là lý do Flutter yêu cầu các Widget như Expanded, Flexible và Positioned phải được sử dụng đúng ngữ cảnh.
5.7. Ví dụ kết hợp Expanded và Positioned
Một giao diện có thể sử dụng Expanded trong Column để chia không gian dọc và Positioned trong Stack để đặt nút nổi.
Scaffold(
body: Column(
children: [
const Padding(
padding: EdgeInsets.all(16),
child: Text(
'Danh sách sản phẩm',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
Expanded(
child: Stack(
children: [
ListView(
children: const [
ListTile(title: Text('Sản phẩm A')),
ListTile(title: Text('Sản phẩm B')),
ListTile(title: Text('Sản phẩm C')),
],
),
Positioned(
right: 16,
bottom: 16,
child: FloatingActionButton(
onPressed: null,
child: Icon(Icons.add),
),
),
],
),
),
],
),
)
Trong cấu trúc này:
-
Columnbố trí tiêu đề và vùng nội dung theo chiều dọc. -
Expandedgiúp vùng nội dung chiếm phần không gian dọc còn lại. -
Stackcho phép xếp chồng danh sách và nút nổi. -
Positionedđặt nút nổi ở góc dưới bên phải của vùngStack. -
ListViewquản lý việc hiển thị danh sách cuộn.
Ví dụ thể hiện cách các ParentDataWidget khác nhau phối hợp trong cùng một giao diện, nhưng mỗi Widget vẫn cần nằm trong đúng ngữ cảnh bố cục của nó.
5.8. Lỗi thường gặp khi sử dụng ParentDataWidget
Đặt Expanded sai vị trí
// Không nên
Stack(
children: [
Expanded(
child: Container(),
),
],
)
Expanded không thể sử dụng trực tiếp trong Stack như trên vì Stack không phải bố cục Flex phù hợp để diễn giải ParentData của Expanded.
Cách sử dụng phù hợp hơn:
Column(
children: [
Expanded(
child: Container(
color: Colors.blue,
),
),
],
)
Đặt Positioned sai vị trí
// Không nên
Column(
children: [
Positioned(
top: 10,
child: Container(),
),
],
)
Positioned cần nằm trong phạm vi của Stack phù hợp. Đặt trực tiếp trong Column có thể khiến Flutter báo lỗi ParentData.
Cách sử dụng phù hợp hơn:
Stack(
children: [
Positioned(
top: 10,
left: 10,
child: Container(
width: 100,
height: 50,
color: Colors.red,
),
),
],
)
Trong các lỗi liên quan đến ParentData, cần kiểm tra cấu trúc cây Widget và loại RenderObject cha thay vì chỉ sửa kích thước hoặc màu sắc.
5.9. Nguyên tắc sử dụng trong dự án thực tế
-
Dùng
Expandedkhi cần chiếm phần không gian còn lại trong Flex. -
Dùng
Flexiblekhi muốn phần tử tham gia co giãn nhưng có thể không cần lấp đầy không gian được phân bổ. -
Dùng
Positionedkhi cần đặt thành phần theo các cạnh củaStack. -
Không đặt
ExpandedhoặcFlexibletrực tiếp vào bố cục không phải Flex. -
Không đặt
Positionedtrực tiếp vàoRow,Columnhoặc những bố cục không hỗ trợ ParentData củaStack. -
Kiểm tra ràng buộc kích thước của Widget cha khi gặp lỗi layout.
-
Hạn chế dùng kích thước cố định nếu giao diện cần thích nghi với nhiều kích thước màn hình.
Kết luận: ParentDataWidget là cơ chế giúp Widget con truyền thông tin bố cục đến RenderObject cha. Hiểu rõ Expanded, Flexible, Positioned và quan hệ giữa ParentData với thuật toán layout giúp lập trình viên kiểm soát không gian giao diện chính xác hơn, tránh các lỗi bố cục thường gặp và xây dựng giao diện Flutter có khả năng thích ứng tốt hơn.








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




