The Flutter state management debate has been a two-horse race for years. BLoC or Riverpod. Streams or reactive graphs. Boilerplate or flexibility.
Meanwhile, the wider frontend world moved on. Angular adopted signals. Solid.js was built on them. Preact switched to them. Vue's reactivity system is essentially signals under a different name. The pattern isn't new — it dates back to the 1970s — but its resurgence in modern frameworks happened because it solves a specific class of problems better than either streams or provider-style reactivity.
Flutter now has signals too. And they deserve a serious look.
What Signals Actually Are
A signal is a reactive primitive that holds a value. When the value changes, anything that depends on it updates automatically. That's it. No events to dispatch, no providers to declare, no streams to subscribe to and unsubscribe from.
final count = signal(0);
// Reading
print(count.value); // 0
// Writing
count.value = 5;
// Derived (computed) signal
final doubled = computed(() => count.value * 2);
print(doubled.value); // 10
// Reactive side effects
effect(() {
print('Count is now: ${count.value}');
});
count.value = 3; // prints "Count is now: 3"If this looks simple, that's the point. The power isn't in the API — it's in what happens under the hood.
Fine-Grained Reactivity: The Key Difference
Here's where signals diverge from both BLoC and Riverpod in a way that actually matters.
BLoC rebuilds based on state emissions. When a BLoC emits a new state, every BlocBuilder listening to it evaluates whether to rebuild (via buildWhen). The granularity is at the BLoC level — you're subscribing to the entire state object.
Riverpod rebuilds based on provider changes. You can use select() to narrow it down, but the mechanism is still "provider changed → check if the selected value changed → rebuild if yes."
Signals rebuild at the individual value level. There's no container, no provider, no state object. Each signal is independently reactive. If a widget depends on count but not name, changing name doesn't trigger any evaluation for that widget. Not a "check and skip" — the dependency simply doesn't exist.
This is fine-grained reactivity. And for UIs with lots of independently changing values, it's meaningfully more efficient.
A Concrete Example
Imagine a trading dashboard. You have 50 stock tickers, each updating independently every second. Each ticker shows price, change percentage, and a color indicator.
With BLoC, you'd either have one BLoC with a list of stocks (every tick rebuilds the entire list) or 50 separate BLoCs (massive boilerplate). The buildWhen approach helps, but you're still evaluating 50 conditions on every emission.
With Riverpod, you'd have a family provider or 50 individual providers. Better than BLoC for this case, but each provider still goes through Riverpod's notification/rebuild pipeline.
With Signals, each ticker is a signal. Each widget reads exactly the signal it needs. When AAPL updates, only the AAPL widget reacts. The framework doesn't even check the other 49.
// Each ticker is its own signal
final tickerSignals = <String, Signal<StockTick>>{};
Signal<StockTick> getTickerSignal(String symbol) {
return tickerSignals.putIfAbsent(
symbol,
() => signal(StockTick.empty(symbol)),
);
}
// Widget — only rebuilds when THIS ticker changes
class TickerWidget extends StatelessWidget {
final String symbol;
const TickerWidget({required this.symbol});
@override
Widget build(BuildContext context) {
final tick = getTickerSignal(symbol).watch(context);
return Row(
children: [
Text(symbol),
Text('\$${tick.price.toStringAsFixed(2)}'),
Icon(
tick.change >= 0 ? Icons.arrow_upward : Icons.arrow_downward,
color: tick.change >= 0 ? Colors.green : Colors.red,
),
],
);
}
}No events. No providers. No buildWhen. No select(). Just a value and its dependents.
The `flutter_signals` Package
The primary signals implementation for Flutter is signals (previously flutter_signals) by Rody Davis. It integrates with Flutter's widget lifecycle and provides the core primitives:
Signal — Mutable Reactive Value
final name = signal('Alice');
// In a widget
@override
Widget build(BuildContext context) {
final currentName = name.watch(context);
return Text(currentName);
}
// Updating — widget rebuilds automatically
name.value = 'Bob';Computed — Derived Reactive Value
final firstName = signal('Alice');
final lastName = signal('Smith');
final fullName = computed(() => '${firstName.value} ${lastName.value}');
// fullName automatically updates when either input changes
// If only firstName changes, only widgets watching fullName rebuild
// lastName's dependents are untouchedComputed signals are lazy — they don't recompute until read. And they cache — reading the same computed signal twice doesn't run the computation twice.
Effect — Reactive Side Effect
final authState = signal<AuthStatus>(AuthStatus.unknown);
// Runs whenever authState changes
effect(() {
if (authState.value == AuthStatus.unauthenticated) {
navigator.pushReplacementNamed('/login');
}
});Effects are for side effects — navigation, analytics, logging, API calls. They run automatically when their dependencies change, but they don't produce a value.
Batch — Grouped Updates
// Without batch: two separate rebuilds
firstName.value = 'Bob';
lastName.value = 'Jones';
// With batch: one rebuild after both changes
batch(() {
firstName.value = 'Bob';
lastName.value = 'Jones';
});This prevents unnecessary intermediate rebuilds when updating multiple related signals.
When Signals Are the Right Choice
Heavy UI reactivity with many independent values
Dashboards, data tables, real-time feeds, configuration panels with dozens of toggles. Anywhere you have many pieces of state that change independently and each affect a small part of the UI. Signals' fine-grained reactivity means you're not paying for state changes that don't affect the current widget.
Lots of derived/computed state
If your UI shows values that are computed from other values — totals, filtered lists, formatted strings, conditional visibility — computed signals handle this elegantly. The dependency graph is implicit in the computation, and updates propagate automatically.
final items = signal<List<CartItem>>([]);
final taxRate = signal(0.08);
final subtotal = computed(() =>
items.value.fold(0.0, (sum, item) => sum + item.price * item.quantity)
);
final tax = computed(() => subtotal.value * taxRate.value);
final total = computed(() => subtotal.value + tax.value);
// Change any input → only affected computations re-run
// Change taxRate → tax and total recompute, subtotal doesn'tLocal component state that doesn't need global coordination
Not everything needs to be in a global state container. A form with validation, an expandable section, a search filter — these are local concerns. Signals handle them without the overhead of creating a provider or a BLoC.
Rapid prototyping
The minimal boilerplate makes signals fast to iterate with. You can sketch out reactive behavior in minutes, without event classes, without provider registration, without any ceremony.
When Signals Are NOT the Right Choice
Complex business flows with auditability requirements
If you need to trace why state changed — not just what it changed to — signals don't give you that out of the box. There's no event stream, no named transitions, no built-in logging of state changes. BLoC's event-driven architecture is purpose-built for this.
Large teams that need enforced patterns
Signals are flexible. Very flexible. A senior developer will use them cleanly. A team of ten developers with varying experience will create ten different patterns for the same problem. BLoC's opinionated structure prevents this. Riverpod's provider model constrains it. Signals don't constrain much of anything.
Deep integration with DDD patterns
If your architecture speaks in domain events, aggregates, and bounded contexts, BLoC's event model maps naturally to that vocabulary. Signals are a reactive primitive, not an architectural pattern. You'd need to build your own conventions on top.
Mature ecosystem and community support
BLoC has years of production battle-testing, extensive documentation, and a large community. Riverpod has Remi Rousselet's active development and a growing ecosystem. Signals in Flutter is newer, with a smaller community and fewer production case studies. This is changing, but it's a real consideration for production apps today.
Signals + BLoC: A Hybrid Approach
Here's something most articles won't tell you: signals and BLoC aren't mutually exclusive.
BLoC handles your complex business flows — authentication, checkout, multi-step processes. Signals handle your UI reactivity — form state, toggle visibility, computed display values, real-time data that doesn't need audit trails.
// BLoC for the business flow
class CheckoutBloc extends Bloc<CheckoutEvent, CheckoutState> {
// Complex state machine with named events and transitions
}
// Signals for the UI layer
final promoCodeInput = signal('');
final isPromoFieldVisible = signal(false);
final promoDiscount = computed(() {
// Derived from the input, updates the UI instantly
final code = promoCodeInput.value;
if (code.length < 4) return 0.0;
return lookupDiscount(code); // synchronous lookup from cached data
});The BLoC manages the checkout flow. The signals manage the UI details. Each tool does what it's good at.
The Comparison Table
| Dimension | BLoC | Riverpod | Signals |
|---|---|---|---|
| Reactive mechanism | Dart Streams | Custom dependency graph | Signal primitives |
| Rebuild granularity | Per-BLoC (filterable via buildWhen) | Per-provider (filterable via select) | Per-signal (automatic) |
| Boilerplate | High (events, states, BLoC class) | Low (provider + code gen) | Minimal (signal + computed) |
| Traceability | Excellent (event stream) | Moderate (provider graph) | Low (no built-in history) |
| Learning curve | Moderate-high | Moderate | Low |
| Team scaling | Excellent (enforced patterns) | Good (convention-dependent) | Risky (no constraints) |
| Testing | Excellent (pure Dart) | Good (ProviderContainer) | Good (direct value testing) |
| Ecosystem maturity | Very mature | Mature | Growing |
Getting Started
If you want to try signals, start small. Pick a screen with lots of local state — a settings page, a data table, a form with computed validation — and rewrite just that screen using signals. Compare the code. Compare the behavior. Then decide if the pattern earns a bigger role in your architecture.
dependencies:
signals: ^6.0.0 # Check for latest versionDon't rip out your existing state management. Signals coexist with BLoC, Riverpod, and Provider without conflict. The question isn't "should I switch?" — it's "is there a class of problems in my app that signals handle better?"
For most apps, the answer is yes. The question is how large that class is.
Next in this series: You Don't Need a Package: ValueNotifier + ListenableBuilder for Simple State — before reaching for any package, know what Flutter gives you for free.