Muhammed Shibli

Flutter

State Management in Flutter: An Honest Comparison After 3 Years

· by Muhammed Shibli

I don't have a religion about Flutter state management anymore, and that took actually shipping production apps with three different approaches to figure out. This isn't a feature-by-feature comparison table — plenty of those exist. It's what actually happened when each one met real client requirements, real deadlines, and real bugs.

Why I don't have a religion about this anymore

Early on I picked Provider because it was what every tutorial used, and for a while that was fine. Then I built a live-streaming app — Kalise — where state needed to sync across gifting, live viewer counts, and call state simultaneously, and Provider's dependency on BuildContext started causing real friction. A widget deep in the tree needing to read state without a context readily available meant either restructuring the widget tree or reaching for workarounds that felt like fighting the framework instead of using it.

That project is where I actually moved to Riverpod, not because a blog post told me to, but because I hit a specific wall Provider didn't handle cleanly.

Provider: where I started, and where it starts hurting

Provider is genuinely good for learning the concept — a widget listens to a ChangeNotifier, the notifier calls notifyListeners(), dependent widgets rebuild. Simple, and for a small app, it's still a perfectly reasonable choice:

class CartNotifier extends ChangeNotifier {
  final List<Item> _items = [];
  List<Item> get items => _items;

  void add(Item item) {
    _items.add(item);
    notifyListeners();
  }
}

Where it starts hurting is scale and testability. Reading state requires a BuildContext, which means anything not directly in the widget tree — a background service, a callback fired from a plugin — needs awkward workarounds to access state cleanly. Testing a notifier in isolation is straightforward, but testing how widgets consume that state usually means standing up a widget tree in your tests, which is heavier than it should be for what you're actually verifying.

Riverpod: what actually changed for me

The specific problem Riverpod solved for me on Kalise: providers aren't tied to BuildContext, so a service class, a background handler, or any part of the app can read state the same way a widget does. Compile-time errors instead of runtime "no provider found" crashes caught real mistakes before they shipped, not after a client found them.

final cartProvider = NotifierProvider<CartNotifier, List<Item>>(CartNotifier.new);

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

  void add(Item item) {
    state = [...state, item];
  }
}

The tradeoff is real, and I won't pretend it isn't: more setup, more concepts (providers, notifiers, the ref object), and for a genuinely small app — a five-screen tool with no complex shared state — that overhead isn't always worth paying. I still reach for plain setState or a lightweight Provider setup on small, contained projects where Riverpod's structure would be more scaffolding than the app needs.

Bloc: powerful, and I still don't reach for it by default

I've used Bloc on client work where the team already had Bloc conventions in place, and it's genuinely strong at what it's built for: a strict, explicit separation between events (what happened) and states (what the UI should show), which pays off enormously in larger teams where multiple developers need to reason about the same state logic consistently.

For the kind of projects I mostly build — solo or small-team client work with a defined scope and a real deadline — Bloc's ceremony (defining events, states, and the bloc mapping between them for even simple interactions) is often more structure than the problem needs. I don't avoid it out of dislike; I avoid it as a default because most of my clients aren't running a ten-person engineering team where that rigidity is the point.

A mistake I made switching mid-project

I want to be honest about one thing that didn't go smoothly: partway through the Kalise build, I moved the gifting and live-viewer-count logic to Riverpod while leaving the profile and settings screens on Provider, planning to migrate the rest "later." Later took longer than planned, and for a few weeks the codebase had two competing patterns for reading state depending on which screen you were in — a genuinely confusing state for anyone (including me, coming back to a file after a few days) to reason about. The lesson wasn't "don't migrate," it was "don't migrate halfway and leave it there." If a state management change is worth making, it's worth finishing in one deliberate pass, or not starting until you can commit the time to finish it, rather than letting two patterns coexist indefinitely because finishing felt less urgent than the next client deadline.

What I actually use today, honestly

Riverpod, by default, on anything I expect to have real complexity or grow past its first version. Plain setState or a small Provider setup for genuinely simple, contained screens where adding Riverpod's structure would just be overhead. Bloc when a client's existing codebase or team already uses it — matching what's there beats introducing a fourth pattern into a project that already has three.

None of this is a universal ranking. It's what happened when Kalise's real-time complexity outgrew Provider, what I learned building things fast enough that Riverpod's upfront cost stopped feeling like a tax, and what convinced me Bloc is a tool for a specific organizational shape, not a universally "more correct" choice. If you're deciding for your own project, the honest question isn't "which is best" — it's "how much shared, cross-cutting state does this app actually have," and the answer to that should pick the tool, not a blog post's opinion.

For how this fits into the bigger picture of how I build Flutter apps, see Flutter and mobile app development, or Flutter vs native in 2026 for the layer above this decision — whether Flutter is the right framework at all.

Frequently asked questions

Which Flutter state management solution should I learn first?

Provider, honestly, even though I've mostly moved past it myself. It teaches the underlying concept — widgets rebuilding in response to state changes — without the extra abstraction Riverpod and Bloc add on top. Once that clicks, moving to Riverpod is a small step, not a relearn.

Is Riverpod just Provider with extra steps?

It's Provider's core idea with the actual problems fixed — compile-time safety instead of runtime context errors, no BuildContext dependency for reading state, and testability that doesn't require mocking a widget tree. It's more setup upfront, and for a genuinely small app that's not always worth it. For anything I expect to grow, it is.

Is Bloc overkill for most apps?

For most of the client work I do, yes. Bloc's strict separation of events and states is genuinely valuable in large teams where consistency across many developers matters more than individual velocity. For a solo developer or small team shipping a focused product, it's often more ceremony than the app needs — though I'll reach for it specifically when a client's team already knows it and expects it.

Do I need to pick one state management approach for the whole app?

No, and pretending otherwise causes more harm than mixing approaches thoughtfully. I've shipped apps using Riverpod for most screens and a simpler local setState for an isolated widget that genuinely doesn't need global state. The goal is matching the tool to the actual problem, not architectural purity for its own sake.

Related reading

Keep going