Provider
Provider is Flutter’s mainstream state-management library: ChangeNotifierProvider exposes a model, Consumer/Selector subscribes to it, dependency injection happens via Provider.of or context.watch/read. It’s the gentle on-ramp before Riverpod / Bloc / Redux.
ChangeNotifier, Consumer, Selector, MultiProvider
EXAMPLE
// 1) Install
// pubspec.yaml
// dependencies:
// provider: ^6.1.2
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// 2) A simple ChangeNotifier model
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // tells watchers to rebuild
}
void reset() {
_count = 0;
notifyListeners();
}
}
// 3) Provide + Consume
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: const CounterDisplay(),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CounterModel>().increment(),
child: const Icon(Icons.add),
),
),
);
}
}
class CounterDisplay extends StatelessWidget {
const CounterDisplay({super.key});
@override
Widget build(BuildContext context) {
final count = context.watch<CounterModel>().count;
return Center(child: Text('Count: $count', style: const TextStyle(fontSize: 36)));
}
}
// 4) context.watch / read / select
// context.watch<T>() — subscribes; rebuilds when notify fires
// context.read<T>() — no subscription; for callbacks / one-shot reads
// context.select<T, R>() — subscribes, but rebuilds only when the selector value changes
class NameView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final name = context.select<User, String>((u) => u.name);
return Text(name);
}
}
// 5) MultiProvider — many models
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthModel()),
ChangeNotifierProvider(create: (_) => CartModel()),
Provider<ApiClient>(create: (_) => ApiClient()),
ProxyProvider<AuthModel, ApiClient>(
update: (_, auth, prev) => ApiClient(token: auth.token),
),
],
child: const App(),
);
// 6) Consumer widget — alternative when you want explicit scope
Consumer<CounterModel>(
builder: (context, model, child) {
return Text('Count: ${model.count}');
},
)
// 7) Consumer with a non-rebuilding child
Consumer<CounterModel>(
builder: (context, model, child) {
return Column(
children: [
Text('Count: ${model.count}'),
child!, // doesn't rebuild
],
);
},
child: const ExpensiveStaticWidget(),
)
// 8) Future + Stream providers
FutureProvider<User>(
create: (_) => api.fetchUser(),
initialData: User.empty(),
child: const UserView(),
);
StreamProvider<int>(
create: (_) => Stream.periodic(const Duration(seconds: 1), (i) => i),
initialData: 0,
child: const TickerView(),
);
// 9) Scoped providers
// Place a Provider deep in the tree → only descendants see it.
// Multiple instances of the same provider type exist in different subtrees.
class NotesScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => NotesModel(),
child: const NotesPage(),
);
}
}
// 10) Provider DISPOSE — automatic
// ChangeNotifierProvider calls notifier.dispose() when it leaves the tree.
// 11) Testing
import 'package:flutter_test/flutter_test.dart';
testWidgets('counter increments', (tester) async {
final model = CounterModel();
await tester.pumpWidget(
ChangeNotifierProvider.value(
value: model,
child: const MaterialApp(home: CounterDisplay()),
),
);
expect(find.text('Count: 0'), findsOneWidget);
model.increment();
await tester.pump();
expect(find.text('Count: 1'), findsOneWidget);
});
// 12) ChangeNotifier vs ValueNotifier vs custom
// • ValueNotifier<T> — single value; lightweight
// • ChangeNotifier — many fields; getters; coarse-grained
// • Custom — extend ChangeNotifier; implement equality if using Selector
// 13) Avoid common pitfalls
// • Calling notifyListeners from build → infinite loop
// • Reading provider in initState — use addPostFrameCallback or context.read
// • Using watch in callbacks (onPressed) — overrebuilds; use read
// • Forgetting select when only one field matters → unnecessary rebuilds
// • Creating model in build() instead of provider.create → model recreated every frame
// 14) Provider vs Riverpod
// Provider (the package above) is the original. Riverpod is the same author's modern successor:
// • Compile-time safety (typed providers, no inheritedWidget lookups failing at runtime)
// • Easier testing + overrides
// • Works outside the widget tree
// For NEW projects, prefer Riverpod. For existing Provider codebases, both are fine.
// 15) Common bugs
// • 'ProviderNotFoundException' — calling Provider.of above its declaration; check tree
// • Model rebuilds parent → use Selector to scope rebuilds
// • Dispose called too early (Hot reload) → ChangeNotifier may emit during HMR; harmless
// • Multiple providers of the same type at different levels — closest one wins; surprising
// • Mutating state without notifyListeners → UI doesn't update
// • Forgot to wrap with ChangeNotifierProvider.value when re-using existing model in tests
Why it matters
Provider gives you DI + observable models with very little ceremony: ChangeNotifierProvider at the top, context.watch/select for subscriptions, context.read in callbacks, MultiProvider for multiple models. Reach for Selector to scope rebuilds, and consider Riverpod for new apps — same mental model, with compile-time safety and easier testing.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const MyApp(),
);
// inside a widget
final model = context.watch<CounterModel>();
Try it Yourself »
Discussion
Loading…