Shared State with Provider
React solves shared state with the Context API or Redux — you create a context, wrap your tree in a provider, and consume it from any descendant. Flutter’s Provider package works the same way: you define a ChangeNotifier class (your state), wrap your tree in ChangeNotifierProvider, and read from any descendant via context.watch or context.read. If you’ve used React Context with useReducer, Provider will feel immediately familiar.
Side-by-side comparison
Section titled “Side-by-side comparison”// React: Context API + useReducerconst CounterContext = createContext(null);
function counterReducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; default: return state; }}
export function CounterProvider({ children }) { const [state, dispatch] = useReducer(counterReducer, { count: 0 }); return ( <CounterContext.Provider value={{ state, dispatch }}> {children} </CounterContext.Provider> );}
// Consuming componentfunction CounterDisplay() { const { state } = useContext(CounterContext); return <p>Count: {state.count}</p>;}
function IncrementButton() { const { dispatch } = useContext(CounterContext); return ( <button onClick={() => dispatch({ type: 'increment' })}> Increment </button> );}// Flutter: Provider + ChangeNotifierclass CounterModel extends ChangeNotifier { int _count = 0; int get count => _count;
void increment() { _count++; notifyListeners(); // equivalent to dispatch / setState }}
// Wrapping the tree — identical to Context.Providervoid main() { runApp( ChangeNotifierProvider( create: (_) => CounterModel(), child: const MyApp(), ), );}
// Consuming widgetsclass CounterDisplay extends StatelessWidget { @override Widget build(BuildContext context) { // context.watch rebuilds this widget when notifyListeners() fires final count = context.watch<CounterModel>().count; return Text('Count: ${count}'); }}
class IncrementButton extends StatelessWidget { @override Widget build(BuildContext context) { // context.read does NOT subscribe — safe for callbacks return ElevatedButton( onPressed: () => context.read<CounterModel>().increment(), child: const Text('Increment'), ); }}Runnable example
Section titled “Runnable example”import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const MyApp(),
),
);
}
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: CounterPage(),
),
),
);
}
}
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
CounterDisplay(),
SizedBox(height: 24),
IncrementButton(),
],
);
}
}
class CounterDisplay extends StatelessWidget {
const CounterDisplay({super.key});
@override
Widget build(BuildContext context) {
final counter = context.watch<CounterModel>();
return Card(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Count',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
const SizedBox(height: 8),
Text(
'${counter.count}',
style: const TextStyle(
fontSize: 64,
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
),
],
),
),
);
}
}
class IncrementButton extends StatelessWidget {
const IncrementButton({super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton.icon(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () => context.read<CounterModel>().increment(),
icon: const Icon(Icons.add),
label: const Text('Increment', style: TextStyle(fontSize: 16)),
);
}
}Riverpod and Bloc are popular alternatives with more features — Provider is the recommended starting point.