Flutter State Management: BLoC vs Riverpod
Choosing the right state management solution is one of the most important architectural decisions in a Flutter project. Having built production apps with both BLoC and Riverpod, here is my honest comparison.
BLoC Pattern
BLoC (Business Logic Component) uses streams and events for predictable state management.
How It Works
1// Events
2abstract class ProjectEvent {}
3class LoadProjects extends ProjectEvent {}
4class FilterByTech extends ProjectEvent {
5 final String technology;
6 FilterByTech(this.technology);
7}
8
9// States
10abstract class ProjectState {}
11class ProjectLoading extends ProjectState {}
12class ProjectLoaded extends ProjectState {
13 final List<Project> projects;
14 ProjectLoaded(this.projects);
15}
16class ProjectError extends ProjectState {
17 final String message;
18 ProjectError(this.message);
19}
20
21// BLoC
22class ProjectBloc extends Bloc<ProjectEvent, ProjectState> {
23 final ProjectRepository repository;
24
25 ProjectBloc(this.repository) : super(ProjectLoading()) {
26 on<LoadProjects>((event, emit) async {
27 emit(ProjectLoading());
28 try {
29 final projects = await repository.fetchAll();
30 emit(ProjectLoaded(projects));
31 } catch (e) {
32 emit(ProjectError(e.toString()));
33 }
34 });
35
36 on<FilterByTech>((event, emit) async {
37 emit(ProjectLoading());
38 final projects = await repository.fetchByTechnology(event.technology);
39 emit(ProjectLoaded(projects));
40 });
41 }
42}BLoC Pros
- Predictable: Events in, states out — easy to trace
- Testing: Excellent with
bloc_testpackage - DevTools: Built-in time-travel debugging
- Separation of concerns: Forces clean architecture
BLoC Cons
- Boilerplate: Event classes, state classes, bloc class for each feature
- Learning curve: Streams, sinks, and reactive programming concepts
- Overkill for simple state: A counter does not need events/states
Riverpod
Riverpod is a reactive caching and data-binding framework that feels more natural in Dart.
How It Works
1// Provider
2final projectRepositoryProvider = Provider<ProjectRepository>((ref) {
3 return ProjectRepositoryImpl(ref.read(apiClientProvider));
4});
5
6final projectsProvider = FutureProvider.autoDispose<List<Project>>((ref) async {
7 final repository = ref.read(projectRepositoryProvider);
8 return repository.fetchAll();
9});
10
11final filteredProjectsProvider = FutureProvider.autoDispose.family<List<Project>, String>(
12 (ref, technology) async {
13 final repository = ref.read(projectRepositoryProvider);
14 return repository.fetchByTechnology(technology);
15 },
16);
17
18// Usage in Widget
19class ProjectListPage extends ConsumerWidget {
20
21 Widget build(BuildContext context, WidgetRef ref) {
22 final projectsAsync = ref.watch(projectsProvider);
23
24 return projectsAsync.when(
25 data: (projects) => ListView.builder(
26 itemCount: projects.length,
27 itemBuilder: (_, i) => ProjectCard(projects[i]),
28 ),
29 loading: () => const CircularProgressIndicator(),
30 error: (err, stack) => ErrorWidget(err.toString()),
31 );
32 }
33}Riverpod Pros
- Less boilerplate: Define a provider, use it — done
- Compile-time safety: No runtime errors from missing providers
- Auto-dispose: Memory management built-in
- Code generation:
riverpod_generatorreduces code further
Riverpod Cons
- Less structure: Freedom can lead to messy code without discipline
- Debugging: No built-in time-travel debugging
- Learning curve: Provider types (Provider, StateProvider, FutureProvider, etc.)
My Recommendation
| Criteria | BLoC | Riverpod |
|---|---|---|
| Large team | ✅ Better | ⚠️ Needs conventions |
| Solo/small team | ⚠️ Overhead | ✅ Faster |
| Complex flows | ✅ Better | ⚠️ Can get messy |
| Simple CRUD | ⚠️ Overkill | ✅ Perfect |
| Testing | ✅ Excellent | ✅ Good |
Use BLoC for enterprise apps with complex business logic and large teams. Use Riverpod for smaller to medium apps where speed matters.
Both are excellent choices — the best one depends on your team and project needs.