StatelessWidget và StatefulWidget trong Flutter: khác nhau thế nào?

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

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à gì?

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

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

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.

Vòng đời cơ bản của State

  • 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.

Cách chọn nhanh

Tình huốngNên dùng
Hiển thị dữ liệu truyền vào, không tự thay đổiStatelessWidget
Có ô nhập, công tắc, animation cục bộStatefulWidget
Dữ liệu dùng chung nhiều màn hìnhGiải pháp quản lý state (Provider, Riverpod...)

Những lỗi thường gặp

  1. Quên gọi setState sau khi đổi dữ liệu.
  2. Gọi setState sau khi widget đã bị dispose, gây cảnh báo hoặc lỗi.
  3. Biến mọi widget thành Stateful dù không cần, làm code khó đọc.
  4. Đặt state quá sâu rồi phải truyền ngược lên nhiều tầng.

Kết luận

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.

Ví dụ thực tế: ô nhập có kiểm tra hợp lệ

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ẹ.

Nâng cấp thiết kế: giữ state ở nơi hợp lý

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.

author_photo
Nguyễn Đình Cường

0 comment