Local State
For state used only within one widget, setState() in a StatefulWidget is sufficient. It is simple, zero dependencies, and perfect for toggles, counters, and form fields.
class ToggleButton extends StatefulWidget {
const ToggleButton({super.key});
@override State<ToggleButton> createState() => _ToggleButtonState();
}
class _ToggleButtonState extends State<ToggleButton> {
bool _on = false;
@override Widget build(BuildContext context) =>
Switch(value: _on, onChanged: (v) => setState(() => _on = v));
}
InheritedWidget
When state must be shared across many widgets deep in the tree, InheritedWidget provides it without prop-drilling. This is what Theme.of(context) and MediaQuery.of(context) use internally.
class CounterProvider extends InheritedWidget {
final int count;
final VoidCallback increment;
const CounterProvider({
super.key,
required this.count,
required this.increment,
required super.child,
});
static CounterProvider of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<CounterProvider>()!;
}
@override
bool updateShouldNotify(CounterProvider old) => count != old.count;
}
// Usage deep in the tree:
// var provider = CounterProvider.of(context);
// Text('$${provider.count}')
Provider Package
provider is the most widely used state management solution — it wraps InheritedWidget with a clean API:
# pubspec.yaml
dependencies:
provider: ^6.1.0
import 'package:provider/provider.dart';
import 'package:flutter/foundation.dart';
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // triggers rebuild of consumers
}
}
// In main:
void main() => runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const MyApp(),
),
);
// In any widget:
class CounterDisplay extends StatelessWidget {
const CounterDisplay({super.key});
@override Widget build(BuildContext context) {
var counter = context.watch<CounterModel>(); // rebuilds on change
return Text('$${counter.count}');
}
}
class IncrementButton extends StatelessWidget {
const IncrementButton({super.key});
@override Widget build(BuildContext context) {
return ElevatedButton(
// context.read — does NOT rebuild on change
onPressed: () => context.read<CounterModel>().increment(),
child: const Text('+'),
);
}
}
Riverpod Overview
riverpod is a compile-safe, testable state management solution created by the Provider author:
import 'package:flutter_riverpod/flutter_riverpod.dart';
final counterProvider = StateNotifierProvider<CounterNotifier, int>(
(ref) => CounterNotifier(),
);
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
}
class CounterWidget extends ConsumerWidget {
const CounterWidget({super.key});
@override Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(children: [
Text('$count'),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: Text('+'),
),
]);
}
}
🧠 Quiz
1. When should you use setState() vs Provider?
- A) Always use setState
- B) setState for local widget state; Provider when multiple widgets need the same state ✅
- C) Only use Provider — setState is deprecated
- D) Only setState works with async code
2. What does context.watch<T>() do in Provider?
- A) Reads the value without rebuilding
- B) Reads the value and rebuilds the widget when it changes ✅
- C) Subscribes to a stream
- D) Dispatches an action
Summary
Choose state management based on scope: setState() for widget-local state, InheritedWidget for tree-level sharing, Provider for most apps (simple, well-documented, Flutter-team recommended), and Riverpod for compile-safe, testable, complex state. All options use Dart streams, futures, and classes you already know.