Provider in Flutter
Provider is a popular state-management and dependency-injection package used in Flutter applications to make objects and application state available to widgets lower in the widget tree. It works with Flutter's inherited-widget mechanism and helps reduce the need to manually pass data and callbacks through multiple widget levels.
Flutter's official documentation demonstrates provider together with ChangeNotifier as a simple approach to application state management. The Provider package offers APIs such as Provider, ChangeNotifierProvider, Consumer, Selector, context.read(), context.watch(), and context.select(). :contentReference[oaicite:0]{index=0}
1. What is Provider?
Provider is a Flutter package that makes a value or object available to descendant widgets through the widget tree.
Instead of passing the same object manually through multiple constructors:
HomePage(
user: user,
cart: cart,
settings: settings,
);
you can provide those objects higher in the widget tree and allow descendant widgets to access them when required.
The package works as a convenient wrapper around Flutter's inherited-widget mechanism, making it easier to expose and consume values.
2. Why Do We Need Provider?
In a small application, setState() may be enough for local widget state. However, when the same state needs to be accessed by several widgets or screens, passing data manually can become difficult.
For example:
HomePage
↓
ProductPage
↓
ProductList
↓
ProductCard
↓
CartButton
If all of these widgets need access to the same shopping cart, manually passing the cart through every constructor can create unnecessary complexity.
Provider allows the cart to be exposed higher in the widget tree so descendant widgets can obtain it directly.
3. Provider and State Management
Provider itself is a mechanism for exposing and accessing objects. It is commonly combined with classes such as ChangeNotifier to create reactive application state.
A common architecture looks like this:
UI
↓
Provider
↓
ChangeNotifier / ViewModel
↓
Business Logic
↓
Repository / Service
↓
API / Database
When the state changes, ChangeNotifier can call notifyListeners(), which tells listening widgets that they need to update. Flutter's documentation uses this pattern for simple application state management. :contentReference[oaicite:1]{index=1}
4. Installing Provider
Add the package to your Flutter project using:
flutter pub add provider
You can also add the dependency manually to pubspec.yaml:
dependencies:
flutter:
sdk: flutter
provider: ^6.1.5+1
The exact package version can change over time, so check the current Provider package version on pub.dev when starting a new project.
5. Importing Provider
After installing the package, import it into your Dart file:
import 'package:provider/provider.dart';
6. Important Provider Concepts
The most important concepts to understand when learning Provider are:
Provider
ChangeNotifier
ChangeNotifierProvider
Consumer
context.read()
context.watch()
context.select()
Selector
MultiProvider
Flutter's simple state-management guide specifically introduces ChangeNotifier, ChangeNotifierProvider, and Consumer as core concepts in its Provider example. :contentReference[oaicite:2]{index=2}
7. Understanding ChangeNotifier
ChangeNotifier is a class from the Flutter SDK that provides change notification to listeners.
A state class can extend ChangeNotifier:
class CounterModel extends ChangeNotifier {
int count = 0;
void increment() {
count++;
notifyListeners();
}
}
When notifyListeners() is called, widgets listening to this notifier can rebuild and display the updated state. The Flutter documentation describes ChangeNotifier as a way to notify listeners when application state changes. :contentReference[oaicite:3]{index=3}
8. Understanding notifyListeners()
notifyListeners() does not directly modify state. It tells registered listeners that the state has changed.
Example:
void increment() {
count++;
notifyListeners();
}
The sequence is:
User Action
↓
increment()
↓
count changes
↓
notifyListeners()
↓
Listening widgets rebuild
↓
Updated UI
9. What is ChangeNotifierProvider?
ChangeNotifierProvider is a Provider widget used to expose a ChangeNotifier instance to descendant widgets.
Basic example:
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: const MyApp(),
)
The descendants of ChangeNotifierProvider can access the CounterModel.
When a notifier is created through the create callback, Provider manages its lifecycle and disposes it when it is no longer needed. :contentReference[oaicite:4]{index=4}
10. Basic Provider Architecture
ChangeNotifier
↓
ChangeNotifierProvider
↓
Widget Tree
↓
Consumer / context.watch
↓
UI
For example:
CounterModel
↓
ChangeNotifierProvider
↓
CounterScreen
↓
Consumer
↓
Text('$count')
11. Creating a Counter Model
Let's create a simple counter state class.
import 'package:flutter/foundation.dart';
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
void decrement() {
_count--;
notifyListeners();
}
void reset() {
_count = 0;
notifyListeners();
}
}
Explanation
_count stores the private state.
count exposes the value through a getter.
increment() increases the counter.
decrement() decreases the counter.
reset() sets the counter back to zero.
notifyListeners() informs listening widgets about the change.
12. Providing the Counter Model
Now provide the model above the widgets that need it.
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: const MyApp(),
),
);
}
The provider creates the CounterModel and makes it available to descendants.
13. Complete Counter Example with Provider
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
void decrement() {
if (_count > 0) {
_count--;
notifyListeners();
}
}
void reset() {
_count = 0;
notifyListeners();
}
}
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const CounterScreen(),
);
}
}
class CounterScreen extends StatelessWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context) {
final counter = context.watch();
return Scaffold(
appBar: AppBar(
title: const Text('Provider Counter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${counter.count}',
style: const TextStyle(
fontSize: 50,
fontWeight: FontWeight.bold,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: counter.decrement,
child: const Text('-'),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: counter.reset,
child: const Text('Reset'),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: counter.increment,
child: const Text('+'),
),
],
),
],
),
),
);
}
}
14. How the Counter Example Works
CounterModel extends ChangeNotifier.
CounterModel stores the counter state.
ChangeNotifierProvider creates and exposes the model.
context.watch() obtains the model and listens for changes.
- The user presses a button.
- The model changes
_count.
notifyListeners() is called.
- The listening widget rebuilds.
- The new counter value is displayed.
15. What is Consumer?
Consumer is a Provider widget that obtains a provided value and gives it to a builder function.
Basic syntax:
Consumer(
builder: (context, counter, child) {
return Text('${counter.count}');
},
)
Whenever the relevant provider notifies its listeners, the Consumer can rebuild its builder output. :contentReference[oaicite:5]{index=5}
16. Consumer Example
class CounterText extends StatelessWidget {
const CounterText({super.key});
@override
Widget build(BuildContext context) {
return Consumer(
builder: (context, counter, child) {
return Text(
'${counter.count}',
style: const TextStyle(
fontSize: 40,
),
);
},
);
}
}
17. Consumer with Button Actions
You can use a Consumer to display state while using context.read() for actions.
Column(
children: [
Consumer(
builder: (context, counter, child) {
return Text('${counter.count}');
},
),
ElevatedButton(
onPressed: () {
context.read().increment();
},
child: const Text('Increment'),
),
],
)
18. context.watch()
context.watch() obtains a provider value and listens for changes.
final counter = context.watch();
When the CounterModel calls notifyListeners(), the widget using watch can rebuild.
Use watch when the widget's UI depends on the provider's current state.
19. context.read()
context.read() obtains the provider value without establishing a listening relationship for that widget.
final counter = context.read();
counter.increment();
This is useful when you only need to call a method and do not need the current value to rebuild the widget.
For example:
ElevatedButton(
onPressed: () {
context.read().increment();
},
child: const Text('Increment'),
)
20. context.select()
context.select() allows a widget to listen to a specific part of a provider's state instead of the entire object.
final count = context.select(
(counter) => counter.count,
);
This can help reduce unnecessary rebuilds when a model contains multiple pieces of state.
21. Consumer vs context.watch()
| Feature | Consumer | context.watch() |
| Reads provider | Yes | Yes |
| Listens to changes | Yes | Yes |
| Provides builder | Yes | No |
| Useful for localized rebuilds | Yes | Can be |
| Syntax | Consumer | context.watch() |
22. Consumer vs context.read()
| Feature | Consumer | context.read() |
| Gets provider value | Yes | Yes |
| Listens for changes | Yes | No |
| Triggers rebuild on notification | Yes | No |
| Good for actions | Yes | Yes |
23. Provider.of()
Provider also supports the Provider.of(context) API.
final counter = Provider.of(context);
By default, this listens to changes. You can disable listening:
final counter = Provider.of(
context,
listen: false,
);
The equivalent modern-style calls are often easier to read:
context.watch();
context.read();
24. Provider Placement
A provider should normally be placed above the widgets that need access to it.
ChangeNotifierProvider
↓
MyApp
↓
HomeScreen
↓
ProductList
↓
ProductCard
All descendant widgets in the provider's scope can access the provided object.
Flutter's documentation recommends placing ChangeNotifierProvider above the widgets that need access to the state, but not higher than necessary. :contentReference[oaicite:6]{index=6}
25. Provider Scope
Provider uses the widget tree to determine which provided object a widget can access.
For example:
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: MaterialApp(
home: const HomeScreen(),
),
)
HomeScreen and its descendants can access CounterModel.
If a widget is outside the provider's scope, attempting to access the provider can result in a ProviderNotFoundException.
26. MultiProvider
Applications often have more than one provider.
Without MultiProvider, providers can become deeply nested:
Provider(
create: (_) => UserModel(),
child: Provider(
create: (_) => ThemeModel(),
child: ChangeNotifierProvider(
create: (_) => CartModel(),
child: MyApp(),
),
),
)
With MultiProvider:
MultiProvider(
providers: [
Provider(
create: (_) => UserModel(),
),
Provider(
create: (_) => ThemeModel(),
),
ChangeNotifierProvider(
create: (_) => CartModel(),
),
],
child: const MyApp(),
)
MultiProvider makes a group of providers easier to read and organize. :contentReference[oaicite:7]{index=7}
27. Multiple Providers Example
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => CounterModel(),
),
ChangeNotifierProvider(
create: (_) => CartModel(),
),
ChangeNotifierProvider(
create: (_) => ThemeModel(),
),
],
child: const MyApp(),
),
);
}
28. Creating a Shopping Cart Model
class CartModel extends ChangeNotifier {
final List _items = [];
List get items =>
List.unmodifiable(_items);
int get itemCount => _items.length;
void addItem(String item) {
_items.add(item);
notifyListeners();
}
void removeItem(String item) {
_items.remove(item);
notifyListeners();
}
void clearCart() {
_items.clear();
notifyListeners();
}
}
29. Reading Cart State
final cart = context.watch();
Text(
'Items: ${cart.itemCount}',
)
30. Updating Cart State
ElevatedButton(
onPressed: () {
context.read().addItem('Flutter Book');
},
child: const Text('Add to Cart'),
)
Here, read() is used because the button needs to call an action rather than listen to the cart for rebuilding.
31. Provider for Login State
Provider can also be used to manage authentication-related UI state.
class AuthModel extends ChangeNotifier {
bool _isLoggedIn = false;
bool get isLoggedIn => _isLoggedIn;
void login() {
_isLoggedIn = true;
notifyListeners();
}
void logout() {
_isLoggedIn = false;
notifyListeners();
}
}
The application can react to authentication changes:
final isLoggedIn =
context.watch().isLoggedIn;
return isLoggedIn
? const HomeScreen()
: const LoginScreen();
32. Provider for Theme State
class ThemeModel extends ChangeNotifier {
bool _darkMode = false;
bool get darkMode => _darkMode;
void toggleTheme() {
_darkMode = !_darkMode;
notifyListeners();
}
}
A widget can update the theme setting:
IconButton(
onPressed: () {
context.read().toggleTheme();
},
icon: const Icon(Icons.dark_mode),
)
33. Provider for Loading, Success, and Error States
Provider combined with ChangeNotifier can manage more than simple counters. It can also represent asynchronous UI states.
class DataModel extends ChangeNotifier {
bool isLoading = false;
String? data;
String? error;
Future loadData() async {
isLoading = true;
error = null;
notifyListeners();
try {
await Future.delayed(
const Duration(seconds: 2),
);
data = 'Data loaded successfully';
isLoading = false;
notifyListeners();
} catch (e) {
error = 'Unable to load data';
isLoading = false;
notifyListeners();
}
}
}
Flutter's current learning pathway demonstrates ChangeNotifier for loading, success, and error state and uses notifyListeners() to trigger UI updates. :contentReference[oaicite:8]{index=8}
34. Displaying Loading and Error States
Consumer(
builder: (context, model, child) {
if (model.isLoading) {
return const CircularProgressIndicator();
}
if (model.error != null) {
return Text(model.error!);
}
return Text(model.data ?? 'No data');
},
)
35. Provider with API Services
A common application architecture separates API communication from state management.
UI
↓
ChangeNotifier
↓
Repository / Service
↓
HTTP API
Example service:
class ApiService {
Future fetchUsers() async {
await Future.delayed(
const Duration(seconds: 1),
);
return 'Users loaded';
}
}
Example ViewModel:
class UserModel extends ChangeNotifier {
final ApiService apiService;
UserModel(this.apiService);
bool isLoading = false;
String? users;
Future loadUsers() async {
isLoading = true;
notifyListeners();
users = await apiService.fetchUsers();
isLoading = false;
notifyListeners();
}
}
36. Providing Dependencies with Provider
Provider can expose services as well as state objects.
MultiProvider(
providers: [
Provider(
create: (_) => ApiService(),
),
ChangeNotifierProvider(
create: (context) =>
UserModel(context.read()),
),
],
child: const MyApp(),
)
This allows the ViewModel to obtain the service from the provider tree. Flutter's architecture documentation also demonstrates Provider for dependency injection of services and repositories. :contentReference[oaicite:9]{index=9}
37. Provider for Repository Architecture
For larger applications, Provider can be organized into layers:
UI
↓
ViewModel / ChangeNotifier
↓
Repository
↓
Service
↓
Remote API / Database
This keeps UI code focused on displaying state and responding to user interaction while data access and business logic can be separated into dedicated classes.
38. Selector
Selector is useful when a widget only needs a specific part of a provider's state.
Selector(
selector: (context, cart) => cart.itemCount,
builder: (context, itemCount, child) {
return Text(
'Items: $itemCount',
);
},
)
The Provider package documents Selector as an option for filtering updates and avoiding unnecessary rebuilds.
39. Consumer with child
Consumer can receive an optional child that does not need to be rebuilt when the provider changes.
Consumer(
child: const Icon(Icons.star),
builder: (context, counter, child) {
return Row(
children: [
Text('${counter.count}'),
if (child != null) child,
],
);
},
)
This can help keep rebuilds limited to the part of the widget tree that depends on changing state. Flutter's documentation specifically recommends using the child parameter for subtrees that do not depend on changing provider state. :contentReference[oaicite:10]{index=10}
40. Provider.of() with listen: false
If you need to access a provider without listening for changes, you can use:
final counter = Provider.of(
context,
listen: false,
);
counter.increment();
This is conceptually similar to:
context.read().increment();
41. Provider.of() with Listening
By default, Provider.of(context) listens for changes.
final counter = Provider.of(context);
return Text('${counter.count}');
When the provider notifies its listeners, the dependent widget can rebuild.
42. Provider and setState()
setState() and Provider solve related but different problems.
| setState() | Provider |
| Built into Flutter | External package |
| Excellent for local widget state | Useful for shared application state |
| State stored in State object | State can be stored in separate model classes |
| Simple for small widgets | Useful for larger state-sharing requirements |
| Uses setState() | Often uses ChangeNotifier and notifyListeners() |
43. When Should You Use Provider?
Provider can be useful when:
- Multiple widgets need the same state.
- State needs to be shared across screens.
- You want to separate state logic from UI.
- You want to expose services or repositories through the widget tree.
- You want a relatively lightweight state-management approach.
- You want to use
ChangeNotifier for reactive updates.
44. When Should You Use setState() Instead?
For simple local UI state, Provider may add unnecessary structure.
For example:
- Password visibility
- Checkbox selection
- One local expandable section
- A small counter used by one widget
- Temporary animation-related state
In these situations, setState() may be simpler.
45. Provider Best Practices
- Keep state classes focused on state and related logic.
- Place providers at the lowest practical level that needs the state.
- Use
context.read() when you need to perform an action without listening.
- Use
context.watch() when the UI depends on changing provider state.
- Use
Consumer or Selector when localized rebuilds are useful.
- Call
notifyListeners() after changing state that listeners depend on.
- Keep API and database logic out of presentation widgets where practical.
- Use repositories or services when application complexity increases.
- Use
MultiProvider to organize multiple providers.
- Use
create when Provider should create and own a new object.
- Use
.value when providing an existing object instance according to Provider's documented lifecycle rules.
46. create vs .value
When creating a new object, Provider recommends using the normal create constructor.
Example:
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const MyApp(),
)
When reusing an existing ChangeNotifier instance, the .value constructor is appropriate:
ChangeNotifierProvider.value(
value: existingCounter,
child: const MyWidget(),
)
The Provider documentation recommends create for new objects and .value for exposing an existing instance.
47. Provider and Automatic Disposal
When a ChangeNotifierProvider creates a notifier using create, Provider manages its lifecycle and automatically disposes the notifier when it is no longer needed. :contentReference[oaicite:11]{index=11}
This is one reason it is important to use the correct constructor when creating or reusing objects.
48. Provider and Lazy Creation
Provider's create and update callbacks are lazy by default. This means the object can be created when it is first requested rather than necessarily being created immediately.
Provider(
create: (_) => MyService(),
child: const MyApp(),
)
If you need to disable lazy creation, Provider supports the lazy parameter:
Provider(
create: (_) => MyService(),
lazy: false,
child: const MyApp(),
)
49. Common Provider Error: ProviderNotFoundException
A common error occurs when a widget attempts to read a provider that is not above it in the widget tree.
For example:
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
final counter = context.watch();
return Text('${counter.count}');
}
}
If CounterModel has not been provided above HomeScreen, Provider cannot find it.
Correct:
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const HomeScreen(),
)
50. Common Provider Error: Wrong BuildContext
Another common mistake is attempting to access a provider using a BuildContext that belongs to a widget above the provider.
For example, when using a provider created in the same build method, a separate descendant context may be needed.
One simple solution is to place the consuming widget below the provider:
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const CounterScreen(),
)
The CounterScreen now receives a context that is inside the provider's scope.
51. Common Provider Mistakes
- Forgetting to add the provider package.
- Forgetting to import
package:provider/provider.dart.
- Using a provider below the widget that needs it.
- Using the wrong generic type.
- Forgetting to call
notifyListeners() after a relevant state change.
- Using
watch() when only an action is required.
- Using
read() when the UI actually needs to listen for changes.
- Using
ChangeNotifierProvider.value incorrectly to create new objects.
- Putting too much unrelated business logic into one ChangeNotifier.
- Creating unnecessarily large provider scopes.
52. Provider Project Structure
A scalable Flutter project can organize Provider-related classes separately.
lib/
├── main.dart
├── models/
│ ├── product.dart
│ └── user.dart
├── providers/
│ ├── auth_provider.dart
│ ├── cart_provider.dart
│ └── theme_provider.dart
├── services/
│ ├── api_service.dart
│ └── auth_service.dart
├── repositories/
│ └── product_repository.dart
├── screens/
│ ├── home_screen.dart
│ ├── login_screen.dart
│ └── cart_screen.dart
└── widgets/
├── product_card.dart
└── cart_button.dart
53. Example Provider Class
import 'package:flutter/foundation.dart';
class CartProvider extends ChangeNotifier {
final List _items = [];
List get items =>
List.unmodifiable(_items);
int get itemCount => _items.length;
bool contains(String item) {
return _items.contains(item);
}
void addItem(String item) {
if (!_items.contains(item)) {
_items.add(item);
notifyListeners();
}
}
void removeItem(String item) {
if (_items.remove(item)) {
notifyListeners();
}
}
void clear() {
if (_items.isNotEmpty) {
_items.clear();
notifyListeners();
}
}
}
54. Registering Multiple Providers
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => CartProvider(),
),
ChangeNotifierProvider(
create: (_) => AuthProvider(),
),
ChangeNotifierProvider(
create: (_) => ThemeProvider(),
),
],
child: const MyApp(),
),
);
}
55. Consuming Multiple Providers
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
final cart = context.watch();
final auth = context.watch();
return Column(
children: [
Text(
'Welcome ${auth.username}',
),
Text(
'Cart Items: ${cart.itemCount}',
),
],
);
}
}
56. Provider and Firebase
Provider can be used as a state-management layer around Firebase operations.
For example:
UI
↓
FirebaseProvider / ViewModel
↓
Firebase Service
↓
Firebase Authentication / Firestore / Storage
A provider can manage:
- Authentication state
- Firestore loading state
- Firestore data
- Firebase Storage upload state
- Error messages
- User profile state
This keeps Firebase interaction and UI state organized rather than putting all Firebase operations directly inside widgets.
57. Provider with Authentication State
class AuthProvider extends ChangeNotifier {
bool _isLoggedIn = false;
bool get isLoggedIn => _isLoggedIn;
Future login() async {
_isLoggedIn = true;
notifyListeners();
}
void logout() {
_isLoggedIn = false;
notifyListeners();
}
}
The UI can react to the authentication state:
class AppRouter extends StatelessWidget {
const AppRouter({super.key});
@override
Widget build(BuildContext context) {
final auth = context.watch();
if (auth.isLoggedIn) {
return const HomeScreen();
}
return const LoginScreen();
}
}
58. Provider with Form State
Provider can also manage larger or shared form state.
class RegistrationProvider extends ChangeNotifier {
String name = '';
String email = '';
bool isLoading = false;
void setName(String value) {
name = value;
notifyListeners();
}
void setEmail(String value) {
email = value;
notifyListeners();
}
Future register() async {
isLoading = true;
notifyListeners();
await Future.delayed(
const Duration(seconds: 2),
);
isLoading = false;
notifyListeners();
}
}
59. Provider and Separation of Concerns
One of the important benefits of using Provider with ChangeNotifier is that UI code does not have to contain every piece of state-management logic.
Instead of:
Screen
├── API call
├── loading logic
├── error logic
├── data transformation
└── UI
You can separate responsibilities:
Screen
↓
Provider / ViewModel
↓
Repository
↓
Service
↓
API
This can make larger applications easier to test, maintain, and extend.
60. Provider and Rebuild Optimization
Provider offers several ways to control which widgets listen to state changes.
Consumer
Selector
context.select()
Consumer.child
These APIs can help avoid rebuilding widgets that do not depend on the changed portion of state. :contentReference[oaicite:12]{index=12}
61. Example of context.select()
class UserProvider extends ChangeNotifier {
String name = 'John';
int age = 25;
void updateName(String value) {
name = value;
notifyListeners();
}
void updateAge(int value) {
age = value;
notifyListeners();
}
}
A widget that only needs the name can select it:
final name = context.select(
(user) => user.name,
);
return Text(name);
The widget is interested in the selected value rather than every property of the provider.
62. Provider vs Other State Management Approaches
| Approach | Typical Use |
| setState() | Simple local widget state |
| Provider | Shared state and dependency exposure |
| ChangeNotifier | Observable state model |
| Riverpod | Provider-style reactive state management |
| BLoC/Cubit | Structured reactive state and business logic |
| ValueNotifier | Small observable values |
The choice depends on the application's requirements, team preferences, architecture, and complexity.
63. Provider Advantages
- Simple to learn.
- Works naturally with Flutter's widget tree.
- Reduces manual prop drilling.
- Supports dependency injection.
- Works well with ChangeNotifier.
- Provides Consumer and Selector for controlled rebuilding.
- Supports multiple provider types.
- Can be used for state, services, repositories, and other dependencies.
- Has official Flutter documentation examples.
- Does not require a large amount of boilerplate for basic use cases.
64. Provider Limitations
- Large ChangeNotifier classes can become difficult to maintain.
- Complex applications may require more structured state architecture.
- Incorrect provider placement can cause runtime errors.
- Developers need to understand listening versus non-listening access.
- Too many providers can make architecture difficult to understand if not organized carefully.
- Business logic should not automatically be placed into one large provider class.
65. Provider Best Practice Example
MultiProvider(
providers: [
Provider(
create: (_) => ApiService(),
),
ChangeNotifierProvider(
create: (context) =>
UserProvider(
context.read(),
),
),
],
child: const MyApp(),
)
This example separates the API service from the state-management class.
66. Complete Practical Provider Example
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CartProvider extends ChangeNotifier {
final List _items = [];
List get items =>
List.unmodifiable(_items);
int get itemCount => _items.length;
void addItem(String item) {
_items.add(item);
notifyListeners();
}
void removeItem(String item) {
_items.remove(item);
notifyListeners();
}
void clearCart() {
_items.clear();
notifyListeners();
}
}
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CartProvider(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const ProductScreen(),
);
}
}
class ProductScreen extends StatelessWidget {
const ProductScreen({super.key});
@override
Widget build(BuildContext context) {
final cart = context.watch();
return Scaffold(
appBar: AppBar(
title: Text(
'Cart: ${cart.itemCount}',
),
),
body: ListView(
children: [
ListTile(
title: const Text('Flutter Course'),
trailing: ElevatedButton(
onPressed: () {
context
.read()
.addItem('Flutter Course');
},
child: const Text('Add'),
),
),
ListTile(
title: const Text('Dart Course'),
trailing: ElevatedButton(
onPressed: () {
context
.read()
.addItem('Dart Course');
},
child: const Text('Add'),
),
),
const SizedBox(height: 20),
...cart.items.map(
(item) => ListTile(
title: Text(item),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
context
.read()
.removeItem(item);
},
),
),
),
],
),
);
}
}
67. Step-by-Step Flow of the Practical Example
- The application creates a
CartProvider.
ChangeNotifierProvider exposes it to the widget tree.
- The product screen uses
context.watch() to display the cart count.
- The user taps Add.
context.read() obtains the provider.
addItem() modifies the cart.
notifyListeners() is called.
- Widgets listening to the cart are rebuilt.
- The updated cart count and item list appear.
68. Interview Questions
Q1. What is Provider in Flutter?
Answer: Provider is a package that makes objects and state available to descendant widgets through the Flutter widget tree.
Q2. Why is Provider used?
Answer: It helps share state and dependencies between widgets without manually passing the same objects through multiple widget constructors.
Q3. What is ChangeNotifier?
Answer: ChangeNotifier is a Flutter class that allows an object to notify registered listeners when its state changes.
Q4. What does notifyListeners() do?
Answer: It signals listening widgets that the state has changed so they can rebuild.
Q5. What is ChangeNotifierProvider?
Answer: It provides a ChangeNotifier instance to descendant widgets and manages the lifecycle of an instance created through its create callback.
Q6. What is Consumer?
Answer: Consumer obtains a provided object and gives it to a builder function that can rebuild when the provider changes.
Q7. What is context.watch()?
Answer: It obtains a provider value and listens for changes.
Q8. What is context.read()?
Answer: It obtains a provider value without listening for changes, making it useful for calling actions or methods.
Q9. What is context.select()?
Answer: It listens to a selected part of a provider's state and can help reduce unnecessary rebuilds.
Q10. What is MultiProvider?
Answer: MultiProvider allows multiple providers to be declared in a more readable and organized structure.
Q11. What is the difference between Provider and ChangeNotifierProvider?
Answer: Provider is a general mechanism for exposing a value, while ChangeNotifierProvider is specifically designed to provide a ChangeNotifier and react to its notifications.
Q12. When should setState() be used instead of Provider?
Answer: For simple local widget state, setState() is often sufficient. Provider becomes useful when state or dependencies need to be shared or separated from individual widgets.
69. Quick Revision
- Provider is a popular Flutter package for exposing state and dependencies.
- It works with Flutter's inherited-widget mechanism.
ChangeNotifier can hold mutable application state.
notifyListeners() signals that state has changed.
ChangeNotifierProvider provides a ChangeNotifier to descendants.
Consumer listens to provider changes and rebuilds its builder.
context.watch() reads and listens.
context.read() reads without listening.
context.select() listens to a selected value.
Selector can help filter rebuilds.
MultiProvider organizes multiple providers.
- Use
create to create new provider-owned objects.
- Use
.value appropriately when exposing an existing instance.
- Keep provider scope as focused as practical.
70. Learning Outcome
After completing this topic, you should be able to:
- Explain what Provider is.
- Install and configure the Provider package.
- Create a ChangeNotifier-based state class.
- Use ChangeNotifierProvider.
- Use Consumer to rebuild UI from provider state.
- Use context.watch(), context.read(), and context.select().
- Use MultiProvider for multiple dependencies.
- Manage counters, carts, authentication, themes, and API states.
- Separate UI from state-management logic.
- Understand Provider lifecycle and common mistakes.
- Choose between local setState() and shared Provider-based state management.
71. Useful Flutter Resources
72. JustAcademy Flutter Resources
For structured Flutter training, practical development, state management, Firebase, API integration, and project-based learning, explore the following resources:
73. Summary
Provider is a useful approach for sharing state and dependencies across a Flutter widget tree. It is especially commonly used with ChangeNotifier, where the model stores application state and calls notifyListeners() when that state changes.
ChangeNotifierProvider makes the notifier available to descendant widgets, while Consumer, context.watch(), context.read(), context.select(), and Selector provide different ways to access and react to that state.
For simple local state, setState() can remain the simplest solution. For state shared across multiple widgets or screens, Provider can provide a more organized structure by separating state-management logic from UI code.