Riverpod vs BLoC trong Flutter: Nên chọn State Management nào cho dự án lớn?
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í | Provider | Riverpod v2 | BLoC Pattern |
|---|---|---|---|
| Phụ thuộc BuildContext | CÓ (dễ lỗi) | KHÔNG (Truy cập tự do) | CÓ (dùng BlocProvider) |
| Độ rườm rà (Boilerplate) | Rất ít | Trung bình (với Code Gen) | Nhiều (Event/State) |
| Khả năng Unit Test | Khá | Rất dễ (Override Provider) | Tối thượng (bloc_test) |
| Phù hợp cho dự án | App nhỏ / Demo | App vừa và lớn | App 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.readtrong callback onPressed, dùngref.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ự án | Khuyến nghị | Lý do |
|---|---|---|
| App MVP, khởi nghiệp cần ra mắt trong 1-2 tháng | Riverpod | Í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ệu | Riverpod | AsyncNotifier 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ụ | BLoC | Mỗ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ự án | BLoC | Quy ướ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ần | Riverpod | Cù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.