Flutter State Management in 2026: Provider vs Riverpod vs Bloc vs Signals

Flutter ships one built-in reactivity primitive, setState(), and four community answers to its limits. Provider is the minimal option. Riverpod is its compile-safe successor. Bloc enforces event-driven architecture. Signals brings fine-grained reactivity from the JavaScript world. This guide shows the same feature in each, then tells you which to choose.

Quick Takeaways

  • Provider is the lowest-ceremony choice. It fits small apps and teams already invested in ChangeNotifier.
  • Riverpod 3 is the strongest default for new apps. It has compile-time safety, built-in async handling, and automatic retry.
  • Bloc/Cubit gives the strictest structure and the best traceability. It costs the most boilerplate.
  • Signals gives the finest-grained rebuilds and the smallest API. Its conventions for structure and testing are the least settled.
Criteria Provider Riverpod 3 Bloc / Cubit Signals
Learning curve Low Medium Medium-High Low
Boilerplate Low Low-Medium High Very low
Async handling Manual Built-in (AsyncValue) Manual (states) FutureSignal / manual
Rebuild granularity Widget (via select) Widget (via select) Widget (buildWhen) Individual reactive expression
Compile-time safety Runtime lookup Strong Medium Strong (global) / Medium
Testability Good Excellent Excellent Good
Best for Small apps Most new apps Large, regulated teams Performance-sensitive UIs

Where Each Library Stands in 2026

Versions matter because the APIs changed substantially in the last year.

Riverpod’s stable 3.0 release landed on September 10, 2025. The package has kept shipping since, and 3.4.1 was published on July 26, 2026. One migration guide notes that Riverpod 3 includes built-in automatic retry with exponential backoff.

Signals for Dart has also matured. signals_flutter is at version 6.3.0, with a stable API that has changed little since the 6.x line began.

flutter_bloc stays on its 9.x line. Bloc’s API is deliberately stable, so a slow release cadence signals maturity, not abandonment.

Provider remains available and works. It is the same author’s earlier package, and Riverpod’s changelog now includes tooling that eases migration from Provider to Riverpod.

The Shared Problem: What State Management Solves

Every library here answers three questions:

  1. Where does state live? Inside a widget, above a subtree, or globally.
  2. How does the UI learn it changed? Listeners, streams, or dependency tracking.
  3. How do you test it without a widget tree? Plain Dart objects versus framework-bound objects.

Use setState() for ephemeral state that one widget owns, such as a toggle or a text field’s focus. Reach for a library when app state crosses widget boundaries: auth sessions, carts, remote data, and settings.

Provider: Minimal Scoped State

Provider wraps InheritedWidget with a friendlier API. You expose a ChangeNotifier and listen to it below.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CounterModel extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners(); // Triggers a rebuild in every dependent widget.
  }
}

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CounterModel(),
      child: const MaterialApp(home: CounterPage()),
    ),
  );
}

The page reads state with select, so it rebuilds only when the selected value changes.

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    // Rebuilds only when `count` changes, not on every notify.
    final count = context.select<CounterModel, int>((m) => m.count);

    return Scaffold(
      body: Center(child: Text('$count')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read<CounterModel>().increment(),
        child: const Icon(Icons.add),
      ),
    );
  }
}

The output is a counter that increments on tap. Only the Text subtree depends on count.

Where Provider strains:

  • Lookups happen at runtime. Requesting a provider that is not in the tree throws ProviderNotFoundException when the widget builds.
  • Async state is manual. You hand-write isLoading, error, and data fields.
  • Providers that depend on other providers need ProxyProvider, which gets verbose fast.

Riverpod 3: Compile-Safe, Async-First

Riverpod removes the widget-tree dependency. Providers are globally declared and resolved through a ProviderContainer, so a missing provider is a compile error, not a runtime crash.

Synchronous State with a Notifier

import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'counter.g.dart';

@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0; // Initial state.

  void increment() => state++; // Assigning state notifies listeners.
}

Run dart run build_runner build to generate counter.g.dart. If you skip code generation, the manual equivalent is NotifierProvider<Counter, int>(Counter.new).

Async State Without Boilerplate

