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 state | Lifetime | Tool |
|---|---|---|
| Ephemeral UI state | One widget | setState |
| Shared within a screen | Subtree | InheritedWidget or Provider |
| App-wide and injectable | Whole app | Riverpod |
| Complex event-driven flows | Feature | BLoC |
| Server data | Cache with TTL | A 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.watchrebuilds on any change,context.selectonly when the selected value changes, andcontext.readnever rebuilds.- Call
readinside callbacks andwatchinsidebuild; 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.
Related
Navigation, routing and deep links with go_router Dart essentials for Flutter
Last refreshed 2026-09-18.