← Articles

BLoC vs Riverpod: how to choose for your app

By Charlin Joe · 11 December 2024

BLoC and Riverpod are both excellent state management solutions for Flutter, but they take different philosophies. Choosing the wrong one makes the codebase harder to maintain.

BLoC in a nutshell

BLoC (via flutter_bloc) is event-driven. You define Events, States, and a Bloc that transforms events to states via streams:

// Explicit, typed events
sealed class AuthEvent {}
class LoginRequested extends AuthEvent {
  final String email, password;
  LoginRequested(this.email, this.password);
}
class LogoutRequested extends AuthEvent {}

// Explicit state machine
sealed class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthAuthenticated extends AuthState { final User user; AuthAuthenticated(this.user); }
class AuthError extends AuthState { final String message; AuthError(this.message); }

class AuthBloc extends Bloc<AuthEvent, AuthState> {
  AuthBloc(this._authRepo) : super(AuthInitial()) {
    on<LoginRequested>(_onLogin);
    on<LogoutRequested>(_onLogout);
  }
}

Riverpod in a nutshell

Riverpod is provider-based. State lives in providers that widgets read and watch:

// Notifier handles state
@riverpod
class AuthNotifier extends _$AuthNotifier {
  @override
  AsyncValue<User?> build() => const AsyncValue.data(null);

  Future<void> login(String email, String password) async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(
      () => ref.read(authRepositoryProvider).login(email, password),
    );
  }

  Future<void> logout() async {
    await ref.read(authRepositoryProvider).logout();
    state = const AsyncValue.data(null);
  }
}

Where they differ

Boilerplate

BLoC requires separate files for Events, States, and the Bloc class. Riverpod (with code generation) requires one file and less ceremony. For 20+ features, this adds up.

Testability

Both are highly testable. BLoC testing is very explicit:

blocTest<AuthBloc, AuthState>(
  'emits AuthLoading then AuthAuthenticated on successful login',
  build: () => AuthBloc(mockAuthRepo),
  act: (bloc) => bloc.add(LoginRequested('[email protected]', 'pass')),
  expect: () => [
    isA<AuthLoading>(),
    isA<AuthAuthenticated>(),
  ],
);

Riverpod testing uses dependency overrides:

test('login succeeds', () async {
  final container = ProviderContainer(
    overrides: [
      authRepositoryProvider.overrideWith((_) => MockAuthRepository()),
    ],
  );
  await container.read(authNotifierProvider.notifier).login('[email protected]', 'pass');
  expect(container.read(authNotifierProvider).value, isA<User>());
});

Side effects and navigation

BLoC handles side effects via BlocListener — an explicit widget that reacts to state changes:

BlocListener<AuthBloc, AuthState>(
  listener: (context, state) {
    if (state is AuthAuthenticated) context.go('/home');
    if (state is AuthError) showSnackbar(context, state.message);
  },
  child: const LoginForm(),
)

Riverpod does this with ref.listen:

ref.listen<AsyncValue<User?>>(authNotifierProvider, (prev, next) {
  next.whenOrNull(
    data: (user) { if (user != null) context.go('/home'); },
    error: (e, _) => showSnackbar(context, e.toString()),
  );
});

Dependency injection

Riverpod's provider graph handles DI natively — providers declare their own dependencies:

@riverpod
AuthRepository authRepository(AuthRepositoryRef ref) {
  return AuthRepository(dio: ref.read(dioProvider));
}

BLoC requires explicit DI setup, typically with RepositoryProvider or a separate package.

When to choose BLoC

  • Your team has strong BLoC experience
  • You want an enforced, explicit event log (useful for debugging complex flows)
  • You're building with a large team and want the structure to prevent anti-patterns
  • You need precise control over event processing (debounce, concurrency transformers)
  • The app has complex state machines with many transitions

When to choose Riverpod

  • You're starting a new project and want less boilerplate
  • The app has many independent features with their own state
  • You value ergonomic AsyncValue handling (loading/error/data in one type)
  • Your team prefers functional-style code over class hierarchies
  • You need fine-grained dependency injection without a separate DI framework

Mixing them

Don't mix BLoC and Riverpod in the same app — pick one. The only exception is using Riverpod's Provider just for DI while managing state with BLoC, but this adds complexity without much gain.

Common pitfalls

Choosing based on hype rather than team fit. Both are production-proven. The biggest factor is what your team already knows. A team that knows BLoC well will be more productive with BLoC than learning Riverpod from scratch.

Overusing BLoC for simple state. A toggle or form field doesn't need a Bloc with 3 events and 4 states. Local setState is fine for local UI state regardless of which global state manager you use.

Using Riverpod without code generation. Riverpod 2.x's @riverpod annotation with build_runner removes most boilerplate. Using Riverpod without code gen means writing significantly more manual provider code.

Sign in to like, dislike, or report.

BLoC vs Riverpod: how to choose for your app — ANN Tech