In Flutter app development, as applications grow in size, state sharing and communication between widgets become increasingly complex. When multiple widgets need to access and modify the same state, the traditional approach of passing parameters through constructors leads to redundant code, high coupling, and poor maintainability. This lesson introduces Provider, a popular state management solution in Flutter that builds on InheritedWidget to elegantly solve cross-widget state sharing problems simply and efficiently.
I. Why We Need State Management
In Flutter, all UI is composed of Widgets, which are immutable, while state represents the mutable data within Widgets. For simple applications, we can manage a single widget's state with setState(), but when state needs to be shared across multiple widgets, we face several challenges:
- Difficult cross-widget communication: Deeply nested child widgets needing access to parent state must receive parameters through multiple levels, creating a "prop drilling" problem.
- Complex state synchronization: Manually synchronizing state when multiple widgets depend on the same state leads to messy code logic.
- Poor code maintainability: State scattered across various widgets makes modifications and debugging difficult.
- Performance issues: Inefficient state management causes unnecessary widget rebuilds, affecting app performance.
For example, shopping cart state in an e-commerce app might need access and modification in product lists, cart pages, checkout pages, and more. With traditional methods, cart data would need to be passed between pages, and all related pages manually notified of changes—an inefficient and error-prone approach.
State management solutions address these issues, with Provider being one of the simplest, most user-friendly, and officially recommended options.
II. Provider Basic Concepts and Core Classes
Provider is a state management library built on Flutter's native InheritedWidget. Its core idea involves extracting shared state into an independent class, providing this state in the widget tree through a Provider widget, and finally consuming this state in child widgets that need it.
1. Core Classes
- ChangeNotifier: A class implementing the observer pattern that notifies all listeners of updates by calling notifyListeners() when state changes.
- ChangeNotifierProvider: A widget that provides a ChangeNotifier instance in the widget tree, making it accessible to widgets in its subtree.
- Consumer: Used to retrieve and listen to a ChangeNotifier instance in child widgets, rebuilding itself when state changes.
- Provider.of: Another method to retrieve ChangeNotifier instances, with an option to listen for state changes.
2. Installing Provider
Before using Provider, add the dependency to pubspec.yaml:
dependencies:
flutter:
sdk: flutter
provider: ^6.1.5 # Use the latest version
Run flutter pub get to install the dependency.
III. Basic Provider Workflow
Using Provider for state management typically follows these steps:
- Create a state class inheriting from ChangeNotifier, encapsulating shared state and methods to modify it.
- Use ChangeNotifierProvider to provide the state instance at an appropriate location in the widget tree.
- Retrieve and use the state in child widgets using Consumer or Provider.of.
Let's demonstrate basic usage with a simple counter example:
1. Create the State Class
import 'package:flutter/foundation.dart';
// Inherit from ChangeNotifier
class Counter with ChangeNotifier {
int _count = 0;
// Provide method to access state
int get count => _count;
// Provide methods to modify state
void increment() {
_count++;
// Notify all listeners
notifyListeners();
}
void decrement() {
_count--;
notifyListeners();
}
void reset() {
_count = 0;
notifyListeners();
}
}
2. Provide the State
Use ChangeNotifierProvider to provide the state in the widget tree, typically at the app's root:
import 'package:provider/provider.dart';
void main() {
runApp(
// Provide Counter instance
ChangeNotifierProvider(
create: (context) => Counter(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Provider Demo',
home: const CounterPage(),
);
}
}
3. Consume the State
Retrieve and use the state in child widgets with Consumer or Provider.of:
Using Consumer
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter Demo')),
body: Center(
// Use Consumer to get Counter instance
child: Consumer<Counter>(
builder: (context, counter, child) {
// Builder method rebuilds when state changes
return Text(
'Current count: ${counter.count}',
style: const TextStyle(fontSize: 24),
);
},
),
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () {
// Use Provider.of to get instance (without listening)
Provider.of<Counter>(context, listen: false).decrement();
},
child: const Icon(Icons.remove),
),
const SizedBox(width: 10),
FloatingActionButton(
onPressed: () {
Provider.of<Counter>(context, listen: false).increment();
},
child: const Icon(Icons.add),
),
],
),
);
}
}
Consumer Performance Optimization
The child parameter in Consumer optimizes performance by avoiding unnecessary rebuilds:
Consumer<Counter>(
builder: (context, counter, child) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// This Text rebuilds when state changes
Text('Count: ${counter.count}'),
// This child doesn't rebuild as it's extracted to the child parameter
child!,
],
);
},
// This child builds only once
child: const Text('This is static text'),
)
Using Provider.of
Provider.of(context) retrieves the nearest Provider of type T and rebuilds the current widget when state changes:
// Listen for state changes, rebuilds when state changes
final counter = Provider.of<Counter>(context);
// Don't listen for state changes, typically for state modification
final counter = Provider.of<Counter>(context, listen: false);
Example usage:
class CounterDisplay extends StatelessWidget {
const CounterDisplay({super.key});
@override
Widget build(BuildContext context) {
// Get Counter instance and listen for changes
final counter = Provider.of<Counter>(context);
return Text(
'Count: ${counter.count}',
style: const TextStyle(fontSize: 24),
);
}
}
class CounterActions extends StatelessWidget {
const CounterActions({super.key});
@override
Widget build(BuildContext context) {
return FloatingActionButton(
onPressed: () {
// Get Counter instance without listening
Provider.of<Counter>(context, listen: false).increment();
},
child: const Icon(Icons.add),
);
}
}
IV. Managing Multiple States
When an application has multiple independent states to manage, use MultiProvider to organize them:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => Counter()),
ChangeNotifierProvider(create: (context) => ThemeProvider()),
ChangeNotifierProvider(create: (context) => UserProvider()),
],
child: const MyApp(),
),
);
}
Then retrieve different states in child widgets:
// Get counter state
final counter = Provider.of<Counter>(context);
// Get theme state
final themeProvider = Provider.of<ThemeProvider>(context);
V. Example: Managing Shopping Cart State with Provider
Let's implement a complete shopping cart example to demonstrate managing complex state with Provider:
1. Define Data Models
// Product model
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,
});
}
// Cart item model
class CartItem {
final String id;
final String productId;
final String name;
final int quantity;
final double price;
CartItem({
required this.id,
required this.productId,
required this.name,
required this.quantity,
required this.price,
});
// Calculate subtotal
double get totalPrice => price * quantity;
}
2. Create Shopping Cart State Management Class
import 'package:flutter/foundation.dart';
import 'dart:collection';
class Cart with ChangeNotifier {
// Store cart items with product IDs as keys
final Map<String, CartItem> _items = {};
// Provide unmodifiable view of cart items
UnmodifiableMapView<String, CartItem> get items => UnmodifiableMapView(_items);
// Get total number of items in cart
int get itemCount => _items.length;
// Calculate total cart amount
double get totalAmount {
var total = 0.0;
_items.forEach((key, cartItem) {
total += cartItem.price * cartItem.quantity;
});
return total;
}
// Add product to cart
void addItem(Product product) {
if (_items.containsKey(product.id)) {
// If product exists, increase quantity
_items.update(
product.id,
(existingItem) => CartItem(
id: existingItem.id,
productId: existingItem.productId,
name: existingItem.name,
quantity: existingItem.quantity + 1,
price: existingItem.price,
),
);
} else {
// If new product, add to cart
_items.putIfAbsent(
product.id,
() => CartItem(
id: DateTime.now().toString(),
productId: product.id,
name: product.name,
quantity: 1,
price: product.price,
),
);
}
notifyListeners();
}
// Remove product from cart
void removeItem(String productId) {
_items.remove(productId);
notifyListeners();
}
// Decrease product quantity in cart
void removeSingleItem(String productId) {
if (!_items.containsKey(productId)) {
return;
}
if (_items[productId]!.quantity > 1) {
_items.update(
productId,
(existingItem) => CartItem(
id: existingItem.id,
productId: existingItem.productId,
name: existingItem.name,
quantity: existingItem.quantity - 1,
price: existingItem.price,
),
);
} else {
_items.remove(productId);
}
notifyListeners();
}
// Clear cart
void clear() {
_items.clear();
notifyListeners();
}
}
3. Create Product List State Class
class Products with ChangeNotifier {
final List<Product> _items = [
Product(
id: 'p1',
name: 'Red Shirt',
price: 29.99,
imageUrl: 'https://picsum.photos/200/300?random=1',
),
Product(
id: 'p2',
name: 'Trousers',
price: 59.99,
imageUrl: 'https://picsum.photos/200/300?random=2',
),
Product(
id: 'p3',
name: 'Yellow Scarf',
price: 19.99,
imageUrl: 'https://picsum.photos/200/300?random=3',
),
Product(
id: 'p4',
name: 'A Shoes',
price: 99.99,
imageUrl: 'https://picsum.photos/200/300?random=4',
),
];
// Get product list
List<Product> get items => [..._items];
}
4. Provide States in Application
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (ctx) => Products()),
ChangeNotifierProvider(create: (ctx) => Cart()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Shopping Cart Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const ProductsOverviewScreen(),
routes: {
CartScreen.routeName: (ctx) => const CartScreen(),
},
);
}
}
5. Implement Product List Page
class ProductsOverviewScreen extends StatelessWidget {
const ProductsOverviewScreen({super.key});
@override
Widget build(BuildContext context) {
// Get product list
final productsData = Provider.of<Products>(context);
final products = productsData.items;
return Scaffold(
appBar: AppBar(
title: const Text('My Shop'),
actions: [
// Cart icon with item count badge
Consumer<Cart>(
builder: (ctx, cart, ch) => Badge(
label: Text(cart.itemCount.toString()),
child: ch!,
),
child: IconButton(
icon: const Icon(Icons.shopping_cart),
onPressed: () {
Navigator.of(context).pushNamed(CartScreen.routeName);
},
),
),
],
),
body: GridView.builder(
padding: const EdgeInsets.all(10.0),
itemCount: products.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 3 / 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemBuilder: (ctx, i) => ProductItem(products[i]),
),
);
}
}
class ProductItem extends StatelessWidget {
final Product product;
const ProductItem(this.product, {super.key});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(10),
child: GridTile(
footer: GridTileBar(
backgroundColor: Colors.black87,
title: Text(
product.name,
textAlign: TextAlign.center,
),
trailing: IconButton(
icon: const Icon(Icons.shopping_cart),
onPressed: () {
// Add product to cart
Provider.of<Cart>(context, listen: false).addItem(product);
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Added item to cart!'),
duration: const Duration(seconds: 2),
action: SnackBarAction(
label: 'UNDO',
onPressed: () {
Provider.of<Cart>(context, listen: false)
.removeSingleItem(product.id);
},
),
),
);
},
color: Theme.of(context).colorScheme.secondary,
),
),
child: Image.network(
product.imageUrl,
fit: BoxFit.cover,
),
),
);
}
}
6. Implement Cart Page
class CartScreen extends StatelessWidget {
static const routeName = '/cart';
const CartScreen({super.key});
@override
Widget build(BuildContext context) {
final cart = Provider.of<Cart>(context);
return Scaffold(
appBar: AppBar(
title: const Text('Your Cart'),
),
body: Column(
children: [
Card(
margin: const EdgeInsets.all(15),
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Total', style: TextStyle(fontSize: 20)),
const Spacer(),
Chip(
label: Text(
'\$${cart.totalAmount.toStringAsFixed(2)}',
style: TextStyle(
color: Theme.of(context).primaryTextTheme. titleLarge?.color,
),
),
backgroundColor: Theme.of(context).primaryColor,
),
TextButton(
onPressed: () {
// Add checkout logic here
},
child: const Text('ORDER NOW'),
)
],
),
),
),
const SizedBox(height: 10),
Expanded(
child: ListView.builder(
itemCount: cart.items.length,
itemBuilder: (ctx, i) {
final cartItem = cart.items.values.toList()[i];
return ListTile(
leading: CircleAvatar(
child: FittedBox(
child: Text('\$${cartItem.price}'),
),
),
title: Text(cartItem.name),
subtitle: Text('Total: \$${(cartItem.price * cartItem.quantity).toStringAsFixed(2)}'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: () {
cart.removeSingleItem(cartItem.productId);
},
),
Text('${cartItem.quantity}'),
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
// Add logic to increase quantity here
// Not implemented in this simplified example
},
),
],
),
);
},
),
)
],
),
);
}
}
VI. Advanced Provider Usage
1. Selector for Optimized Rebuilds
Selector is an advanced version of Consumer that determines whether to rebuild based on specified conditions, further optimizing performance:
Selector<Counter, int>(
// Select the state to listen to
selector: (context, counter) => counter.count,
// Only rebuild when selected state changes
builder: (context, count, child) {
return Text('Count: $count');
},
)
Selector takes two generic parameters: the first is the ChangeNotifier type, and the second is the type of state to monitor. The builder is only called when the value returned by the selector method changes.
2. State Persistence
Combining with local storage libraries like shared_preferences or hive enables state persistence:
class Counter with ChangeNotifier {
int _count = 0;
final SharedPreferences _prefs;
Counter(this._prefs) {
// Load state from local storage
_count = _prefs.getInt('count') ?? 0;
}
int get count => _count;
void increment() {
_count++;
// Save state to local storage
_prefs.setInt('count', _count);
notifyListeners();
}
}
// Provide state with persistence
ChangeNotifierProvider(
create: (context) => Counter(SharedPreferences.getInstance()),
child: MyApp(),
)
3. State Encapsulation and Business Logic Separation
For complex applications, separate business logic from state management to keep ChangeNotifier clean:
// Business logic layer
class CartService {
Future<void> addToCart(Product product) async {
// Handle business logic for adding to cart, such as network requests
await Future.delayed(const Duration(milliseconds: 300));
}
}
// State management layer
class Cart with ChangeNotifier {
final CartService _cartService;
final List<CartItem> _items = [];
Cart(this._cartService);
List<CartItem> get items => [..._items];
Future<void> addItem(Product product) async {
try {
// Call business logic
await _cartService.addToCart(product);
// Update state
_items.add(CartItem(...));
notifyListeners();
} catch (e) {
// Handle errors
rethrow;
}
}
}
VII. Provider Best Practices
- Appropriate state granularity: Avoid creating overly large state classes; split state by functional modules.
- Minimal rebuild principle: Use Consumer and Selector to limit rebuild scope and avoid unnecessary rebuilds.
- Single responsibility: Each state class should manage only a related set of states, following the single responsibility principle.
- Immutable data: For complex data structures in state classes, use immutable objects and update state by replacing entire objects.
- Avoid state creation in build methods: Ensure ChangeNotifier instances are created in the create method, not in build methods.
- Clean up resources: If your state class uses resources that need manual release (like timers), clean them up in the dispose method:
class TimerModel with ChangeNotifier {
late Timer _timer;
int _seconds = 0;
TimerModel() {
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
_seconds++;
notifyListeners();
});
}
int get seconds => _seconds;
@override
void dispose() {
// Clean up resources
_timer.cancel();
super.dispose();
}
}
Top comments (0)