Lesson 18 / 25

Riverpod, Bloc and How to Choose

Two popular approaches and a decision guide.

Beyond Provider

Riverpod (by Provider's author) declares providers as global, compile-safe objects that do not depend on the widget tree; widgets read them through a WidgetRef (ref.watch, ref.read), and a ProviderScope at the root holds the state. It handles async data (FutureProvider, AsyncNotifier and the AsyncValue type), caching and disposal, and is easy to override in tests. Bloc (package flutter_bloc) separates UI from logic with Cubits (methods that emit new states) or Blocs (incoming events mapped to states), giving a strict, traceable event flow favoured by large teams. Choose by team and app size: setState for local state, Provider for small to medium apps, Riverpod for testable async-heavy apps, Bloc where explicit events and auditability matter. Both libraries have changed APIs across major versions, so check the docs for your version.

The same counter in Riverpod and Bloc

Notifier with Riverpod 2.x/3.x style; Cubit with flutter_bloc.

// ---- Riverpod ----
final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);

class CounterNotifier extends Notifier<int> {
  @override
  int build() => 0;
  void increment() => state++;
}

class RiverpodCounter extends ConsumerWidget {
  const RiverpodCounter({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return TextButton(
      onPressed: () => ref.read(counterProvider.notifier).increment(),
      child: Text('$count'),
    );
  }
}
// root: runApp(const ProviderScope(child: MyApp()));

// ---- Bloc (Cubit) ----
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);
  void increment() => emit(state + 1);
}

class BlocCounter extends StatelessWidget {
  const BlocCounter({super.key});
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<CounterCubit, int>(
      builder: (context, count) => TextButton(
        onPressed: () => context.read<CounterCubit>().increment(),
        child: Text('$count'),
      ),
    );
  }
}
// above it: BlocProvider(create: (_) => CounterCubit(), child: ...)

Pick one and be consistent

Every mainstream option can build a large app well. Mixing three state libraries in one codebase is a bigger problem than choosing the "wrong" one.

Quick check: In Bloc, how does a Cubit publish a new state?

  • By calling emit(newState)
  • By calling notifyListeners()
  • By calling setState()
  • By returning a widget
Answer

By calling emit(newState) — emit pushes the new state to BlocBuilder and other listeners.