Flutter gives you too many options (and that is a problem)
State management is one of those Flutter topics where everyone has an opinion. And very quickly the conversation turns into "this is the right way" versus "that is outdated."
That is where it starts going wrong. There is no need for a million layers, extra complexity, and boilerplate just to make state reactive. It can be done simply, cleanly, and in a way that stays readable six months later.
This is a personal take based on working with signals in production and spending time trying to understand Riverpod better. Not a universal rule. Just where we currently stand.
setState. Provider. Bloc. Cubit. Riverpod. Signals. The list keeps growing.
Options are good. But the side effect is that developers, especially those newer to Flutter, start thinking the more complicated solution is automatically the better one. If you are not using the trendy tool, you must be doing it wrong.
That assumption costs teams real time and real money.
Riverpod is the modern standard. That does not mean it is always the right fit.
Riverpod is a good tool. It gives you clear rules, a structured way to connect state, reusable building blocks, caching, and devtools. A lot of Flutter developers reach for it by default, and that makes sense.
But complexity becomes a problem when simple things stop feeling simple.
Terms like Consumer, Notifier, and all the provider variations do not feel intuitive, especially if you are coming from the JavaScript or web world. To be direct: it feels confusing and boilerplate-heavy for what it gives you in small to medium apps. The feature itself can be simple, but the state setup already feels heavier than the problem it solves.
The documentation is thorough, but when you are still learning the basics, the sheer volume of it works against you rather than for you.
A simpler approach that works in production
In our projects, we use a combination that keeps the state layer thin and readable:
- signals for reactivity
- extended signals for persisted state
- getIt for dependency injection
- injectable for wiring DI cleanly
- store classes for structure
The basic model is very direct. signal(...) is state. computed(...) is derived state. Watch(...) is the reactive part of the UI. That is the entire mental model.
We use store classes on top of that. A store keeps state, computed values, and methods together. It is just a normal singleton class. That is the whole point.
Real examples from our codebase
1. A map store
Here is a store that handles map state. Mutable state, derived state, and methods, all in one place.
@singleton
class MapStore {
final searchQuery = signal('');
final mapViewType = signal(MapViewType.simple);
late final isSimpleMap =
computed(() => mapViewType.value == MapViewType.simple);
void updateSearch(String query) {
searchQuery.value = query;
}
}
No providers, no notifiers, no build methods to override. State is a signal. Derived state is computed. Methods mutate the signal directly.
2. Precise UI rebuilds
This is the part that makes the biggest practical difference. You can have a large build method, wrap only the small reactive part with Watch, and only that widget rebuilds when the signal changes.
Row(
children: [
const Text('Offline: '),
Watch((ctx) => Text(store.isOffline.value ? 'Yes' : 'No')),
],
)
No Consumer widget. No ref.watch with selectors. Just wrap the reactive part. Done.
Signals vs Riverpod: side by side
This is not about one being objectively better. It is about what is easier to read, teach, and maintain for most projects.
Basic state
Signals:
final count = signal(0);
count.value++;
Riverpod:
final counterProvider =
NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
class CounterNotifier extends Notifier<int> {
@override
int build() => 0;
void increment() => state++;
}
Both solve the same problem. The signals version is two lines. The Riverpod version requires a class, a provider declaration, a build method, and a mutation method.
Derived state
Signals:
final count = signal(0);
final doubled = computed(() => count.value * 2);
Riverpod:
final counterProvider =
NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
final doubledProvider = Provider((ref) {
final count = ref.watch(counterProvider);
return count * 2;
});
In Riverpod, there is already more to keep in your head. A separate provider for derived state, the ref.watch call, the connection between the two providers. With signals, the dependency is implicit in the computed callback.
UI reactivity
Signals:
Watch((context) => Text('${store.likes.value}'))
Riverpod:
Consumer(
builder: (context, ref, _) {
final likes = ref.watch(shopProvider.select((s) => s.likes));
return Text('$likes');
},
)
This is where the gap becomes most obvious. To get granular UI reactivity in Riverpod, you need Consumer, ref, watch, and select. In signals, you wrap the widget with Watch and access the value. One concept instead of four.
When to use what
The practical decision is not "which tool is the most advanced?" It is "which tool keeps this app understandable six months from now?"
If signals already cover the problem clearly, stay with signals. If you need persistence, DI, or extra structure, add those pieces directly instead of switching the whole state model. If a team already knows Riverpod well and benefits from its model, then Riverpod makes sense. Context matters.
What does not make sense is assuming that more abstraction automatically means better architecture.
Signals are not perfect either
Signals do not magically solve every architectural problem. You still need discipline around structure, naming, and boundaries. They do not give you built-in caching or devtools. They do not enforce a specific pattern across your team.
But their core mental model is small enough that a new developer on the team can understand it in an hour, not a week. And in most projects, that tradeoff is worth it.
If you come from Vue or Svelte, signals will feel familiar immediately. The reactive primitives are almost identical. If you come from React, the mental shift is smaller than learning Riverpod from scratch.
The bottom line
You do not always need more layers and more boilerplate to build something serious.
Signals are easier to learn, easier to read, and easier to use in real code. Riverpod is a very good tool that clearly solves a lot, and there is a reason so many Flutter developers like it. But for the way most teams actually work, especially on small to medium products, the simpler reactive model is often the better fit.
Pick the tool that makes your codebase easier to hand off, not the one that looks most impressive in a conference talk.



