← Blog / Flutter

Flutter State Management in 2026 — Provider vs Riverpod vs BLoC

Coding Liquids blog cover featuring Sagnik Bhattacharya for Flutter State Management in 2026 — Provider vs Riverpod vs BLoC, with comparison cards, a dark grid background, and choice-focused messaging.
Coding Liquids blog cover featuring Sagnik Bhattacharya for Flutter State Management in 2026 — Provider vs Riverpod vs BLoC, with comparison cards, a dark grid background, and choice-focused messaging.

If there's one topic that causes more confusion in the Flutter world than any other, it's state management. Every week I get asked: "Sagnik, should I use Provider, Riverpod, or BLoC?" I've trained hundreds of developers on Flutter, and I've seen teams succeed and struggle with all three. The answer isn't about which one is "best" — it's about which one fits your project, your team, and your architecture goals.

This guide breaks down all three approaches with real code, honest trade-offs, and clear recommendations based on years of production experience.

Why State Management Matters

Every Flutter app has state — the data that determines what your UI displays at any given moment. A counter value, a list of products fetched from an API, whether a user is logged in, the current theme. When your app is small, managing state with setState() inside a StatefulWidget works fine. But the moment your app grows — multiple screens sharing data, API calls, authentication flows, caching — things get messy fast.

State management solutions solve three core problems:

  • Sharing state across widgets without passing data through every constructor (prop drilling)
  • Separating business logic from UI so your code is testable and maintainable
  • Rebuilding only the widgets that need to change when state updates, keeping your app performant

Let's look at how Provider, Riverpod, and BLoC each tackle these problems differently.

Provider — The Established Default

Provider was the first state management solution officially recommended by the Flutter team. It's built on top of InheritedWidget — Flutter's native mechanism for passing data down the widget tree — and wraps it in a much friendlier API.

How Provider Works

You create a class that holds your state (typically extending ChangeNotifier), provide it at the top of the widget tree using ChangeNotifierProvider, and consume it anywhere below using context.watch() or Consumer.

class CartNotifier extends ChangeNotifier {
  final List<Product> _items = [];

  List<Product> get items => List.unmodifiable(_items);
  double get total => _items.fold(0, (sum, p) => sum + p.price);

  void add(Product product) {
    _items.add(product);
    notifyListeners();
  }

  void remove(Product product) {
    _items.remove(product);
    notifyListeners();
  }
}
// Providing it
ChangeNotifierProvider(
  create: (_) => CartNotifier(),
  child: MyApp(),
)

// Consuming it
final cart = context.watch<CartNotifier>();
Text('Total: \$${cart.total}');

Provider Strengths

  • Simple mental model — if you understand the widget tree, you understand Provider
  • Minimal boilerplate — a ChangeNotifier class and a Provider widget is all you need
  • Massive community — most tutorials, Stack Overflow answers, and courses teach Provider first
  • Flutter team endorsed — it's listed in the official Flutter documentation

Provider Limitations

  • Depends on BuildContext — you can only access providers through the widget tree, which makes it awkward to use in services, repositories, or pure Dart code
  • Runtime errors — if you try to read a provider that doesn't exist above you in the tree, you get a runtime exception, not a compile-time error
  • Combining providers is clunky — when one provider depends on another, you end up with ProxyProvider chains that are hard to follow
  • No auto-dispose — you have to manage provider lifecycles manually for anything beyond simple cases

Riverpod — Provider's Evolution

Riverpod was created by Remi Rousselet — the same developer who built Provider. Think of it as "Provider 2.0" — it fixes every major limitation of Provider while keeping the same philosophy of simplicity. The name itself is an anagram of "Provider."

How Riverpod Works

Instead of placing providers in the widget tree, Riverpod declares them as global variables. Don't panic — they're not mutable globals. They're declarative definitions that Riverpod's container manages. You consume them using ref.watch() inside a ConsumerWidget.

// Declare providers globally
final cartProvider = NotifierProvider<CartNotifier, List<Product>>(
  CartNotifier.new,
);

class CartNotifier extends Notifier<List<Product>> {
  @override
  List<Product> build() => [];

  void add(Product product) {
    state = [...state, product];
  }

  void remove(Product product) {
    state = state.where((p) => p != product).toList();
  }
}

