Flutter: State Management with Provider
Provider is one of the most popular and Google-recommended packages for state management in Flutter. It is used to share data across the widget tree and listen to changes.
What is State and Why Should It Be Managed?
State is the data condition your application has at any given moment. For example:
- Whether the user is logged in
- Number of items in the cart
- Theme preference (light/dark)
- Values in form fields
- Data from APIs
Without state management, sharing this data between widgets and reflecting updates becomes very difficult.
Limitations of setState
Flutter uses setState for simple state management. However, as your app grows, you face serious problems:
1. Prop Drilling Problem
// You have to pass data 5 levels down
GrandParent(
child: Parent(
child: Child(
child: GrandChild(
child: GreatGrandChild(
userData: userData, // Must be passed all the way!
),
),
),
),
)2. Unnecessary Rebuilds
When you use setState, the entire widget rebuilds - not just the changed part.
3. State Loss
State can be lost when navigating the widget tree. Data resets when you change pages and return.
4. Untestability
Writing unit tests becomes difficult when UI and business logic are intertwined.
5. Code Repetition
You need to constantly pass parameters to use the same data in multiple places.
How Provider Solves These Problems
✅ Centralized State Management
Data is managed from a single place, accessible from everywhere.
✅ Efficient Rebuilds
Only changed widgets are rebuilt.
✅ Separation of Concerns
Business logic and UI are separated from each other.
✅ Easy Testability
You can easily write tests by mocking Providers.
✅ No Prop Drilling
Widgets that need data read directly from Provider.
When Should You Use Provider?
| Situation | setState | Provider |
|---|---|---|
| Simple counter in single widget | ✅ | ❌ |
| Form validation | ✅ | ❌ |
| User info across multiple pages | ❌ | ✅ |
| Cart management | ❌ | ✅ |
| Theme/Language settings | ❌ | ✅ |
| Data from APIs | ❌ | ✅ |
| Complex form states | ❌ | ✅ |
General Rule: If state is used by multiple widgets or pages, consider Provider.
Why Provider?
| Feature | setState | Provider |
|---|---|---|
| Scope | Single widget | Entire widget tree |
| Complexity | Simple | Medium |
| Scalability | Hard | Easy |
| Testability | Hard | Easy |
| Prop drilling | Required | Not needed |
⚠️ Important Note: Provider is an external package and does not work in DartPad. To run the examples on your computer, follow the installation steps below.
Installation
Add to your pubspec.yaml file:
dependencies:
flutter:
sdk: flutter
provider: ^6.1.1Then run in terminal:
flutter pub getCore Concepts
1. ChangeNotifier
Class that holds state and notifies changes:
import 'package:flutter/foundation.dart';
class CounterProvider extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // Notify listeners
}
void decrement() {
_count--;
notifyListeners();
}
void reset() {
_count = 0;
notifyListeners();
}
}2. ChangeNotifierProvider
Adding Provider to widget tree:
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterProvider(),
child: MyApp(),
),
);
}3. Consumer
Listening to state and updating UI:
Consumer<CounterProvider>(
builder: (context, counter, child) {
return Text(
'${counter.count}',
style: TextStyle(fontSize: 48),
);
},
)4. context.read and context.watch
// Read (doesn't listen to changes) - Use in buttons
context.read<CounterProvider>().increment();
// Watch (listens to changes) - Use in build method
final count = context.watch<CounterProvider>().count;Full Example: Counter App
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// 1. State class
class CounterProvider extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
void decrement() {
_count--;
notifyListeners();
}
}
// 2. Main
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterProvider(),
child: MaterialApp(
home: CounterPage(),
),
),
);
}
// 3. UI
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Provider Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Counter Value:'),
Consumer<CounterProvider>(
builder: (context, counter, child) {
return Text(
'${counter.count}',
style: TextStyle(fontSize: 72, fontWeight: FontWeight.bold),
);
},
),
SizedBox(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FloatingActionButton(
onPressed: () => context.read<CounterProvider>().decrement(),
child: Icon(Icons.remove),
),
SizedBox(width: 16),
FloatingActionButton(
onPressed: () => context.read<CounterProvider>().increment(),
child: Icon(Icons.add),
),
],
),
],
),
),
);
}
}MultiProvider
Using multiple providers:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CounterProvider()),
ChangeNotifierProvider(create: (_) => ThemeProvider()),
ChangeNotifierProvider(create: (_) => UserProvider()),
],
child: MyApp(),
),
);
}Real World Example: Theme Switching
// Theme Provider
class ThemeProvider extends ChangeNotifier {
ThemeMode _themeMode = ThemeMode.light;
ThemeMode get themeMode => _themeMode;
bool get isDarkMode => _themeMode == ThemeMode.dark;
void toggleTheme() {
_themeMode = isDarkMode ? ThemeMode.light : ThemeMode.dark;
notifyListeners();
}
}
// Main
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => ThemeProvider(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return MaterialApp(
themeMode: themeProvider.themeMode,
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
home: HomePage(),
);
},
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final themeProvider = context.watch<ThemeProvider>();
return Scaffold(
appBar: AppBar(title: Text('Theme Settings')),
body: Center(
child: SwitchListTile(
title: Text('Dark Mode'),
value: themeProvider.isDarkMode,
onChanged: (_) => themeProvider.toggleTheme(),
),
),
);
}
}Real World Example: Shopping Cart
// Product model
class Product {
final String id;
final String name;
final double price;
Product({required this.id, required this.name, required this.price});
}
// Cart Provider
class CartProvider extends ChangeNotifier {
final List<Product> _items = [];
List<Product> get items => List.unmodifiable(_items);
int get itemCount => _items.length;
double get totalPrice => _items.fold(0, (sum, item) => sum + item.price);
void addItem(Product product) {
_items.add(product);
notifyListeners();
}
void removeItem(String productId) {
_items.removeWhere((item) => item.id == productId);
notifyListeners();
}
void clearCart() {
_items.clear();
notifyListeners();
}
}
// Usage
class ProductCard extends StatelessWidget {
final Product product;
ProductCard({required this.product});
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
title: Text(product.name),
subtitle: Text('\$${product.price}'),
trailing: IconButton(
icon: Icon(Icons.add_shopping_cart),
onPressed: () {
context.read<CartProvider>().addItem(product);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${product.name} added to cart')),
);
},
),
),
);
}
}
// Cart icon (in AppBar)
class CartIcon extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Stack(
children: [
IconButton(
icon: Icon(Icons.shopping_cart),
onPressed: () {
// Navigate to cart page
},
),
Positioned(
right: 0,
top: 0,
child: Consumer<CartProvider>(
builder: (context, cart, child) {
return cart.itemCount > 0
? CircleAvatar(
radius: 10,
backgroundColor: Colors.red,
child: Text(
'${cart.itemCount}',
style: TextStyle(fontSize: 12, color: Colors.white),
),
)
: SizedBox.shrink();
},
),
),
],
);
}
}Optimization with Selector
Listen to specific changes only:
// Listens to all changes (inefficient)
Consumer<CartProvider>(
builder: (context, cart, child) {
return Text('${cart.itemCount} items');
},
)
// Rebuilds only when itemCount changes (efficient)
Selector<CartProvider, int>(
selector: (context, cart) => cart.itemCount,
builder: (context, itemCount, child) {
return Text('$itemCount items');
},
)Provider Types
| Type | Usage |
|---|---|
Provider |
Immutable values |
ChangeNotifierProvider |
Mutable state (most common) |
FutureProvider |
Async data |
StreamProvider |
Stream data |
ProxyProvider |
Dependent providers |
FutureProvider Example
final userProvider = FutureProvider<User>((ref) async {
final response = await http.get(Uri.parse('https://api.example.com/user'));
return User.fromJson(jsonDecode(response.body));
});
// Usage
FutureProvider<List<Product>>(
create: (_) => fetchProducts(),
initialData: [],
child: ProductList(),
)Best Practices
1. Define Provider as high as possible
// ✅ Correct - In main
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => MyProvider(),
child: MyApp(),
),
);
}
// ❌ Wrong - Deep in tree
class SomePage extends StatelessWidget {
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MyProvider(), // New instance on every build!
child: ...,
);
}
}2. context.read vs context.watch
// ✅ Use watch in build method
Widget build(BuildContext context) {
final count = context.watch<CounterProvider>().count;
return Text('$count');
}
// ✅ Use read in event handlers
onPressed: () {
context.read<CounterProvider>().increment();
}
// ❌ Don't use read in build method
Widget build(BuildContext context) {
final count = context.read<CounterProvider>().count; // Won't update
return Text('$count');
}3. Keep Consumer as narrow as possible
// ✅ Correct - Only necessary part rebuilds
Scaffold(
appBar: AppBar(title: Text('Page')),
body: Consumer<CounterProvider>(
builder: (context, counter, child) {
return Text('${counter.count}');
},
),
)
// ❌ Wrong - Entire page rebuilds
Consumer<CounterProvider>(
builder: (context, counter, child) {
return Scaffold(
appBar: AppBar(title: Text('Page')),
body: Text('${counter.count}'),
);
},
)Summary
- Provider: Recommended state management solution for Flutter
- ChangeNotifier: Class that holds and notifies state
- notifyListeners(): Called to update UI
- Consumer: Listens to state changes
- context.watch: Use in build (listens)
- context.read: Use in events (doesn't listen)
- MultiProvider: Multiple providers
- Selector: Performance optimization
Provider is a powerful and flexible state management solution that can be used in Flutter projects of all sizes.