Riverpod vs Bloc in 2026: Which Should You Use?
Riverpod and Bloc are the two state management libraries most Flutter teams choose between today. Both are mature, well documented and used in large production apps, so there is no wrong answer. There is, however, usually a better fit for your team and your app. This guide compares them using current versions, Riverpod 3 and Bloc 9, with code you can copy.
The short answer
- Choose Riverpod if you want less code, built-in handling of loading and error states for API data, and easy dependency overrides in tests. It suits small and medium teams and apps that are mostly "fetch data, show data".
- Choose Bloc if you want every change of state to go through an explicit, logged event, and a structure that looks the same in every feature no matter who wrote it. It suits larger teams and apps with complex flows such as checkouts, onboarding wizards or offline sync.
- Consider Cubit (part of the Bloc library) if you like Bloc's structure but not its boilerplate.
Riverpod in one minute
Riverpod was created by Remi Rousselet, the author of Provider, to fix Provider's biggest problems. Providers are declared outside the widget tree, so a missing provider is a compile-time problem rather than a crash at runtime.
A provider can hold a simple value, the result of a Future or Stream, or a Notifier class with methods that change its state. Widgets read providers with ref.watch (rebuild on change) or ref.read (read once, for example in a button handler).
Riverpod 3, released in 2025, added:
- Automatic retry for providers that fail, with exponential backoff you can configure.
- Paused listeners for widgets that are not visible, so off-screen widgets stop rebuilding.
Ref.mounted, to check a provider is still alive after anawait.- A single
Reftype for generated providers, replacing types such asFetchUserRef. - Legacy status for
StateProviderandStateNotifierProvider. They still work, but are now imported fromlegacy.dart;Notifieris the recommended replacement. - Offline persistence and mutations, both still marked experimental.
Bloc in one minute
Bloc was created by Felix Angelov and separates business logic from UI using two building blocks:
- A Cubit exposes methods. Calling a method emits a new state.
- A Bloc receives events and emits states in response. Every change starts with an event object, which you can log, replay and test.
Since Bloc 8, event handlers are registered with on<Event>(...) and emit states through an Emitter, so you no longer need Streams or async* generators to write a Bloc. Bloc 9, released in January 2025 and now on 9.2, mainly tightened listener safety and raised the minimum Dart version, so upgrading from 8 is usually painless.
Boilerplate: a counter
Riverpod 3, with a Notifier:
class Counter extends Notifier<int> {
@override
int build() => 0;
void increment() => state++;
}
final counterProvider = NotifierProvider<Counter, int>(Counter.new);
// In a ConsumerWidget
final count = ref.watch(counterProvider);
ref.read(counterProvider.notifier).increment();
Bloc 9, with a Cubit:
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}
// In a widget below a BlocProvider<CounterCubit>
final count = context.watch<CounterCubit>().state;
context.read<CounterCubit>().increment();
For simple state the two are now very close. The gap only opens up when you use a full Bloc with events, which is where Bloc's extra structure starts to pay for itself.
Async data: loading a user
This is where the day-to-day difference is most visible.
Riverpod 3 with code generation. The provider is a function, and Riverpod gives you loading and error states for free through AsyncValue:
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'user.g.dart';
@riverpod
Future<User> user(Ref ref, int id) async {
final repository = ref.watch(userRepositoryProvider);
return repository.getUser(id);
}
// In a ConsumerWidget
final user = ref.watch(userProvider(1));
return switch (user) {
AsyncData(:final value) => Text(value.name),
AsyncError() => const Text('Could not load this user'),
_ => const CircularProgressIndicator(),
};
Bloc 9 with sealed classes. You describe the events and states yourself, which is more code but makes every possible state explicit:
sealed class UserEvent {}
final class UserRequested extends UserEvent {
UserRequested(this.id);
final int id;
}
sealed class UserState {}
final class UserInitial extends UserState {}
final class UserLoading extends UserState {}
final class UserLoaded extends UserState {
UserLoaded(this.user);
final User user;
}
final class UserFailure extends UserState {
UserFailure(this.message);
final String message;
}
class UserBloc extends Bloc<UserEvent, UserState> {
UserBloc(this._repository) : super(UserInitial()) {
on<UserRequested>(_onRequested);
}
final UserRepository _repository;
Future<void> _onRequested(UserRequested event, Emitter<UserState> emit) async {
emit(UserLoading());
try {
emit(UserLoaded(await _repository.getUser(event.id)));
} catch (e) {
emit(UserFailure(e.toString()));
}
}
}
// In the UI
BlocBuilder<UserBloc, UserState>(
builder: (context, state) => switch (state) {
UserLoaded(:final user) => Text(user.name),
UserFailure() => const Text('Could not load this user'),
_ => const CircularProgressIndicator(),
},
);
Because the state classes are sealed, the Dart compiler warns you if a switch forgets a state. Before Dart 3, most teams used the freezed package to get the same safety.
Testing
Both are easy to unit test without widgets.
Riverpod 3 uses a ProviderContainer. ProviderContainer.test disposes itself when the test ends, and any provider can be swapped for a fake:
test('loads the user', () async {
final container = ProviderContainer.test(
overrides: [userRepositoryProvider.overrideWithValue(FakeUserRepository())],
);
final user = await container.read(userProvider(1).future);
expect(user.name, 'Ada');
});
Bloc uses the bloc_test package. You add events and assert the exact sequence of states:
blocTest<UserBloc, UserState>(
'emits loading then loaded',
build: () => UserBloc(FakeUserRepository()),
act: (bloc) => bloc.add(UserRequested(1)),
expect: () => [isA<UserLoading>(), isA<UserLoaded>()],
);
Riverpod makes it easier to replace dependencies anywhere in the graph. Bloc makes it easier to prove that a flow goes through the right states in the right order.
Rebuilds and performance
Both can limit rebuilds to the widgets that actually use a changed value:
- Riverpod:
ref.watch(userProvider(1).select((u) => u.value?.name))rebuilds only when the name changes. - Bloc:
BlocSelectororcontext.selectdoes the same.
In real apps, the performance difference between the two is negligible. Rebuild problems almost always come from how state is structured, not from the library.
Team fit and maintenance
Bloc is opinionated. Every feature has the same shape, event names read like a log of what the user did, and new developers can find their way around quickly once they know the pattern. That consistency is why many agencies and larger teams standardise on it. Developers coming from Android's MVI or Redux-style architectures usually find it familiar.
Riverpod is more flexible. That makes it faster to work with, but it also means two developers can structure the same feature in quite different ways, so you need your own conventions. Developers coming from React often find its providers familiar, because watching a provider feels similar to using a hook.
Side by side
| Riverpod 3 | Bloc 9 | |
|---|---|---|
| Simple state | Notifier | Cubit |
| Async loading and error states | Built in (AsyncValue) | You model them as states |
| Code generation | Optional, but recommended (@riverpod) | Not needed |
| Dependency injection | Built in (providers) | Usually RepositoryProvider or a service locator |
| Traceability | Provider observers and logs | Every change is an event you can log |
| Learning curve | Easy start, conventions needed | More concepts up front, very consistent |
| Best fit | Small and medium teams, data-heavy screens | Larger teams, complex multi-step flows |
How to decide
Ask three questions:
- Will more than three or four developers work on the app over its life? If yes, Bloc's consistency usually saves more time than its boilerplate costs.
- Are most screens "load data, show it, refresh it"? If yes, Riverpod's
AsyncValuehandling will save you a lot of code. - Do you have flows where the order of steps matters, such as payments, onboarding or offline sync? If yes, Bloc's events make them easier to reason about and test.
If the answers point in different directions, go with what your team already knows. A consistently applied library is worth more than the theoretically better one.
Related resources
- Flutter app publishing checklist
- Common Flutter errors and debugging patterns
- Check and pretty print API responses while you build with our JSON Formatter.
- Planning a Flutter app? Our software cost calculator gives you a realistic price range and timeline.
Frequently Asked Questions
Is Provider deprecated?
No. Provider still works and is still maintained, but its author built Riverpod to fix Provider's main weaknesses, such as runtime ProviderNotFoundException errors. For a new project, Riverpod or Bloc is the better starting point.
Can I use Riverpod and Bloc in the same app?
Technically yes, and it can make sense for a short period while migrating from one to the other. As a long-term setup it makes the codebase harder to learn and review, so pick one for new features.
Does Bloc need code generation?
No. Bloc and Cubit work without any generated code. Many teams used freezed to generate states and events, but Dart 3 sealed classes and pattern matching now cover most of that need without a build step.
Do Riverpod and Bloc work on web and desktop?
Yes. Both are pure Dart state management libraries with Flutter bindings, so the same code runs on iOS, Android, web, macOS, Windows and Linux.
Is Cubit the same as Bloc?
Cubit is part of the Bloc library. A Cubit exposes methods that emit new states directly, while a Bloc receives events and maps them to states. Cubit has far less boilerplate; Bloc gives you a full log of events, which helps with complex flows.
Found this helpful?
Join thousands of developers using our tools to write better code, faster.