@riverpod
Future<List<Todo>> todos(Ref ref) async {
  final repo = ref.watch(todoRepositoryProvider);
  return repo.fetchAll(); // Loading and error states come for free.
}
class TodoList extends ConsumerWidget {
  const TodoList({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todos = ref.watch(todosProvider);

    return todos.when(
      data: (items) => ListView(
        children: [for (final t in items) ListTile(title: Text(t.title))],
      ),
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (e, _) => Text('Failed: $e'),
    );
  }
}

The provider returns an AsyncValue with three states. Failed fetches retry automatically, so you no longer write a retry loop in every repository call.

What Changed in Riverpod 3

Change Impact
Unified Ref type No more generated TodosRef types. One Ref everywhere.
StateProvider and StateNotifierProvider moved to legacy.dart Old code needs an import change. New code uses Notifier.
Automatic retry Failed providers retry with exponential backoff.
autoDispose as a modifier No separate base class.
Exceptions wrapped in ProviderException Catch the wrapper, then inspect .exception.

The reported migration cost for a real app moving from 2.x is small. One guide estimates about half a day, mostly mechanical search-and-replace. Projects already using @riverpod code generation have even less to change.

Where Riverpod strains: The concept count is higher than Provider’s: ref.watch versus ref.read versus ref.listen, plus provider families and scoping. Code generation adds a build step.

Bloc and Cubit: Explicit Events, Predictable Transitions

Bloc separates events (input) from states (output). Every state change is traceable to an event, which makes logs, replay, and audits straightforward.

Cubit: The Lightweight Form

import 'package:flutter_bloc/flutter_bloc.dart';

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
}

Bloc: Event-Driven with Concurrency Control

A search box is where Bloc earns its boilerplate. The restartable() transformer cancels stale requests.

import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

sealed class SearchEvent {}

final class QueryChanged extends SearchEvent {
  QueryChanged(this.query);
  final String query;
}

sealed class SearchState {}

final class SearchIdle extends SearchState {}

final class SearchLoading extends SearchState {}

final class SearchSuccess extends SearchState {
  SearchSuccess(this.results);
  final List<String> results;
}

final class SearchFailure extends SearchState {}

class SearchBloc extends Bloc<SearchEvent, SearchState> {
  SearchBloc(this._repo) : super(SearchIdle()) {
    on<QueryChanged>(_onQueryChanged, transformer: restartable());
  }

  final SearchRepository _repo;

  Future<void> _onQueryChanged(
    QueryChanged event,
    Emitter<SearchState> emit,
  ) async {
    emit(SearchLoading());
    try {
      final results = await _repo.search(event.query);
      emit(SearchSuccess(results));
    } catch (_) {
      emit(SearchFailure());
    }
  }
}

When a new QueryChanged arrives mid-request, restartable() cancels the previous handler. The UI never shows results for an outdated query. Sealed classes also make the switch over states exhaustive, so the compiler flags any missing case.

Where Bloc strains: Every feature needs an event class, a state class, and a handler. For a settings toggle, that is too much ceremony. Use Cubit for simple cases and reserve full Bloc for flows with real concurrency or audit needs.

Signals: Fine-Grained Reactivity

Signals track dependencies automatically. When a signal changes, only the expressions that read it re-run. There is no listener registration and no provider tree.

import 'package:flutter/material.dart';
import 'package:signals_flutter/signals_flutter.dart';

final counter = signal(0);
final doubled = computed(() => counter.value * 2); // Memoized derived value.

class CounterView extends StatelessWidget {
  const CounterView({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Only this builder re-runs when `counter` changes.
        Watch((context) => Text('Count: ${counter.value}')),
        Watch((context) => Text('Doubled: ${doubled.value}')),
        ElevatedButton(
          onPressed: () => counter.value++,
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

The two Text widgets rebuild independently. The surrounding Column and the button never rebuild. A computed signal recalculates only when its dependencies change, and caches the result otherwise.

Side Effects

// Runs once immediately, then again whenever `counter` changes.
final dispose = effect(() {
  debugPrint('Counter is now ${counter.value}');
});

// Call dispose() when the effect's owner is torn down.

Where Signals strains:

  • Global signals are easy to write and easy to couple tightly. Without discipline, your app becomes a web of module-level variables.
  • Dependency injection is not built in. You pair signals with get_it or an InheritedWidget to scope and swap them in tests.
  • Community conventions for large codebases are thinner than for Bloc or Riverpod.

Performance and Memory Trade-Offs

Rebuild cost scales with how many widgets a change touches. In big-O terms, a naive notifyListeners() on a model costs O(n) in the number of dependent widgets. Selectors and signals shrink that to the widgets that read the changed field.

Aspect Provider Riverpod 3 Bloc Signals
Update propagation Notifies all listeners, filtered by select Notifies watchers of changed provider Emits state, filtered by buildWhen Notifies only dependent expressions
Derived state Manual or ProxyProvider Providers watching providers Manual in the bloc computed (memoized)
Disposal Tied to widget tree autoDispose modifier Closed by BlocProvider Manual or via widget mixin
Memory overhead Low Low-Medium (provider container) Medium (streams, event queue) Low
DevTools Basic Dedicated devtool extension Bloc Observer, logging Signals devtool

None of these libraries is a bottleneck in typical apps. Real slowness comes from rebuilding large subtrees. Selectors, buildWhen, and Watch all fix that, so pick based on architecture fit, not micro-benchmarks.

Bad Code vs. Good Code: Refactoring Scenarios

Anti-Pattern 1: Watching the Whole Model (Provider)

// BAD: rebuilds the entire page on ANY change to CartModel.
class CartBadge extends StatelessWidget {
  const CartBadge({super.key});

  @override
  Widget build(BuildContext context) {
    final cart = context.watch<CartModel>();
    return Text('${cart.items.length}');
  }
}
// GOOD: rebuilds only when the item count changes.
class CartBadge extends StatelessWidget {
  const CartBadge({super.key});

  @override
  Widget build(BuildContext context) {
    final count = context.select<CartModel, int>((c) => c.items.length);
    return Text('$count');
  }
}

Anti-Pattern 2: Reading Inside build (Riverpod)

// BAD: ref.read inside build never subscribes. The UI goes stale.
Widget build(BuildContext context, WidgetRef ref) {
  final user = ref.read(userProvider);
  return Text(user.name);
}
// GOOD: watch in build, read in callbacks.
Widget build(BuildContext context, WidgetRef ref) {
  final user = ref.watch(userProvider);
  return ElevatedButton(
    onPressed: () => ref.read(userProvider.notifier).refresh(),
    child: Text(user.name),
  );
}

Anti-Pattern 3: Mutating State In Place (Bloc)

// BAD: same list instance, so Bloc sees equal state and skips the emit.
void addItem(Item item) {
  state.items.add(item);
  emit(state);
}
// GOOD: create a new state object. Use Equatable or freezed for equality.
void addItem(Item item) {
  emit(state.copyWith(items: [...state.items, item]));
}

Anti-Pattern 4: Unscoped Global Signals

// BAD: module-level mutable state that every test shares.
final cartItems = signal<List<Item>>([]);
// GOOD: wrap signals in a class so you can create, inject, and dispose them.
class CartController {
  final items = signal<List<Item>>([]);
  late final count = computed(() => items.value.length);

  void add(Item item) => items.value = [...items.value, item];
  void dispose() {
    count.dispose();
    items.dispose();
  }
}

Choosing the Right Tool

Use this table as a starting point, then adjust for your team.

Your situation Pick Why
Prototype or small app, minimal dependencies Provider Least code, shallow learning curve
New production app, mixed sync and async data Riverpod 3 Safe, testable, async built in
Large team, strict architecture, audit trails Bloc Events and states are explicit and loggable
Animation-heavy or dashboard UI with many small updates Signals Per-expression rebuilds
Existing Provider app that is growing painful Riverpod Migration tooling exists, concepts carry over
Team with web background (Preact, Solid, Angular) Signals Same mental model

Mixing is legitimate. A common setup is Riverpod for app-level data and plain setState() or ValueNotifier for local widget state. Avoid running two global state libraries in the same feature.

Testing Each Approach

Your state logic should run without a widget tree. All four support that, with different tools.

// Riverpod: override dependencies in a ProviderContainer.
test('increments counter', () {
  final container = ProviderContainer();
  addTearDown(container.dispose);

  container.read(counterProvider.notifier).increment();

  expect(container.read(counterProvider), 1);
});
// Bloc: bloc_test gives a declarative API.
blocTest<CounterCubit, int>(
  'emits [1] on increment',
  build: CounterCubit.new,
  act: (cubit) => cubit.increment(),
  expect: () => [1],
);

Both tests run as plain Dart in milliseconds. For Provider, test the ChangeNotifier directly. For signals, test the controller class and read .value.

FAQ

Is Provider deprecated in 2026?

No. Provider is still published and works on current Flutter. It is the same author’s earlier package, and Riverpod is positioned as the successor. For new projects, most developers start with Riverpod. Existing Provider apps need no urgent rewrite.

Is Riverpod better than Bloc?

Neither is strictly better. Riverpod needs less boilerplate and handles async data with AsyncValue. Bloc enforces an event-to-state structure that suits large teams and apps needing detailed logs. Choose Riverpod for speed and flexibility, and Bloc for strictness.

Are Signals production-ready in Flutter?

Yes, for the reactive core. signals_flutter is on a stable 6.x line, and the API for signal, computed, effect, and Watch is small. The gap is architectural convention: you must decide how to scope, inject, and dispose signals yourself.

Can I use more than one state management library in a Flutter app?

Yes, but keep their roles distinct. Use one library for shared app state and setState() or ValueNotifier for local state. Running two global solutions in one feature adds confusion and duplicates logic.

Hot this week

Robotics Predictions for 2027

Seven data-driven robotics predictions for 2027: VLA policies, humanoid actuators, ROS 2 real-time, safety filters, and code. Read the engineering roadmap.

Robots and Jobs: What the Evidence Says in 2026

Robots and jobs in 2026: IFR installation data, wage and employment estimates, and a payback model engineers can run. Read the evidence.

Robotics Careers in 2026: Roles, Skills, and Salaries

Explore robotics careers in 2026: top roles, must-have ROS 2 and controls skills, and U.S. salary ranges. Build your robotics career roadmap today.

Robot Sensors Explained: How Robots Perceive the World

Learn how LiDAR, IMU, encoders, and depth cameras work, with fusion math and ROS2 code. Build a reliable perception stack today.

20 Beginner Robot Projects Ranked by Difficulty

Build 20 beginner robot projects ranked from easiest to hardest, with wiring, code, and ROS2 steps. Pick your first build and start today.

Topics

Robotics Predictions for 2027

Seven data-driven robotics predictions for 2027: VLA policies, humanoid actuators, ROS 2 real-time, safety filters, and code. Read the engineering roadmap.

Robots and Jobs: What the Evidence Says in 2026

Robots and jobs in 2026: IFR installation data, wage and employment estimates, and a payback model engineers can run. Read the evidence.

Robotics Careers in 2026: Roles, Skills, and Salaries

Explore robotics careers in 2026: top roles, must-have ROS 2 and controls skills, and U.S. salary ranges. Build your robotics career roadmap today.

Robot Sensors Explained: How Robots Perceive the World

Learn how LiDAR, IMU, encoders, and depth cameras work, with fusion math and ROS2 code. Build a reliable perception stack today.

20 Beginner Robot Projects Ranked by Difficulty

Build 20 beginner robot projects ranked from easiest to hardest, with wiring, code, and ROS2 steps. Pick your first build and start today.

Robot Computers in 2026: Raspberry Pi 5, Jetson, and Industrial PCs Compared

Raspberry Pi 5 vs NVIDIA Jetson vs industrial PCs for ROS 2 robots. See specs, latency math and a decision matrix, then pick your compute.

AI in Robotics: Programmed Behavior vs Machine Learning (and When You Need Each)

Programmed control or machine learning? Compare PID, FSMs, and RL with ROS2 code, benchmarks, and a decision framework. Pick the right one.

What Is ROS 2? A Beginner’s Guide to the Robot Operating System in 2026

Learn what ROS 2 is, how nodes, topics, and DDS work, and build your first publisher and subscriber. Start your ROS 2 project today.

Related Articles

Popular Categories