final cartTotalProvider = Provider<double>((ref) {
  final items = ref.watch(cartProvider);
  return items.fold(0, (sum, p) => sum + p.price);
});
// Consuming it
class CartScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final total = ref.watch(cartTotalProvider);
    return Text('Total: \$${total}');
  }
}

Riverpod Strengths

  • Compile-time safety — no more runtime "ProviderNotFoundException" errors. If a provider doesn't exist, your code won't compile
  • No BuildContext dependency — providers can depend on each other, be used in tests, or accessed from anywhere without needing the widget tree
  • Auto-dispose — providers automatically clean up when no longer listened to. No memory leaks from forgotten subscriptions
  • Built-in async supportFutureProvider and StreamProvider handle loading, error, and data states out of the box with AsyncValue
  • Provider composition is natural — one provider watching another is just ref.watch(otherProvider). No ProxyProvider gymnastics
  • Code generation option — the @riverpod annotation can auto-generate provider declarations, reducing boilerplate even further

Riverpod Limitations

  • Learning curve — the global provider pattern and ref system feel unfamiliar to developers coming from Provider or React
  • Two APIs to learn — Riverpod offers both a manual API and a code-generation API. Beginners often aren't sure which to start with
  • Fewer tutorials — while growing fast, the Riverpod ecosystem is still smaller than Provider's. Finding answers to niche questions can take longer
  • Migration effort — moving an existing Provider codebase to Riverpod is a significant refactor, not a drop-in replacement

BLoC — The Enterprise Choice

BLoC (Business Logic Component) takes a fundamentally different approach. Instead of notifiers and watchers, it uses events and states flowing through streams. You send an event into a Bloc, the Bloc processes it, and emits a new state. The UI listens to state changes and rebuilds accordingly.

How BLoC Works

You define events (what happened), states (what the UI should show), and a Bloc class that maps events to states.

// Events
sealed class CartEvent {}
class AddToCart extends CartEvent {
  final Product product;
  AddToCart(this.product);
}
class RemoveFromCart extends CartEvent {
  final Product product;
  RemoveFromCart(this.product);
}

// State
class CartState {
  final List<Product> items;
  double get total => items.fold(0, (sum, p) => sum + p.price);
  const CartState({this.items = const []});
}

// Bloc
class CartBloc extends Bloc<CartEvent, CartState> {
  CartBloc() : super(const CartState()) {
    on<AddToCart>((event, emit) {
      emit(CartState(items: [...state.items, event.product]));
    });
    on<RemoveFromCart>((event, emit) {
      emit(CartState(
        items: state.items.where((p) => p != event.product).toList(),
      ));
    });
  }
}
// Consuming it
BlocBuilder<CartBloc, CartState>(
  builder: (context, state) {
    return Text('Total: \$${state.total}');
  },
)

// Sending events
context.read<CartBloc>().add(AddToCart(product));

BLoC Strengths

  • Extreme testability — every interaction is an event, every output is a state. Testing a Bloc means: send event, assert state. No mocking widgets or build contexts
  • Predictable data flow — events go in, states come out. There's no ambiguity about how state changes happen or what triggered them
  • Great for large teams — the enforced structure (events, states, blocs) means every developer follows the same pattern. Code reviews are faster because the architecture is consistent
  • Excellent debuggingBlocObserver lets you log every event and state transition globally. When something breaks, you can trace exactly what happened
  • Concurrency handling — Bloc has built-in event transformers for debouncing, throttling, and sequential processing of events

BLoC Limitations

  • Verbose — for a simple feature, you need an events file, a states file, and a bloc file. A counter app in BLoC is significantly more code than in Provider or Riverpod
  • Overkill for small apps — if your app has 5 screens and simple CRUD, BLoC's ceremony adds complexity without proportional benefit
  • Steeper learning curve — understanding streams, events, states, and the reactive paradigm takes time, especially for developers new to Flutter
  • Event-to-state mapping can get complex — when a single event triggers multiple async operations or depends on other Blocs, the handler logic can become hard to follow

Head-to-Head Comparison

FactorProviderRiverpodBLoC
Learning curveLowMediumHigh
BoilerplateMinimalLowHigh
Type safetyRuntime checksCompile-timeCompile-time
TestabilityGoodExcellentExcellent
Async supportManualBuilt-inBuilt-in
ScalabilitySmall-medium appsAll sizesMedium-large apps
Debugging toolsDevToolsDevTools + Riverpod inspectorBlocObserver + DevTools
Community sizeLargestGrowing fastLarge (enterprise)
Requires BuildContextYesNoYes (for UI layer)
Auto-disposeNoYesVia BlocProvider

