Provider — a state management package for Flutter, created by Remi Rousselet in 2019 as a wrapper over InheritedWidget. Provider solves the problem of passing data down the widget tree without props drilling: any widget can access state via context.read<T>() or context.watch<T>(). According to pub.dev, Provider is the most popular Flutter state manager with over 25 thousand likes.
Key Takeaways
Provider — a package for state management and dependency injection in Flutter, built on top of InheritedWidget. Provider provides an object (state, service, repository) in the widget tree and automatically rebuilds the UI when data changes. Unlike direct use of InheritedWidget, Provider eliminates all boilerplate: no need to write an InheritedWidget subclass, set up a static of() method, or manage nesting.
Provider is the officially recommended way to manage state in Flutter by Google (Flutter Team, 2019–2023). The package is part of the Flutter Ecosystem and is maintained by the Flutter team. At its release, Provider was proposed as a replacement for global variables and InheritedWidget: any object is accessible from anywhere without passing through the constructor.
According to the Flutter Community Survey 2025, Provider is used in 72% of Flutter applications. The main reasons for its popularity are: minimal entry threshold, built-in ChangeNotifier support, compatibility with other architectures (MVVM, BLoC), and no external dependencies.
ChangeNotifier — a built-in Flutter class that implements the Listener pattern. ChangeNotifier notifies subscribers of changes by calling notifyListeners(). In the context of Provider, ChangeNotifier is the main class for state: you create a class that extends ChangeNotifier with fields and methods that call notifyListeners() after data changes.
class CounterProvider extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
void reset() {
_count = 0;
notifyListeners();
}
}notifyListeners rules: call it after fully modifying the data — not in the middle of a method, but at the end. If a method performs multiple changes, call notifyListeners() once after all changes, not after each one. This prevents multiple redraws in a single logical step. For batch updates, use notifyListeners together with setState-like patterns.
Alternatives to ChangeNotifier: ValueNotifier — for a single value (good for primitives), StateNotifier — from the state_notifier package (rarely used alone). Most Provider solutions use ChangeNotifier due to its built-in support and simplicity.
Consumer — a widget that subscribes to ChangeNotifier and rebuilds on each call to notifyListeners(). Consumer takes a builder function with three parameters: context, model, child. Child — a widget that does not depend on the model and is not rebuilt by Consumer. This is an optimization: if Consumer contains a static widget (icon, text without data), it is passed via child and is not recreated.
Consumer<CounterProvider>(
builder: (context, provider, child) => Column(
children: [
child!, // not rebuilt
Text('${provider.count}'),
ElevatedButton(
onPressed: () => provider.increment(),
child: Icon(Icons.add),
),
],
),
child: Text('Counter:'),
)context.watch — a BuildContext extension method for subscribing to a Provider. Returns the model and subscribes the current widget to its changes. context.read — access without subscription (for onPressed handlers, initState, and dispose). context.select — subscription to a specific field of the model without rebuilding when other fields change. Select is the most performant option for complex models with 10+ fields.
When to use Consumer, watch, or select: Consumer — when a child widget is needed for optimization. watch — in the build method for simple reading. select — when the model has multiple fields but the widget depends on only one. Provider automatically unsubscribes when the widget is destroyed, preventing memory leaks.
MultiProvider — a widget for registering multiple Providers without nesting. Instead of a tree with 5 levels of Provider → Provider → Provider, MultiProvider takes a list of providers. Each subsequent Provider can use the previous ones via the constructor. MultiProvider is the standard way to organize the root level of an application.
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CartProvider()),
ChangeNotifierProvider(create: (_) => AuthProvider()),
ProxyProvider<AuthProvider, OrderProvider>(
update: (_, auth, __) => OrderProvider(auth.userId),
),
],
child: MaterialApp(home: HomePage()),
)ProxyProvider — a Provider that depends on another Provider. ProxyProvider gets values from other Providers and passes them to its object. For example, OrderProvider depends on AuthProvider (needs userId). When AuthProvider changes, ProxyProvider automatically recreates OrderProvider with the new userId. ChangeNotifierProxyProvider — the ProxyProvider version for ChangeNotifier.
StreamProvider and FutureProvider: StreamProvider subscribes to a Stream (Firebase, WebSocket) and updates Consumer on each new event. FutureProvider — for async initialization: runs a Future, shows loading, then passes the result to widgets. Both solve common tasks without manual subscription management.
Provider is tested by wrapping the widget in a MultiProvider with test values. No real API or database is needed for testing — the Provider is replaced with a mocked object. The provider package provides ProviderScope for test isolation — each test creates its own Provider tree independently.
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Counter increments on button tap',
(tester) async {
await tester.pumpWidget(
ChangeNotifierProvider(
create: (_) => CounterProvider(),
child: CounterScreen(),
),
);
await tester.tap(find.byKey(Key('increment')));
await tester.pump();
expect(find.text('1'), findsOneWidget);
},
);
}MockProvider: to test widgets with a Provider that depends on an API, create a stub subclass or use mockito / mocktail. Provider does not require special mocking tools — any object extending ChangeNotifier can be passed via create without calling the real service. Program Providers through interfaces (abstract class) for easy substitution.
Provider performance is based on InheritedWidget: when a Provider changes, all widgets subscribed via context.watch or Consumer are rebuilt. To prevent unnecessary redraws, use context.select (subscription to a specific field), Consumer with the child parameter, and const for static widgets. Provider does not rebuild branches not subscribed to changes.
| Method | Subscription | Rebuild | Use Case |
|---|---|---|---|
| context.watch | Full model | Any change | Simple widgets |
| Consumer | Full model | Any change | With child optimization |
| context.select | Specific field | Only when field changes | Complex models |
| context.read | No | Never | Event handlers |
Limitations: Provider does not support business logic isolation at the Event level (like BLoC). All changes happen through direct calls to ChangeNotifier methods, which may lead to uncontrolled change chains. For complex scenarios (multiple async operations, complex validation), Provider falls short compared to BLoC and Riverpod.
Migration from Provider: Provider can be easily combined with other packages. To migrate to Riverpod, use ChangeNotifierProvider.adaptive — an adapter that allows using existing ChangeNotifiers with Riverpod without rewriting. For BLoC — BlocProvider can be placed inside a Provider tree, gradually replacing ChangeNotifier with Bloc.
Frequently Asked Questions
Provider — a wrapper over InheritedWidget for dependency injection with ChangeNotifier. BLoC — an architectural pattern with Event + Stream for logic isolation. Provider is easier to learn, BLoC structures code more strictly. Provider suits small applications and UI state, BLoC is for complex business logic. According to Flutter Community 2025, both are often used together in the same project.
ChangeNotifierProvider — a type of Provider for ChangeNotifier instances. It creates the object via create, provides it to descendants, and rebuilds Consumer when notifyListeners is called. ChangeNotifierProvider automatically calls dispose on ChangeNotifier when removed from the tree. There are three creation methods: ChangeNotifierProvider.value (for an existing object), ChangeNotifierProvider (for lazy creation), and ChangeNotifierProvider.create (for explicit lazy creation).
Use context.select instead of context.watch — the widget rebuilds only when the selected field changes. Split large ChangeNotifiers into several small ones (one model — one responsibility). Use Consumer child for static parts. For lists, use ListView.builder with keys. Provider DevTools (Flutter Inspector) shows which widgets are rebuilding and why.
Yes. Provider (without ChangeNotifier) — for injecting immutable objects (repository, API client, configuration). ValueListenableProvider — for ValueNotifier. StreamProvider — for Stream (Firebase, WebSocket). FutureProvider — for Future (loading configuration at startup). ProxyProvider — for Providers that depend on other Providers. ChangeNotifier is only needed for mutable state with UI updates.
ProviderNotFoundException — a runtime exception that occurs when trying to get a Provider that was not declared higher in the widget tree. Common causes: Provider is declared lower than the widget trying to read it; Provider is declared in one route and read in another; a typo in the type. Solution: move the Provider higher up the tree or use MultiProvider at the MaterialApp level for global dependencies.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also