Riverpod vs BLoC trong Flutter: Nên chọn State Management nào cho dự án lớn?

2026-07-30 12 phút đọc 0 lượt xemBởi Uwmee Software

Một trong những quyết định kiến trúc quan trọng nhất khi bắt đầu một dự án Flutter là lựa chọn giải pháp State Management (Quản lý trạng thái). Một mô hình chuẩn giúp ứng dụng dễ dàng mở rộng, dễ viết Unit Test và loại bỏ hoàn toàn các lỗi re-render thừa.

1. Provider: Giải pháp nhập môn đơn giản

Provider là thư viện dựa trên InheritedWidget của Flutter. Dễ học, phù hợp cho các ứng dụng nhỏ. Tuy nhiên, điểm yếu là phụ thuộc vào BuildContext và dễ gặp lỗi ProviderNotFoundException khi ứng dụng phình to.

2. BLoC (Business Logic Component): Chuẩn mực cho Enterprise

BLoC tách biệt 100% giữa giao diện UI và logic nghiệp vụ bằng cơ chế Event ➔ BLoC ➔ State dựa trên Reactive Stream (RxDart). BLoC cực kỳ chặt chẽ, dễ viết Unit Test và là sự lựa chọn số 1 cho các ngân hàng và tập đoàn lớn.

3. Riverpod v2: Sự tiến hóa hoàn hảo của Provider

Được chính tác giả của Provider viết lại, Riverpod v2 giải quyết triệt để các nhược điểm của Provider: KHÔNG phụ thuộc vào BuildContext, kiểm tra lỗi compile-time an toàn 100% và hỗ trợ Auto-Dispose cache thông minh.

4. Bảng so sánh BLoC vs Riverpod vs Provider

Tiêu chíProviderRiverpod v2BLoC Pattern
Phụ thuộc BuildContextCÓ (dễ lỗi)KHÔNG (Truy cập tự do)CÓ (dùng BlocProvider)
Độ rườm rà (Boilerplate)Rất ítTrung bình (với Code Gen)Nhiều (Event/State)
Khả năng Unit TestKháRất dễ (Override Provider)Tối thượng (bloc_test)
Phù hợp cho dự ánApp nhỏ / DemoApp vừa và lớnApp Enterprise khổng lồ

5. Mã nguồn thực tế: Cùng một màn hình đếm số viết bằng 2 thư viện

Cách viết bằng Riverpod v2 (Notifier)

// 1. Khai báo Notifier, không cần BuildContext
class CounterNotifier extends Notifier<int> {
  @override
  int build() => 0;

  void increment() => state = state + 1;
}

final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);

// 2. Dùng trong Widget
class CounterView extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return ElevatedButton(
      onPressed: () => ref.read(counterProvider.notifier).increment(),
      child: Text('Da bam: ' + count.toString()),
    );
  }
}

Cách viết bằng BLoC (Event ➔ State)

// 1. Khai bao Event
abstract class CounterEvent {}
class IncrementPressed extends CounterEvent {}

// 2. Khai bao Bloc
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<IncrementPressed>((event, emit) => emit(state + 1));
  }
}

// 3. Dung trong Widget
BlocBuilder<CounterBloc, int>(
  builder: (context, count) => ElevatedButton(
    onPressed: () => context.read<CounterBloc>().add(IncrementPressed()),
    child: Text('Da bam: ' + count.toString()),
  ),
);

Nhìn vào 2 đoạn mã trên có thể thấy ngay điểm mấu chốt: Riverpod gọi hàm trực tiếp, còn BLoC bắt buộc đi qua một lớp Event trung gian. Với một tính năng đơn giản, BLoC dài dòng hơn; nhưng với một luồng nghiệp vụ 20 trạng thái, lớp Event chính là thứ giúp cả team không viết lộn logic của nhau.

6. Xử lý gọi API bất đồng bộ: nơi khác biệt lộ rõ nhất

Màn hình đếm số ở trên quá đơn giản để thấy sự khác biệt thật. Hãy xem một tình huống mà mọi ứng dụng đều phải làm: gọi API lấy danh sách sản phẩm và hiển thị 3 trạng thái loading, thành công, lỗi.

Riverpod xử lý bằng AsyncNotifier

class ProductList extends AsyncNotifier<List<Product>> {
  @override
  Future<List<Product>> build() async {
    return ref.read(repoProvider).fetchProducts();
  }
}

final productProvider = AsyncNotifierProvider<ProductList, List<Product>>(ProductList.new);

// Trong Widget, 3 trang thai duoc xu ly gon trong mot khoi when()
ref.watch(productProvider).when(
  loading: () => const CircularProgressIndicator(),
  error: (e, _) => Text('Loi: ' + e.toString()),
  data: (items) => ProductGrid(items: items),
);

Riverpod bọc sẵn ba trạng thái vào lớp AsyncValue. Bạn không phải tự khai báo lớp Loading, lớp Error hay lớp Success nào cả.

BLoC xử lý bằng các lớp State tường minh

abstract class ProductState {}
class ProductLoading extends ProductState {}
class ProductError extends ProductState { final String msg; ProductError(this.msg); }
class ProductLoaded extends ProductState { final List<Product> items; ProductLoaded(this.items); }

