How to Handle State in Flutter with Provider
How to Handle State in Flutter with Provider
State management is one of the most discussed topics in Flutter development. For small apps, setState is perfectly adequate. But as your app grows — multiple screens sharing data, async operations updating the UI, widgets deep in the tree needing global state — you need a more scalable solution. Provider is Flutter's officially recommended state management package for most use cases. This guide explains how to use it effectively.
What Is Provider?
Provider is a wrapper around Flutter's InheritedWidget that makes it dramatically easier to use. At its core, it solves one problem: how do you share data between widgets without passing it down through every constructor (prop drilling)?
Provider places data above the widget tree and lets any descendant widget subscribe to it. When the data changes, only the subscribing widgets rebuild — not the entire tree.
Installing Provider
Add it to your pubspec.yaml:
dependencies:
provider: ^6.1.2
The Core Pattern: ChangeNotifier
The most common Provider pattern uses ChangeNotifier:
- Create a class that extends
ChangeNotifier - Expose data as fields and mutate them through methods
- Call
notifyListeners()after mutations to trigger UI rebuilds
class CartModel extends ChangeNotifier {
final List<Item> _items = [];
List<Item> get items => List.unmodifiable(_items);
int get totalPrice => _items.fold(0, (sum, item) => sum + item.price);
void addItem(Item item) {
_items.add(item);
notifyListeners();
}
void removeItem(String id) {
_items.removeWhere((item) => item.id == id);
notifyListeners();
}
}
Providing the Model
Wrap your app (or the subtree that needs access) with ChangeNotifierProvider:
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CartModel(),
child: const MyApp(),
),
);
}
The create callback receives a BuildContext and returns the notifier. Provider manages the notifier's lifecycle — it disposes it automatically when the provider is removed from the tree.
Consuming the Model
There are three ways to read from a provider:
context.watch — rebuilds on change
class CartBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cart = context.watch<CartModel>();
return Badge(count: cart.items.length);
}
}
context.read — one-time read, no rebuild
ElevatedButton(
onPressed: () {
context.read<CartModel>().addItem(selectedItem);
},
child: const Text('Add to Cart'),
)
Use context.read in callbacks, not in build methods. Using it in build means the widget will not rebuild when the value changes.
context.select — rebuild on partial change
final totalPrice = context.select<CartModel, int>(
(cart) => cart.totalPrice,
);
select is a performance optimization — the widget only rebuilds when totalPrice changes, not when other fields in CartModel change.
Multiple Providers
For apps with multiple pieces of shared state, use MultiProvider:
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CartModel()),
ChangeNotifierProvider(create: (_) => UserModel()),
ChangeNotifierProvider(create: (_) => ThemeModel()),
],
child: const MyApp(),
)
Handling Async with Provider
class ProductsModel extends ChangeNotifier {
List<Product> _products = [];
bool _loading = false;
String? _error;
List<Product> get products => _products;
bool get loading => _loading;
String? get error => _error;
Future<void> fetchProducts() async {
_loading = true;
_error = null;
notifyListeners();
try {
_products = await api.getProducts();
} catch (e) {
_error = e.toString();
} finally {
_loading = false;
notifyListeners();
}
}
}
In the widget:
@override
void initState() {
super.initState();
context.read<ProductsModel>().fetchProducts();
}
When to Move Beyond Provider
Provider handles most use cases well. Consider alternatives when:
- Your state logic is very complex with many interdependencies — Riverpod or Bloc offer better structure
- You need code generation for boilerplate reduction — Riverpod with
@riverpodannotations - You want strict separation of events and state — Bloc with event/state classes
Provider is not wrong at scale — many production apps use it. But Riverpod addresses several of Provider's edge cases (reading context outside widgets, testing without BuildContext) and is worth learning as a next step.
Conclusion
Provider gives you a clean, testable way to share and react to state changes across your Flutter widget tree. The ChangeNotifier + ChangeNotifierProvider + context.watch/read/select trio covers the vast majority of real-world state management needs. Start with Provider, learn its patterns thoroughly, and you will have a solid foundation for understanding more advanced solutions when you need them.
Sign in to like, dislike, or report.