Nguyễn Đình Cường
0 comment
09 Nov, 2026
Khi mới học Flutter, hầu như ai cũng gặp câu hỏi: lúc nào dùng StatelessWidget, lúc nào dùng StatefulWidget? Hiểu đúng sự khác biệt này giúp bạn viết giao diện gọn gàng và tránh những lỗi cập nhật màn hình khó hiểu.
State là dữ liệu có thể thay đổi trong lúc ứng dụng chạy và ảnh hưởng tới giao diện: số lần bấm nút, nội dung ô nhập, công tắc bật hay tắt, danh sách tải về từ máy chủ. Nếu giao diện chỉ phụ thuộc vào dữ liệu được truyền vào và không tự đổi, widget đó không cần state riêng.
StatelessWidget là widget không giữ trạng thái thay đổi. Nó chỉ nhận tham số khi được tạo rồi vẽ ra giao diện tương ứng. Ví dụ một thẻ hiển thị tên sản phẩm và giá:
class ProductCard extends StatelessWidget {
final String name;
final int price;
const ProductCard({super.key, required this.name, required this.price});
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
title: Text(name),
subtitle: Text('$price đ'),
),
);
}
}
Widget này luôn hiển thị đúng dữ liệu truyền vào. Muốn đổi nội dung, widget cha phải tạo lại nó với dữ liệu mới.
StatefulWidget gắn với một đối tượng State tồn tại lâu hơn từng lần build. Khi dữ liệu trong State đổi và bạn gọi setState, Flutter chạy lại phương thức build để cập nhật giao diện.
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _count = 0;
void _increase() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text('Đã bấm $_count lần')),
floatingActionButton: FloatingActionButton(
onPressed: _increase,
child: const Icon(Icons.add),
),
);
}
}
Nếu bạn tăng _count mà không bọc trong setState, giá trị biến có đổi nhưng màn hình sẽ không vẽ lại. Đây là lỗi rất hay gặp ở người mới.
initState(): chạy một lần khi State được tạo, thích hợp để khởi tạo controller hoặc gọi dữ liệu ban đầu.build(): chạy mỗi khi cần vẽ lại, nên giữ nhẹ và không làm việc nặng ở đây.didUpdateWidget(): chạy khi widget cha cung cấp cấu hình mới.dispose(): chạy khi State bị hủy, nơi giải phóng controller, hủy stream hoặc timer.| Tình huống | Nên dùng |
|---|---|
| Hiển thị dữ liệu truyền vào, không tự thay đổi | StatelessWidget |
| Có ô nhập, công tắc, animation cục bộ | StatefulWidget |
| Dữ liệu dùng chung nhiều màn hình | Giải pháp quản lý state (Provider, Riverpod...) |
Quy tắc dễ nhớ: bắt đầu bằng StatelessWidget, chỉ chuyển sang StatefulWidget khi thật sự có dữ liệu thay đổi cục bộ. Khi ứng dụng lớn dần, bạn sẽ muốn tách state ra khỏi widget bằng các thư viện chuyên dụng, nhưng nền tảng này vẫn là điều cần nắm chắc.
Một tình huống rất hay gặp là biểu mẫu có ô nhập và nút gửi chỉ bật khi nội dung hợp lệ. Đây là trường hợp cần StatefulWidget vì bạn phải giữ controller và trạng thái hợp lệ:
class EmailForm extends StatefulWidget {
const EmailForm({super.key});
@override
State<EmailForm> createState() => _EmailFormState();
}
class _EmailFormState extends State<EmailForm> {
final _controller = TextEditingController();
bool _valid = false;
@override
void initState() {
super.initState();
_controller.addListener(() {
final ok = _controller.text.contains('@');
if (ok != _valid) setState(() => _valid = ok);
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(controller: _controller),
ElevatedButton(
onPressed: _valid ? () {} : null,
child: const Text('Gửi'),
),
],
);
}
}
Chú ý ba điểm: controller được tạo một lần và giải phóng trong dispose, listener chỉ gọi setState khi giá trị thật sự đổi để tránh vẽ lại thừa, và nút bị vô hiệu hóa bằng cách truyền null cho onPressed. Ví dụ nhỏ này cho thấy cả vòng đời lẫn cách giữ build nhẹ.
Một câu hỏi tiếp theo thường gặp là state nên nằm ở widget nào. Quy tắc thực dụng: đặt state ở widget thấp nhất mà vẫn bao trùm mọi widget cần dùng nó. Nếu chỉ một ô nhập cần biết nội dung của chính nó, state nằm ngay trong ô đó. Nếu hai widget anh em cùng cần một giá trị, hãy nâng state lên widget cha chung và truyền xuống qua tham số, đồng thời truyền hàm callback để widget con báo thay đổi lên.
Khi thấy mình phải truyền cùng một giá trị qua ba bốn tầng widget chỉ để tới nơi cần dùng, đó là dấu hiệu nên chuyển sang giải pháp quản lý state như Provider hay Riverpod. Mặt khác, đừng nâng state quá cao một cách vô ích: state ở gốc ứng dụng làm mọi thay đổi nhỏ vẽ lại cả cây widget, ảnh hưởng hiệu năng. Ngoài ra, hãy nhớ rằng State của StatefulWidget bị hủy khi widget rời khỏi cây, nên dữ liệu cần sống lâu hơn màn hình phải được lưu ở nơi khác.
Muốn luyện tập với nhiều bài tập thực hành hơn, bạn có thể tìm khóa học Flutter phù hợp tại codebem.com.
Nguyễn Đình Cường
0 comment