class ProductBloc extends Bloc<ProductEvent, ProductState> {
  ProductBloc(this.repo) : super(ProductLoading()) {
    on<FetchProducts>((event, emit) async {
      emit(ProductLoading());
      try {
        emit(ProductLoaded(await repo.fetchProducts()));
      } catch (e) {
        emit(ProductError(e.toString()));
      }
    });
  }
  final ProductRepository repo;
}

Rõ ràng BLoC viết dài hơn nhiều. Nhưng đổi lại, mỗi trạng thái là một lớp có tên gọi cụ thể, có thể mang theo dữ liệu riêng (ví dụ thêm lớp ProductLoadingMore cho tình huống tải thêm trang). Khi nghiệp vụ có 8 trạng thái khác nhau thay vì 3, cách của BLoC mới thực sự phát huy giá trị.

7. So sánh khả năng viết Unit Test

Đây là tiêu chí quyết định với các dự án dài hạn, vì code không test được sẽ không dám sửa.

// Test Riverpod: ghi de provider bang du lieu gia
final container = ProviderContainer(
  overrides: [repoProvider.overrideWithValue(FakeRepo())],
);
expect(await container.read(productProvider.future), hasLength(3));

// Test BLoC: dung goi bloc_test, kiem tra chuoi state phat ra
blocTest<ProductBloc, ProductState>(
  'phat ra Loading roi Loaded',
  build: () => ProductBloc(FakeRepo()),
  act: (bloc) => bloc.add(FetchProducts()),
  expect: () => [isA<ProductLoading>(), isA<ProductLoaded>()],
);

Cả hai đều test được dễ dàng, nhưng theo hai triết lý khác nhau: Riverpod test kết quả cuối cùng, BLoC test toàn bộ chuỗi trạng thái đã đi qua. Với app tài chính cần chứng minh nghiệp vụ đã chạy đúng từng bước, cách của BLoC có giá trị pháp lý hơn. Xem thêm bài quy trình viết test toàn diện cho Flutter.

8. Bốn lỗi thường gặp ở cả hai thư viện

  • Gọi ref.watch trong hàm xử lý sự kiện: Phải dùng ref.read trong callback onPressed, dùng ref.watch ở đó sẽ tạo vòng lặp rebuild vô tận.
  • Đặt provider hoặc bloc quá cao trong cây widget: Một thay đổi nhỏ khiến cả màn hình rebuild, gây giật lag. Hãy bọc càng sát widget cần dữ liệu càng tốt, xem thêm bài tối ưu hiệu năng Flutter 60fps.
  • Nhét logic gọi API trực tiếp vào Notifier hoặc Bloc: Luôn tách qua lớp Repository để sau này đổi nguồn dữ liệu không phải sửa lớp state.
  • Quên hủy đăng ký stream trong BLoC: Phải override hàm close() để giải phóng tài nguyên, nếu không sẽ rò rỉ bộ nhớ khi người dùng vào ra màn hình nhiều lần.

9. Nên chọn Riverpod hay BLoC? Bảng quyết định theo quy mô dự án

Tình huống dự ánKhuyến nghịLý do
App MVP, khởi nghiệp cần ra mắt trong 1-2 thángRiverpodÍt boilerplate, viết nhanh, dễ đổi hướng khi yêu cầu thay đổi liên tục
App có nhiều luồng gọi API bất đồng bộ, cần cache dữ liệuRiverpodAsyncNotifier và cơ chế Auto-Dispose xử lý loading/error/cache sẵn
App tài chính, ngân hàng, y tế cần audit từng bước nghiệp vụBLoCMỗi hành động là một Event có thể log lại, tái hiện và kiểm toán được
Team trên 5 lập trình viên, nhiều người mới vào dự ánBLoCQuy ước nghiêm ngặt buộc mọi người viết cùng một khuôn, dễ review code
Đội ngũ đã quen Provider, muốn nâng cấp dầnRiverpodCùng tác giả, khái niệm gần giống nên chi phí học lại thấp nhất

10. Chuyển từ BLoC sang Riverpod (hoặc ngược lại) có khó không?

Chi phí chuyển đổi phụ thuộc vào việc trước đó bạn có tách lớp nghiệp vụ ra khỏi UI hay không. Nếu dự án đã tuân theo Clean Architecture chia lớp Data - Domain - Presentation, phần Repository và UseCase được giữ nguyên 100%, bạn chỉ viết lại lớp Presentation. Ngược lại, nếu logic gọi API nằm lẫn trong Widget thì việc chuyển đổi gần như là viết lại toàn bộ.

Lời khuyên thực tế: đừng chuyển đổi giữa dự án chỉ vì thư viện mới trông hấp dẫn hơn. Hãy chốt một thư viện ngay từ ngày đầu và viết Unit Test cho lớp state để bảo vệ nghiệp vụ về sau.

Kết luận

Riverpod thắng về tốc độ phát triển và sự linh hoạt; BLoC thắng về tính quy chuẩn và khả năng kiểm toán nghiệp vụ. Không có lựa chọn nào sai, chỉ có lựa chọn không phù hợp với quy mô đội ngũ và độ phức tạp nghiệp vụ của bạn. Uwmee Software áp dụng kiến trúc Riverpod & BLoC chuẩn mực cho các sản phẩm app Flutter. Đọc thêm bài viết tối ưu hiệu năng Flutter.

Câu hỏi thường gặp

Bạn cần giải pháp phần mềm chuyên nghiệp?

Uwmee Software cung cấp dịch vụ 12 tester Google Play & thiết kế website cao cấp giúp nâng tầm thương hiệu của bạn.