Widget Tree là cấu trúc phân cấp của các widget trong Flutter, xác định bố cục của giao diện người dùng. Mỗi phần tử của giao diện, từ một nút bấm đến toàn bộ màn hình, được đại diện bởi một widget riêng biệt được lồng trong một container cha. Flutter cập nhật Widget Tree mỗi khi trạng thái thay đổi — framework so sánh cây mới và cây cũ và áp dụng các thay đổi tối thiểu. Theo Flutter Team, 2025, cấu trúc cây hiệu quả ảnh hưởng trực tiếp đến độ mượt của hoạt ảnh và khả năng phản hồi của giao diện.
Những điểm chính
Widget Tree là một mô tả khai báo của giao diện người dùng trong Flutter, được xây dựng dưới dạng cây các widget lồng nhau. Mỗi widget xác định một phần của UI: cấu hình, tham số hiển thị và hành vi khi tương tác. Nhà phát triển mô tả giao diện sẽ trông như thế nào ở trạng thái ứng dụng hiện tại và Flutter chịu trách nhiệm chuyển đổi mô tả đó thành pixel trên màn hình.
Không giống như các framework mệnh lệnh nơi nhà phát triển trực tiếp thao tác các phần tử giao diện, Flutter sử dụng cách tiếp cận khai báo. Khi trạng thái ứng dụng thay đổi, một Widget Tree mới được tạo ra và framework tính toán sự khác biệt giữa cây cũ và cây mới. Điều này giảm thiểu số lượng thao tác kết xuất và làm cho mã dễ dự đoán hơn.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("Widget Tree")),
body: Center(
child: Column(
children: [
Text("Xin chào, Flutter"),
ElevatedButton(
onPressed: () {},
child: Text("Nhấn tôi"),
),
],
),
),
),
);
}
}
Trong ví dụ này, Widget Tree bao gồm MaterialApp, Scaffold, AppBar, Center, Column, Text và ElevatedButton. Mỗi widget này là một nút trong cây. Khi trạng thái ứng dụng thay đổi, Flutter gọi lại phương thức build và so sánh kết quả với cây trước đó.
Widget Tree bắt đầu với một widget gốc được truyền vào phương thức runApp. Widget gốc thường là MaterialApp, CupertinoApp hoặc WidgetsApp — nó thiết lập các cài đặt toàn cục của ứng dụng. Từ gốc, cây phân nhánh thành các widget con, mỗi widget có thể chứa các phần tử con riêng.
Các widget trong Flutter được chia thành single-child (nhận một con qua tham số child) và multi-child (nhận danh sách con qua children). Ví dụ single-child: Center, Padding, SizedBox, Container. Multi-child: Column, Row, Stack, ListView, GridView. Sự khác biệt này ảnh hưởng đến cấu trúc Widget Tree: widget multi-child tạo cây rộng hơn, trong khi single-child tạo cây sâu hơn.
BuildContext là vị trí của một widget trong Widget Tree. Mỗi widget có BuildContext riêng, được truyền vào phương thức build và dùng để truy cập widget cha, theme, MediaQuery và các InheritedWidget khác. BuildContext đóng vai trò cầu nối giữa widget và phần tử của nó trong Element Tree.
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
return Container(
color: theme.colorScheme.primary,
child: Text(
"Chiều rộng màn hình: ${mediaQuery.size.width}"
),
);
}
}
Trong ví dụ này, BuildContext được sử dụng để lấy theme và kích thước màn hình. Flutter đi lên Widget Tree đến Theme và MediaQuery gần nhất, là các InheritedWidget. Điều này cho thấy cách context kết nối một widget với vị trí của nó trong hệ thống phân cấp.
Khi một ứng dụng Flutter khởi động, hàm runApp được gọi, nhận widget gốc và bắt đầu xây dựng Widget Tree. Quá trình bao gồm ba giai đoạn: tạo cấu hình widget, hình thành Element Tree và xây dựng RenderObject Tree để kết xuất thực tế.
Hàm runApp tạo một phần tử gốc thông qua WidgetsFlutterBinding, kết nối framework với công cụ đồ họa. Widget gốc được đặt vào cây và Flutter gọi phương thức build để lấp đầy nó bằng các widget con. Mỗi lần gọi build tạo ra một đồ thị con mới của Widget Tree.
Sau khi xây dựng Widget Tree, Flutter thực hiện bố cục ban đầu — tính toán kích thước và vị trí của tất cả widget. Quá trình này bắt đầu từ gốc và lan truyền xuống dưới cây. Mỗi widget nhận các ràng buộc từ cha và trả về kích thước đã tính. Nếu kích thước không khớp, Flutter tạo ra lỗi bố cục.
Sau khi hoàn tất bố cục, Flutter tiến hành kết xuất từng widget. RenderObject chuyển đổi mô tả giao diện thành các lệnh đồ họa được thực thi bởi GPU qua Skia hoặc Impeller. Toàn bộ quá trình — từ Widget Tree đến pixel — lặp lại mỗi khi trạng thái thay đổi với tốc độ lên đến 120 khung hình mỗi giây.
StatelessWidget là widget không có trạng thái nội bộ có thể thay đổi. Giao diện của nó hoàn toàn được xác định bởi các tham số đầu vào được truyền qua hàm tạo. Nếu các tham số không thay đổi, StatelessWidget không được xây dựng lại. Điều này làm cho nó nhẹ về mặt hiệu suất.
Sử dụng StatelessWidget cho các phần tử giao diện tĩnh: biểu tượng, nhãn văn bản, dấu phân cách trang trí và nút đơn giản không có logic nội bộ. Theo tài liệu Flutter, khoảng 70% widget trong một ứng dụng điển hình có thể là StatelessWidget, giảm tải cho trình dọn rác và tăng tốc rebuild.
StatefulWidget tạo một đối tượng State tồn tại giữa các lần xây dựng lại widget. Khi trạng thái thay đổi (qua setState), Flutter đánh dấu widget là “bẩn” và xây dựng lại nó ở khung hình tiếp theo. StatefulWidget cho phép các phần tử tương tác: trường nhập liệu, hoạt ảnh, bộ đếm thời gian và danh sách động.
class CounterWidget extends StatefulWidget {
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text("Đếm: $_count"),
ElevatedButton(
onPressed: () {
setState(() => _count++);
},
child: Text("Tăng"),
),
],
);
}
}
Trong ví dụ này, StatefulWidget sử dụng setState để cập nhật bộ đếm. Khi trạng thái được cập nhật, Flutter chỉ xây dựng lại phần đã thay đổi của Widget Tree — CounterWidget và các phần tử con của nó. Các widget cha không bị xây dựng lại, đây là lợi thế chính của mô hình khai báo của Flutter.
Widget Tree là lớp cấu hình, trong khi Element Tree là liên kết trung gian giữa các widget và kết xuất thực tế. Mỗi widget trong Widget Tree tạo một phần tử trong Element Tree, lưu trữ tham chiếu đến widget và quản lý vòng đời của nó. Kiến trúc này cho phép Flutter xử lý các thay đổi một cách hiệu quả.
Khi Flutter gặp một widget lần đầu tiên, nó gọi phương thức createElement, tạo ra một phần tử tương ứng. Đối với StatelessWidget, StatelessElement được tạo; đối với StatefulWidget, StatefulElement được tạo, cũng khởi tạo một đối tượng State. Phần tử tồn tại giữa các chu kỳ xây dựng lại, ngay cả khi widget được tạo lại.
Key là định danh giúp Flutter khớp các widget từ Widget Tree cũ và mới. Nếu một widget có Key, Flutter sử dụng nó để tìm phần tử tương ứng thay vì vị trí của nó trong cây. Các khóa cần thiết khi làm việc với danh sách động nơi thứ tự các phần tử có thể thay đổi.
ListView(
children: items.map((item) => ListItem(
key: ValueKey(item.id),
data: item,
)).toList(),
)
Không có Key, Flutter khớp các phần tử theo vị trí, có thể dẫn đến việc bảo toàn trạng thái không chính xác khi thay đổi thứ tự. ValueKey với định danh duy nhất đảm bảo mỗi phần tử giữ nguyên trạng thái bất kể vị trí của nó trong danh sách.
Cấu trúc Widget Tree ảnh hưởng trực tiếp đến hiệu suất ứng dụng Flutter. Cây sâu với nhiều widget lồng nhau cần nhiều thời gian hơn cho giai đoạn bố cục và tăng mức sử dụng bộ nhớ. Flutter DevTools cung cấp các công cụ để phân tích Widget Tree và xác định các điểm nghẽn.
Mỗi cấp độ lồng ghép thêm các tính toán bổ sung trong quá trình bố cục và vẽ. Thay vì lồng ghép chuỗi sâu, hãy sử dụng cấu trúc phẳng hơn. Ví dụ: Row với Expanded có thể thay thế nhiều Container lồng nhau với Align. Theo Flutter Team, tối ưu hóa cây có thể giảm thời gian bố cục lên đến 40%.
Flutter DevTools cung cấp công cụ “Widget Inspector”, hiển thị Widget Tree hiện tại theo thời gian thực. Nhà phát triển có thể chọn bất kỳ widget nào trên màn hình và xem vị trí của nó trong cây, tham số và ràng buộc bố cục. Điều này giúp xác định lồng ghép bất ngờ, rebuild quá mức và vấn đề về kích thước.
RepaintBoundary là widget cô lập một phần của Widget Tree để kết xuất độc lập. Nếu nội dung bên trong RepaintBoundary thay đổi, chỉ khu vực đó được vẽ lại, không phải toàn bộ màn hình. Sử dụng RepaintBoundary cho hoạt ảnh, danh sách và các phần tử thường xuyên cập nhật khác.
RepaintBoundary(
child: CustomPaint(
painter: MyPainter(),
child: SizedBox(
width: 200,
height: 200,
),
),
)
Trong ví dụ này, RepaintBoundary cô lập CustomPaint vào một khu vực kết xuất riêng biệt. Khi hoạt ảnh trong khu vực này được cập nhật, chỉ widget CustomPaint được vẽ lại, phần còn lại của màn hình không thay đổi. Điều này đặc biệt hữu ích trong các giao diện phức tạp với nhiều phần tử hoạt ảnh.
Câu hỏi thường gặp
Widget Tree là mô tả giao diện khai báo được tạo lại mỗi lần rebuild. Element Tree tồn tại giữa các lần cập nhật và quản lý vòng đời, trạng thái và ánh xạ widget với RenderObject thực tế.
Không có giới hạn về số lượng widget, nhưng trên thực tế, cây với hàng nghìn widget có thể làm chậm giai đoạn bố cục. Flutter được tối ưu cho cây lên đến vài nghìn nút; với số lượng lớn hơn, nên sử dụng ảo hóa qua ListView.builder.
Sử dụng Flutter DevTools — tab “Widget Inspector”. Chạy ứng dụng ở chế độ gỡ lỗi, mở DevTools trong trình duyệt và chọn bất kỳ widget nào trên màn hình để xem vị trí của nó trong Widget Tree.
Rebuild là quá trình tạo lại cấu hình widget khi trạng thái thay đổi. Flutter gọi lại phương thức build cho các widget đã thay đổi, so sánh Widget Tree mới với cây trước đó và áp dụng các thay đổi tối thiểu lên Element Tree.
Giảm độ sâu lồng ghép, sử dụng const widget cho phần tử tĩnh, áp dụng RepaintBoundary để cô lập hoạt ảnh và tránh StatefulWidget quá mức khi StatelessWidget là đủ.
Tóm tắt
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm