State management: setState, Provider, Riverpod and BLoC

Decide what counts as state, lift it to the right place, and choose between the built-in tools and the three libraries most Flutter teams use.

Where state belongs

Kind of stateLifetimeTool
Ephemeral UI stateOne widgetsetState
Shared within a screenSubtreeInheritedWidget or Provider
App-wide and injectableWhole appRiverpod
Complex event-driven flowsFeatureBLoC
Server dataCache with TTLA query layer, not a store
class CounterPage extends StatefulWidget {
  const CounterPage({super.key});

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _count = 0;

  void _increment() => setState(() => _count++);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $_count'),
        ElevatedButton(onPressed: _increment, child: const Text('Add')),
      ],
    );
  }
}

The first question is not which library to install, it is whether the state needs to outlive one widget. If it does not, setState is the correct answer and adding a package is overhead.

Sharing with Provider

class CartModel extends ChangeNotifier {
  final _items = <String, int>{};

  Map<String, int> get items => Map.unmodifiable(_items);
  int get totalCount => _items.values.fold(0, (sum, q) => sum + q);

  void add(String sku) {
    _items[sku] = (_items[sku] ?? 0) + 1;
    notifyListeners();
  }
}

// above the widget that needs it
ChangeNotifierProvider(
  create: (_) => CartModel(),
  child: const ShopPage(),
)

// and in a descendant
class CartBadge extends StatelessWidget {
  const CartBadge({super.key});

  @override
  Widget build(BuildContext context) {
    final count = context.select<CartModel, int>((c) => c.totalCount);
    return Badge(label: Text('$count'));
  }
}
  • context.watch rebuilds on any change, context.select only when the selected value changes, and context.read never rebuilds.
  • Call read inside callbacks and watch inside build; mixing them up is the classic Provider mistake.
  • Provide models as high in the tree as the sharing requires and no higher, or every change rebuilds unrelated widgets.

Riverpod and BLoC in practice

final counterProvider = NotifierProvider<Counter, int>(Counter.new);

class Counter extends Notifier<int> {
  @override
  int build() => 0;
  void increment() => state++;
}

// composed and overridable for tests
final greetingProvider = Provider<String>((ref) {
  final n = ref.watch(counterProvider);
  return 'Count is $n';
});

class Greeting extends ConsumerWidget {
  const Greeting({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final text = ref.watch(greetingProvider);
    return Text(text);
  }
}
⚠️
In BLoC, a state object must be immutable and each emit must produce a new instance. Mutating a list inside the current state and emitting it again means == still matches and the UI never rebuilds — a silent, confusing bug.

FAQ

Which library should a new project choose?
Riverpod for most apps: it is compile-time safe, needs no BuildContext, and its providers are trivially overridden in tests. Choose BLoC when your team already knows it or the flows have many explicit events and transitions.
Do I still need <code>setState</code> once I use a state library?
Yes. Animation controllers, form field focus, and expand/collapse toggles are local UI concerns. Putting them in a global store adds noise without benefit.

Navigation, routing and deep links with go_router Dart essentials for Flutter

Last refreshed 2026-09-18.