Skip to content

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.

React
// React: Context API + useReducer
const 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 component
function 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
// Flutter: Provider + ChangeNotifier
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // equivalent to dispatch / setState
}
}
// Wrapping the tree — identical to Context.Provider
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const MyApp(),
),
);
}
// Consuming widgets
class 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'),
);
}
}
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.

What does calling `notifyListeners()` inside a `ChangeNotifier` method do?
What is the key difference between `context.watch<T>()` and `context.read<T>()`?
What is the React equivalent of a Flutter `ChangeNotifier` class?