Quản lý state trong Flutter: setState, Provider hay Riverpod?

  • author-image

    Nguyễn Đình Cường

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

Ứng dụng Flutter nhỏ có thể sống ổn với setState, nhưng khi nhiều màn hình cùng dùng chung dữ liệu, bạn sẽ cần một cách quản lý state có tổ chức. Bài viết giúp bạn hiểu các lựa chọn phổ biến và biết khi nào nên dùng cái nào.

Vì sao setState không đủ?

setState phù hợp với trạng thái cục bộ của một widget, như bật tắt một ô hay đếm số. Vấn đề xuất hiện khi giỏ hàng, thông tin đăng nhập hoặc cài đặt giao diện cần được nhiều màn hình đọc và sửa. Lúc đó bạn phải truyền dữ liệu qua nhiều tầng widget (prop drilling), code rối và dễ lỗi.

Ý tưởng chung của các thư viện

Hầu hết thư viện đều làm cùng một việc: đưa state ra khỏi widget, đặt nó ở nơi có thể chia sẻ, và tự động báo cho các widget quan tâm vẽ lại khi dữ liệu đổi. Khác biệt chủ yếu nằm ở cách viết và mức độ cấu trúc.

Provider với ChangeNotifier

Provider là giải pháp đơn giản, được nhiều người dùng đầu tiên. Bạn tạo một lớp kế thừa ChangeNotifier chứa dữ liệu và gọi notifyListeners() khi có thay đổi.

class CartModel extends ChangeNotifier {
  final List<String> _items = [];

  List<String> get items => List.unmodifiable(_items);

  void add(String item) {
    _items.add(item);
    notifyListeners();
  }
}

// Cung cấp ở gốc ứng dụng
ChangeNotifierProvider(
  create: (_) => CartModel(),
  child: const MyApp(),
)

// Đọc trong widget
final cart = context.watch<CartModel>();
Text('Số món: ${cart.items.length}')

Ưu điểm là dễ học, ít code. Nhược điểm là phụ thuộc vào context, và khi dự án lớn thì cần kỷ luật để không dồn mọi thứ vào vài lớp khổng lồ.

Riverpod

Riverpod do cùng tác giả với Provider phát triển, khắc phục nhiều hạn chế: không phụ thuộc vào cây widget để khai báo, kiểm tra lỗi lúc biên dịch tốt hơn, dễ kiểm thử và dễ xử lý dữ liệu bất đồng bộ như kết quả gọi API (loading, data, error). Đổi lại, bạn cần học thêm một số khái niệm như provider, ref và các loại notifier.

Bloc

Bloc áp dụng mô hình sự kiện và trạng thái: giao diện gửi event, Bloc xử lý và phát ra state mới. Cách này rất rõ ràng và dễ theo dõi, phù hợp với nhóm lớn cần quy ước chặt, nhưng khá nhiều code mẫu so với các lựa chọn khác.

Nên chọn gì?

Bối cảnhGợi ý
Học và làm ứng dụng nhỏsetState, sau đó Provider
Dự án vừa, nhiều dữ liệu bất đồng bộRiverpod
Nhóm lớn, cần quy trình chặt chẽBloc hoặc Riverpod

Không có lựa chọn đúng tuyệt đối. Quan trọng là cả nhóm thống nhất một cách và áp dụng nhất quán.

Nguyên tắc dùng được với mọi thư viện

  • Giữ logic nghiệp vụ ngoài widget để dễ kiểm thử.
  • Chỉ lắng nghe phần dữ liệu cần thiết để tránh vẽ lại không cần thiết.
  • Trạng thái chỉ nên có một nguồn sự thật, đừng sao chép dữ liệu ở nhiều nơi.
  • Xử lý rõ ba trạng thái: đang tải, thành công, lỗi.

Kết luận

Đừng sa đà vào việc so sánh thư viện quá lâu. Hãy chọn một cái, làm một dự án thật và rút kinh nghiệm. Khi hiểu bản chất, chuyển từ thư viện này sang thư viện khác sẽ không khó.

Ví dụ Riverpod cho dữ liệu bất đồng bộ

Điểm mạnh dễ thấy của Riverpod là cách xử lý kết quả gọi API với ba trạng thái rõ ràng. Giả sử bạn có hàm fetchPosts trả về Future:

final postsProvider = FutureProvider<List<Post>>((ref) async {
  return fetchPosts();
});

class PostsPage extends ConsumerWidget {
  const PostsPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final posts = ref.watch(postsProvider);

    return posts.when(
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (e, st) => Center(child: Text('Lỗi: $e')),
      data: (items) => ListView(
        children: [for (final p in items) ListTile(title: Text(p.title))],
      ),
    );
  }
}

Phương thức when buộc bạn xử lý đủ cả ba trường hợp, nên khó quên màn hình lỗi hay màn hình chờ. Để làm mới dữ liệu, gọi ref.invalidate(postsProvider). Cách làm này thay cho đoạn FutureBuilder thủ công và đặt logic tải dữ liệu ra khỏi widget, giúp kiểm thử dễ hơn rất nhiều.

Lộ trình chuyển đổi dần dần

Bạn không cần chọn đúng thư viện ngay ngày đầu rồi viết lại tất cả. Một lộ trình êm ái là: bắt đầu bằng setState cho trạng thái cục bộ, khi thấy dữ liệu cần chia sẻ thì chuyển các phần đó sang Provider hoặc Riverpod, còn phần cục bộ vẫn để nguyên. Hai cách sống chung hoàn toàn bình thường trong cùng một ứng dụng.

Khi đã dùng một thư viện, hãy tập thói quen tách mô hình dữ liệu bất biến (immutable): thay vì sửa trực tiếp danh sách, tạo bản sao mới với thay đổi. Điều này giúp thư viện phát hiện thay đổi chính xác và tránh lỗi giao diện không cập nhật. Cuối cùng, viết thử bài kiểm thử cho lớp quản lý state; đó là phép thử xem thiết kế của bạn đã tách khỏi giao diện đủ tốt hay chưa.

Nếu bạn cần lộ trình có hướng dẫn cho phần kiến trúc ứng dụng Flutter, hãy ghé qua các khóa học trên codebem.com để xem nội dung phù hợp.

author_photo
Nguyễn Đình Cường

0 comment