State management is the core challenge of any Flutter app. Here are the most common approaches, from simplest to most robust.
setState — local widget state
Best for: small, single-widget state that doesn’t need to be shared.
class Counter extends StatefulWidget {
@override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $_count'),
ElevatedButton(
onPressed: () => setState(() => _count++),
child: Text('Increment'),
),
],
);
}
}Provider — recommended default
Best for: most apps. Simple, well-documented, built on InheritedWidget.
Add the dependency in pubspec.yaml:
dependencies:
provider: ^6.1.0Define a change notifier:
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}Wrap your app and consume:
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: MyApp(),
),
);
}
class CounterWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = context.watch<CounterModel>();
return Column(
children: [
Text('Count: ${counter.count}'),
ElevatedButton(
onPressed: () => context.read<CounterModel>().increment(),
child: Text('Increment'),
),
],
);
}
}- Use
context.watch<T>()to listen (rebuilds on change) - Use
context.read<T>()to access without listening (e.g. inonPressed)
Riverpod — compile-time safe, testable
Best for: apps that want compile-time safety and less boilerplate than Provider.
dependencies:
flutter_riverpod: ^2.5.0// Define a provider
final counterProvider = StateProvider<int>((ref) => 0);
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: Text('Increment'),
),
],
);
}
}Bloc — event-driven, scalable
Best for: larger apps with complex state transitions and dedicated teams.
dependencies:
flutter_bloc: ^8.1.0// Events
abstract class CounterEvent {}
class Increment extends CounterEvent {}
// State
class CounterState {
final int count;
CounterState(this.count);
}
// Bloc
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterState(0)) {
on<Increment>((event, emit) {
emit(CounterState(state.count + 1));
});
}
}
// Widget
class CounterWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CounterBloc(),
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Column(
children: [
Text('Count: ${state.count}'),
ElevatedButton(
onPressed: () => context.read<CounterBloc>().add(Increment()),
child: Text('Increment'),
),
],
);
},
),
);
}
}Which should you use?
| Approach | Complexity | Best for |
|---|---|---|
setState |
Low | Single-widget, ephemeral state |
| Provider | Medium | Most apps, shared state |
| Riverpod | Medium | Apps wanting compile-time safety |
| Bloc | High | Complex state machines, large teams |