When to Use What — My Recommendations

After years of building and teaching Flutter, here's how I recommend choosing:

Choose Provider When

  • You're building a small to medium app (under 15-20 screens)
  • Your team is new to Flutter and needs the gentlest learning curve
  • The app has simple state needs — a few shared objects, basic CRUD, no complex async chains
  • You're following a tutorial or course that teaches Provider (don't fight the teaching material)

Choose Riverpod When

  • You're starting a new project and want the best long-term architecture foundation
  • Your app has complex async requirements — API calls, caching, real-time data, pagination
  • You want compile-time safety and don't want to debug runtime provider errors
  • You need to test business logic independently from the widget tree
  • You're a solo developer or small team that values flexibility over enforced structure

Choose BLoC When

  • You're working on a large-scale app with a team of 5+ developers
  • Your organisation values consistent architecture and enforced patterns across the codebase
  • The app has complex business logic — multi-step workflows, concurrent operations, event-driven features
  • You need enterprise-grade traceability — logging every event and state change for debugging or auditing
  • Your team has experience with reactive programming (RxDart, streams, or similar patterns from other platforms)

Can You Mix Them?

Yes — and many production apps do. A common pattern I see in successful projects:

  • Riverpod for dependency injection and simple state (theme, auth status, feature flags)
  • BLoC for complex feature modules that benefit from the event-state pattern (checkout flows, real-time chat, form wizards)

The key rule: be consistent within a feature module. Don't use Provider for the cart, Riverpod for the profile, and BLoC for the feed within the same app. Pick a primary approach and use the secondary one only where it genuinely adds value.

What About setState, GetX, and MobX?

A few quick takes on other options you'll encounter:

  • setState — perfectly fine for local widget state (form fields, animations, toggles). Don't let anyone tell you it's "wrong." It's the wrong choice for shared state, but it's the right choice for widget-local state.
  • GetX — popular for its simplicity, but I don't recommend it for production apps. It encourages patterns that break as apps scale, the documentation is inconsistent, and it creates tight coupling between your code and the GetX library. I've helped multiple teams migrate away from it.
  • MobX — excellent if you're coming from the JavaScript/React world. Code generation makes it low-boilerplate. But the Flutter community has largely moved toward Riverpod and BLoC, so you'll find fewer Flutter-specific resources.

A Real-World Architecture Example

Here's the folder structure I use for Riverpod-based projects in my training sessions. It scales cleanly from 5 screens to 50:

lib/
├── core/
│   ├── constants/
│   ├── theme/
│   └── utils/
├── features/
│   ├── auth/
│   │   ├── data/          # repositories, API clients
│   │   ├── domain/        # models, entities
│   │   └── presentation/  # screens, widgets, providers
│   ├── cart/
│   │   ├── data/
│   │   ├── domain/
│   │   └── presentation/
│   └── products/
│       ├── data/
│       ├── domain/
│       └── presentation/
├── shared/
│   ├── providers/         # app-wide providers (theme, auth state)
│   └── widgets/           # reusable UI components
└── main.dart

Each feature is self-contained. The presentation folder holds the providers (or blocs) for that feature. Shared state lives in shared/providers. This structure works equally well with Riverpod or BLoC — the difference is whether the presentation folder contains *_provider.dart files or *_bloc.dart + *_event.dart + *_state.dart files.

My Personal Take

If you're asking me what I'd choose for a new project today — Riverpod. It hits the sweet spot between simplicity and power. It's flexible enough for a solo developer building a side project and robust enough for a team shipping a production cross-platform app. The compile-time safety alone saves hours of debugging.

But here's the thing I tell every batch of students: the state management solution is not the architecture. Provider, Riverpod, and BLoC are tools — they help you manage state, but they don't automatically make your code clean. Clean separation of concerns, proper dependency injection, testable business logic, and a consistent folder structure matter more than which package you import.

Master the principles. The tools will follow.

Related Posts

Want to learn Flutter from scratch?

My Complete Flutter Guide takes you from zero to building production-ready Android, iOS, and web apps — including state management with Riverpod and BLoC.

Explore Courses