State management in Flutter can feel overwhelming, especially when you're starting out. You've probably heard about Provider, BLoC, Riverpod, and GetX, but Provider remains one of the most popular choices for good reason. If you're still confused about how Provider works, you're not alone!
In this comprehensive guide, I'll break down Flutter Provider from the ground up, show you practical examples, and help you understand when and why to use it. By the end of this post, you'll have a solid understanding of Provider and be ready to use it in your own projects.
What is Flutter Provider?
Provider is a state management library that acts as a wrapper around InheritedWidget. It allows you to share data across your widget tree without passing it down through constructors. Think of it as a way to make data available globally in your app while keeping it reactive and efficient.
Key Benefits:
- Simple to understand and implement
- Minimal boilerplate code
- Excellent performance with automatic optimizations
- Great for beginners and complex apps alike
- Officially recommended by the Flutter team
Setting Up Provider
First, add Provider to your pubspec.yaml
:
dependencies:
flutter:
sdk: flutter
provider: ^6.1.5
Run flutter pub get
to install the package.
Core Concepts You Need to Know
1. ChangeNotifier
ChangeNotifier
is a class that provides change notification to its listeners. When you want to notify widgets that something has changed, you call notifyListeners()
.
import 'package:flutter/foundation.dart';
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // This tells listeners that something changed
}
void decrement() {
_count--;
notifyListeners();
}
void reset() {
_count = 0;
notifyListeners();
}
}
2. ChangeNotifierProvider
This widget provides a ChangeNotifier
to its descendants. It automatically handles the lifecycle of your model.
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: MyApp(),
)
3. Consumer
Consumer
is a widget that listens to changes in your model and rebuilds when notifyListeners()
is called.
Consumer<CounterModel>(
builder: (context, counter, child) {
return Text('Count: ${counter.count}');
},
)
4. Provider.of
An alternative way to access your model data. Use listen: false
when you don't want to rebuild the widget.
// This will rebuild when the model changes
final counter = Provider.of<CounterModel>(context);
// This won't rebuild when the model changes
final counter = Provider.of<CounterModel>(context, listen: false);
Let's Build a Complete Example
Let's create a simple shopping cart app to see Provider in action:
Step 1: Create the Models
// models/product.dart
class Product {
final String id;
final String name;
final double price;
final String imageUrl;
Product({
required this.id,
required this.name,
required this.price,
required this.imageUrl,
});
}
// models/cart_item.dart
class CartItem {
final Product product;
int quantity;
CartItem({
required this.product,
this.quantity = 1,
});
double get totalPrice => product.price * quantity;
}
Step 2: Create the Cart Provider
// providers/cart_provider.dart
import 'package:flutter/foundation.dart';
import '../models/product.dart';
import '../models/cart_item.dart';
class CartProvider extends ChangeNotifier {
final List<CartItem> _items = [];
List<CartItem> get items => List.unmodifiable(_items);
int get itemCount => _items.length;
double get totalAmount {
return _items.fold(0.0, (sum, item) => sum + item.totalPrice);
}
void addItem(Product product) {
// Check if item already exists
final existingIndex = _items.indexWhere(
(item) => item.product.id == product.id,
);
if (existingIndex >= 0) {
// Increase quantity
_items[existingIndex].quantity++;
} else {
// Add new item
_items.add(CartItem(product: product));
}
notifyListeners();
}
void removeItem(String productId) {
_items.removeWhere((item) => item.product.id == productId);
notifyListeners();
}
void updateQuantity(String productId, int quantity) {
final index = _items.indexWhere(
(item) => item.product.id == productId,
);
if (index >= 0) {
if (quantity <= 0) {
_items.removeAt(index);
} else {
_items[index].quantity = quantity;
}
notifyListeners();
}
}
void clearCart() {
_items.clear();
notifyListeners();
}
}
Step 3: Setup the Provider in main.dart
// main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/cart_provider.dart';
import 'screens/product_list_screen.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => CartProvider(),
child: MaterialApp(
title: 'Shopping Cart Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ProductListScreen(),
),
);
}
}
Step 4: Create the Product List Screen
// screens/product_list_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/cart_provider.dart';
import '../models/product.dart';
import 'cart_screen.dart';
class ProductListScreen extends StatelessWidget {
final List<Product> products = [
Product(id: '1', name: 'Laptop', price: 999.99, imageUrl: 'laptop.jpg'),
Product(id: '2', name: 'Phone', price: 699.99, imageUrl: 'phone.jpg'),
Product(id: '3', name: 'Headphones', price: 199.99, imageUrl: 'headphones.jpg'),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Products'),
actions: [
Consumer<CartProvider>(
builder: (context, cart, child) {
return Stack(
children: [
IconButton(
icon: Icon(Icons.shopping_cart),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => CartScreen()),
);
},
),
if (cart.itemCount > 0)
Positioned(
right: 8,
top: 8,
child: Container(
padding: EdgeInsets.all(2),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10),
),
constraints: BoxConstraints(
minWidth: 16,
minHeight: 16,
),
child: Text(
'${cart.itemCount}',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
textAlign: TextAlign.center,
),
),
),
],
);
},
),
],
),
body: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ProductCard(product: product);
},
),
);
}
}
class ProductCard extends StatelessWidget {
final Product product;
const ProductCard({Key? key, required this.product}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.all(8),
child: ListTile(
leading: CircleAvatar(
child: Text(product.name[0]),
),
title: Text(product.name),
subtitle: Text('\$${product.price.toStringAsFixed(2)}'),
trailing: Consumer<CartProvider>(
builder: (context, cart, child) {
return ElevatedButton(
onPressed: () {
cart.addItem(product);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${product.name} added to cart!'),
duration: Duration(seconds: 1),
),
);
},
child: Text('Add to Cart'),
);
},
),
),
);
}
}
Step 5: Create the Cart Screen
// screens/cart_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/cart_provider.dart';
class CartScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Shopping Cart'),
),
body: Consumer<CartProvider>(
builder: (context, cart, child) {
if (cart.items.isEmpty) {
return Center(
child: Text(
'Your cart is empty',
style: TextStyle(fontSize: 18),
),
);
}
return Column(
children: [
Expanded(
child: ListView.builder(
itemCount: cart.items.length,
itemBuilder: (context, index) {
final item = cart.items[index];
return Card(
margin: EdgeInsets.all(8),
child: ListTile(
leading: CircleAvatar(
child: Text(item.product.name[0]),
),
title: Text(item.product.name),
subtitle: Text(
'\$${item.product.price.toStringAsFixed(2)} x ${item.quantity}',
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.remove),
onPressed: () {
cart.updateQuantity(
item.product.id,
item.quantity - 1,
);
},
),
Text('${item.quantity}'),
IconButton(
icon: Icon(Icons.add),
onPressed: () {
cart.updateQuantity(
item.product.id,
item.quantity + 1,
);
},
),
IconButton(
icon: Icon(Icons.delete),
onPressed: () {
cart.removeItem(item.product.id);
},
),
],
),
),
);
},
),
),
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border(top: BorderSide(color: Colors.grey)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total: \$${cart.totalAmount.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
ElevatedButton(
onPressed: cart.items.isEmpty
? null
: () {
// Implement checkout logic
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Checkout'),
content: Text('Order placed successfully!'),
actions: [
TextButton(
onPressed: () {
cart.clearCart();
Navigator.pop(context);
},
child: Text('OK'),
),
],
),
);
},
child: Text('Checkout'),
),
],
),
),
],
);
},
),
);
}
}
Advanced Provider Patterns
1. MultiProvider
When you need multiple providers, use MultiProvider
:
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => CartProvider()),
ChangeNotifierProvider(create: (context) => UserProvider()),
ChangeNotifierProvider(create: (context) => ThemeProvider()),
],
child: MyApp(),
)
2. Selector Widget
Use Selector
when you only want to rebuild for specific changes:
Selector<CartProvider, int>(
selector: (context, cart) => cart.itemCount,
builder: (context, itemCount, child) {
return Text('Items: $itemCount');
},
)
3. ProxyProvider
Use ProxyProvider
when one provider depends on another:
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => AuthProvider()),
ChangeNotifierProxyProvider<AuthProvider, CartProvider>(
create: (context) => CartProvider(),
update: (context, auth, cart) => cart..updateAuth(auth),
),
],
child: MyApp(),
)
Common Mistakes to Avoid
1. Forgetting to Call notifyListeners()
// ❌ Wrong
void increment() {
_count++;
// Missing notifyListeners()
}
// âś… Correct
void increment() {
_count++;
notifyListeners();
}
2. Using Provider.of with listen: true in Event Handlers
// ❌ Wrong - This can cause issues
ElevatedButton(
onPressed: () {
Provider.of<CartProvider>(context).addItem(product);
},
child: Text('Add'),
)
// âś… Correct
ElevatedButton(
onPressed: () {
Provider.of<CartProvider>(context, listen: false).addItem(product);
},
child: Text('Add'),
)
3. Not Disposing Resources
class MyProvider extends ChangeNotifier {
StreamSubscription? _subscription;
MyProvider() {
_subscription = someStream.listen((data) {
// Handle data
notifyListeners();
});
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
}
When to Use Provider
Use Provider when:
- You need simple state management
- You're building small to medium-sized apps
- You want minimal boilerplate
- You're new to Flutter state management
Consider alternatives when:
- You have complex business logic (consider BLoC)
- You need time-travel debugging
- You're building very large applications
- You need more advanced features like middleware
Performance Tips
- Use Consumer strategically - Only wrap widgets that actually need to rebuild
- Use Selector for granular updates
- Keep your models focused - Don't put everything in one provider
- Use const constructors where possible
- Consider using Consumer with child parameter for widgets that don't change
Consumer<CartProvider>(
builder: (context, cart, child) {
return Column(
children: [
Text('Total: ${cart.totalAmount}'),
child!, // This widget won't rebuild
],
);
},
child: ExpensiveWidget(), // Define the static child here
)
Conclusion
Provider is an excellent choice for Flutter state management because it's simple, performant, and officially supported. It strikes a perfect balance between ease of use and power, making it ideal for most Flutter applications.
The key concepts to remember:
-
ChangeNotifier
for your data models -
ChangeNotifierProvider
to provide data to your widget tree -
Consumer
to listen to changes and rebuild widgets - Always call
notifyListeners()
when your data changes - Use
listen: false
when you don't need to rebuild
Start with Provider for your next Flutter project, and you'll find that state management doesn't have to be complicated. As your app grows, you can always migrate to more complex solutions if needed.
Have you tried Provider in your Flutter projects? What challenges🏆 did you face? Share your experiences in the comments below! 💬
Top comments (0)