Provider: Read, Watch & Select
Understanding Provider Access Methods
Provider offers three distinct ways to access state from your widgets: context.watch, context.read, and context.select. Each method serves a different purpose and has unique performance characteristics. Choosing the right method is essential for building efficient, responsive Flutter applications.
context.watch — Rebuild on Every Change
The context.watch<T>() method subscribes your widget to a provider and triggers a rebuild every time the provider’s value changes. This is the most commonly used access method and is ideal when your widget needs to display data that updates frequently.
Basic context.watch Usage
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
class CounterDisplay extends StatelessWidget {
const CounterDisplay({super.key});
@override
Widget build(BuildContext context) {
// Rebuilds every time CounterModel calls notifyListeners()
final counter = context.watch<CounterModel>();
return Text(
'Count: \${counter.count}',
style: const TextStyle(fontSize: 24),
);
}
}
When you call context.watch<CounterModel>(), the widget registers itself as a listener. Every time notifyListeners() is called on the CounterModel, the widget’s build method runs again, ensuring the UI stays in sync with the latest state.
Provider.of<T>(context) as an equivalent to context.watch<T>(). Both subscribe to changes and trigger rebuilds. The context.watch syntax is shorter and more modern.
context.read — One-Time Access, No Rebuild
The context.read<T>() method retrieves the provider’s current value without subscribing to changes. The widget will not rebuild when the provider’s value changes. This is perfect for calling methods on a provider (like triggering actions) without needing to observe its state.
Using context.read for Actions
class CounterActions extends StatelessWidget {
const CounterActions({super.key});
@override
Widget build(BuildContext context) {
// No watch needed here — we only call methods
return ElevatedButton(
onPressed: () {
// Read the provider to call a method
context.read<CounterModel>().increment();
},
child: const Text('Increment'),
);
}
}
In this example, the button only needs to trigger an action. It does not display any data from the provider, so there is no reason to subscribe to changes. Using context.read prevents unnecessary rebuilds of the button widget.
context.read inside the build method to display data. If you read a value without watching it, your widget will show stale data because it will not rebuild when the value changes. Use context.watch or context.select for any value you display in the UI.
context.select — Rebuild on Specific Field Changes
The context.select<T, R>() method is the most powerful optimization tool in Provider. It lets you select a specific piece of a provider’s state, and the widget only rebuilds when that particular piece changes. This is crucial for complex models with many fields.
Selective Rebuilds with context.select
class UserModel extends ChangeNotifier {
String _name = 'Edrees';
String _email = 'edrees@example.com';
int _loginCount = 0;
String get name => _name;
String get email => _email;
int get loginCount => _loginCount;
void updateName(String newName) {
_name = newName;
notifyListeners();
}
void updateEmail(String newEmail) {
_email = newEmail;
notifyListeners();
}
void recordLogin() {
_loginCount++;
notifyListeners();
}
}
class UserNameDisplay extends StatelessWidget {
const UserNameDisplay({super.key});
@override
Widget build(BuildContext context) {
// Only rebuilds when the name changes
// Does NOT rebuild when email or loginCount changes
final name = context.select<UserModel, String>(
(user) => user.name,
);
return Text('Hello, \$name');
}
}
class UserEmailDisplay extends StatelessWidget {
const UserEmailDisplay({super.key});
@override
Widget build(BuildContext context) {
// Only rebuilds when the email changes
final email = context.select<UserModel, String>(
(user) => user.email,
);
return Text('Email: \$email');
}
}
In the example above, UserNameDisplay only rebuilds when the name property changes. If recordLogin() is called and increments loginCount, UserNameDisplay is not rebuilt because the selected value (name) did not change. Provider uses the == operator to compare the previous and new selected values.
The Selector Widget
In addition to context.select, Provider offers a Selector widget that provides the same selective rebuild behavior in a widget-based syntax. This can be useful when you want more explicit control or when working with complex selection logic.
Using the Selector Widget
class OptimizedUserCard extends StatelessWidget {
const OptimizedUserCard({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// Selector widget — only rebuilds when name changes
Selector<UserModel, String>(
selector: (context, user) => user.name,
builder: (context, name, child) {
return Text(
'Name: \$name',
style: const TextStyle(fontSize: 20),
);
},
),
// Selector with a child that never rebuilds
Selector<UserModel, int>(
selector: (context, user) => user.loginCount,
builder: (context, count, child) {
return Row(
children: [
child!, // Static icon — never rebuilds
Text('Logins: \$count'),
],
);
},
child: const Icon(Icons.login), // Cached child
),
],
);
}
}
Selector widget’s child parameter works like Consumer’s child — it is built once and reused across rebuilds. Use it for static parts of the subtree that do not depend on the selected value.
When to Use Each Method
Choosing the right access method is critical for both correctness and performance. Here is a clear guide:
Decision Guide
// 1. context.watch — Display data that changes
// Use when: Your widget SHOWS provider data
// Rebuilds: On EVERY notifyListeners() call
Widget build(BuildContext context) {
final model = context.watch<MyModel>(); // Subscribes
return Text(model.someValue); // Displays data
}
// 2. context.read — Trigger actions
// Use when: Your widget CALLS methods on a provider
// Rebuilds: NEVER (no subscription)
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => context.read<MyModel>().doSomething(),
child: const Text('Action'), // Static label
);
}
// 3. context.select — Display specific data
// Use when: Your widget shows PART of a provider's data
// Rebuilds: ONLY when the selected value changes
Widget build(BuildContext context) {
final title = context.select<MyModel, String>(
(m) => m.title,
);
return Text(title); // Only this field
}
Performance Optimization with Select
The biggest performance gains come from using context.select in list items and frequently updating widgets. Consider a scenario where a list of products is displayed, but only the price updates in real time:
Optimized List with select
class ProductCatalog extends ChangeNotifier {
final List<Product> _products = [];
List<Product> get products => List.unmodifiable(_products);
void updatePrice(int index, double newPrice) {
_products[index] = _products[index].copyWith(price: newPrice);
notifyListeners();
}
}
class ProductListView extends StatelessWidget {
const ProductListView({super.key});
@override
Widget build(BuildContext context) {
// Watch the entire list length for additions/removals
final productCount = context.select<ProductCatalog, int>(
(catalog) => catalog.products.length,
);
return ListView.builder(
itemCount: productCount,
itemBuilder: (context, index) {
return ProductTile(index: index);
},
);
}
}
class ProductTile extends StatelessWidget {
final int index;
const ProductTile({super.key, required this.index});
@override
Widget build(BuildContext context) {
// Each tile only rebuilds when ITS product changes
final product = context.select<ProductCatalog, Product>(
(catalog) => catalog.products[index],
);
return ListTile(
title: Text(product.name),
subtitle: Text('\$\${product.price.toStringAsFixed(2)}'),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => context.read<ProductCatalog>().updatePrice(
index, product.price + 1.0,
),
),
);
}
}
Common Mistakes
Understanding what not to do is just as important as knowing the correct patterns. Here are the most frequent mistakes developers make with Provider access methods:
Mistake 1: Using watch in Callbacks
Wrong vs Right: watch in Callbacks
// WRONG — context.watch inside a callback
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
// This will throw an error at runtime!
final model = context.watch<MyModel>();
model.doSomething();
},
child: const Text('Click'),
);
}
// CORRECT — use context.read in callbacks
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
// read is designed for one-time access
final model = context.read<MyModel>();
model.doSomething();
},
child: const Text('Click'),
);
}
Mistake 2: Using read in build for Display
Wrong vs Right: read in build
// WRONG — widget will show stale data
Widget build(BuildContext context) {
final counter = context.read<CounterModel>();
return Text('Count: \${counter.count}'); // Never updates!
}
// CORRECT — use watch to stay in sync
Widget build(BuildContext context) {
final counter = context.watch<CounterModel>();
return Text('Count: \${counter.count}'); // Updates on change
}
// BEST — use select if you only need one field
Widget build(BuildContext context) {
final count = context.select<CounterModel, int>(
(c) => c.count,
);
return Text('Count: \$count'); // Updates only when count changes
}
context.watch inside initState, dispose, or any asynchronous callback (like Future.then or Timer callbacks) will throw an error. Always use context.read outside of the build method.
Practical Example: Optimized Shopping Cart
Let’s build a complete example that demonstrates all three access methods working together in a shopping cart scenario:
Complete Shopping Cart Example
class CartModel extends ChangeNotifier {
final List<CartItem> _items = [];
double _taxRate = 0.15;
List<CartItem> get items => List.unmodifiable(_items);
int get itemCount => _items.length;
double get taxRate => _taxRate;
double get subtotal =>
_items.fold(0, (sum, item) => sum + item.price * item.quantity);
double get tax => subtotal * _taxRate;
double get total => subtotal + tax;
void addItem(CartItem item) {
_items.add(item);
notifyListeners();
}
void removeItem(int index) {
_items.removeAt(index);
notifyListeners();
}
void updateQuantity(int index, int quantity) {
_items[index] = _items[index].copyWith(quantity: quantity);
notifyListeners();
}
}
// Badge only cares about item count
class CartBadge extends StatelessWidget {
const CartBadge({super.key});
@override
Widget build(BuildContext context) {
// select: only rebuilds when itemCount changes
final count = context.select<CartModel, int>(
(cart) => cart.itemCount,
);
return Badge(
label: Text('\$count'),
child: const Icon(Icons.shopping_cart),
);
}
}
// Total display only cares about the total
class CartTotalDisplay extends StatelessWidget {
const CartTotalDisplay({super.key});
@override
Widget build(BuildContext context) {
// select: only rebuilds when total changes
final total = context.select<CartModel, double>(
(cart) => cart.total,
);
return Text(
'Total: \$\${total.toStringAsFixed(2)}',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
);
}
}
// Checkout button only needs to trigger actions
class CheckoutButton extends StatelessWidget {
const CheckoutButton({super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
// read: one-time access to call a method
final cart = context.read<CartModel>();
processCheckout(cart.items, cart.total);
},
child: const Text('Checkout'),
);
}
}
// Cart page watches the full model for the item list
class CartPage extends StatelessWidget {
const CartPage({super.key});
@override
Widget build(BuildContext context) {
// watch: rebuilds on any cart change
final cart = context.watch<CartModel>();
return Column(
children: [
Expanded(
child: ListView.builder(
itemCount: cart.items.length,
itemBuilder: (context, index) {
final item = cart.items[index];
return ListTile(
title: Text(item.name),
subtitle: Text('Qty: \${item.quantity}'),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () =>
context.read<CartModel>().removeItem(index),
),
);
},
),
),
const CartTotalDisplay(),
const CheckoutButton(),
],
);
}
}