Flutter State Management Without the Fluff

David Havlin
David Havlin
Our work
March 11, 2026
5 min read

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.

David Havlin

Read what's next

The Hidden 80% Tax on Every Company That Builds Native for iOS and Android

The real business case for Flutter, including when it is not the right call. Two native apps means double the cost, double the coordination, and double the bugs. Here is the math most companies skip.

Flutter State Management Without the Fluff

Signals, store classes, and why simpler reactivity might be all you need. A practical comparison of Signals vs Riverpod with real code from our production apps.

Ako sme zjednodušili cestovanie Bratislavským krajom III - Design

The third part of our IDS BK app redesign series focuses on the final visual design. We cover the creation of the moodboard, visual direction, illustrations, animations, and the design of both light and dark modes.