DEV Community

kanta13jp1
kanta13jp1

Posted on

Flutter State Management Deep Dive — Riverpod vs BLoC vs Provider

Flutter State Management Deep Dive — Riverpod vs BLoC vs Provider

Flutter state management debates can get heated, but the right choice depends on team size, testability needs, and async complexity. Here's an objective comparison.

Counter Example: Three Approaches

Riverpod 2.0 (my recommendation)

// providers.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'providers.g.dart';

@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0;
  void increment() => state++;
  void decrement() => state--;
}

// UI
class CounterWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Column(children: [
      Text('$count'),
      ElevatedButton(
        onPressed: () => ref.read(counterProvider.notifier).increment(),
        child: const Text('+'),
      ),
    ]);
  }
}
Enter fullscreen mode Exit fullscreen mode

BLoC

// counter_bloc.dart
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>((event, emit) => emit(state + 1));
    on<Decrement>((event, emit) => emit(state - 1));
  }
}

sealed class CounterEvent {}
class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}

// UI
BlocBuilder<CounterBloc, int>(
  builder: (context, count) => Column(children: [
    Text('$count'),
    ElevatedButton(
      onPressed: () => context.read<CounterBloc>().add(Increment()),
      child: const Text('+'),
    ),
  ]),
)
Enter fullscreen mode Exit fullscreen mode

Async Data: Where Riverpod Shines

// Auto-handles loading/error states
@riverpod
Future<List<Task>> tasks(TasksRef ref) async {
  final client = ref.watch(supabaseProvider);
  final res = await client.from('tasks').select().order('created_at');
  return res.map(Task.fromJson).toList();
}

// UI: pattern-match on AsyncValue
ref.watch(tasksProvider).when(
  data: (tasks) => TaskList(tasks: tasks),
  loading: () => const CircularProgressIndicator(),
  error: (e, _) => ErrorWidget(message: e.toString()),
)
Enter fullscreen mode Exit fullscreen mode

Dependency Injection

// Riverpod: automatic dependency resolution
@riverpod
SupabaseClient supabase(SupabaseRef ref) => Supabase.instance.client;

@riverpod
TaskRepository taskRepo(TaskRepoRef ref) =>
    TaskRepository(client: ref.watch(supabaseProvider));

@riverpod
Future<List<Task>> tasks(TasksRef ref) =>
    ref.watch(taskRepoProvider).getAll();
Enter fullscreen mode Exit fullscreen mode
// BLoC: explicit manual wiring
MultiBlocProvider(
  providers: [
    BlocProvider(create: (_) => AuthBloc(repo: AuthRepository())),
    BlocProvider(create: (ctx) => TaskBloc(
      repo: TaskRepository(client: ctx.read<SupabaseClient>()),
    )),
  ],
  child: const App(),
)
Enter fullscreen mode Exit fullscreen mode

Testing

// Riverpod: ProviderContainer, easy overrides
test('increment counter', () {
  final container = ProviderContainer(
    overrides: [
      supabaseProvider.overrideWithValue(MockSupabaseClient()),
    ],
  );
  addTearDown(container.dispose);

  expect(container.read(counterProvider), 0);
  container.read(counterProvider.notifier).increment();
  expect(container.read(counterProvider), 1);
});

// BLoC: bloc_test package
blocTest<CounterBloc, int>(
  'emits [1] when Increment added',
  build: () => CounterBloc(),
  act: (bloc) => bloc.add(Increment()),
  expect: () => [1],
);
Enter fullscreen mode Exit fullscreen mode

Decision Matrix

Criteria Riverpod 2.0 BLoC Provider
Learning curve Medium High Low
Testability Excellent Excellent Good
Async handling Excellent Good Poor
Team scalability Excellent Excellent Fair
Boilerplate Low (generated) High Low
Best for Any size Large teams Prototypes

My take: Riverpod 2.0 + riverpod_annotation for most projects. Code generation handles boilerplate, AsyncValue makes async UI trivial, and ProviderContainer overrides make tests clean.

Since migrating to Riverpod, state-related bugs dropped by 70%.


What state management do you use in production? Share in the comments!

Top comments (0)