Flutter Lesson 40 of 83 4 min read
Provider in Flutter: State Management Tutorial
Learn the provider package in Flutter: ChangeNotifierProvider, context.watch, read and select, Consumer and MultiProvider, with a cart example.
On this page
provider is the most widely used state management package in Flutter. It does one job: put an object above part of the widget tree so that any widget below can get it, and be rebuilt when it changes. It is a convenient wrapper around InheritedWidget.
flutter pub add provider
Three steps #
- Write a model, a
ChangeNotifier. - Provide it above the widgets that need it.
- Consume it with
context.watchorcontext.read.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// 1. The model
class CartModel extends ChangeNotifier {
final List<String> _items = [];
List<String> get items => List.unmodifiable(_items);
int get count => _items.length;
void add(String item) {
_items.add(item);
notifyListeners();
}
void clear() {
_items.clear();
notifyListeners();
}
}
void main() {
runApp(
// 2. Provide it above the whole app
ChangeNotifierProvider(
create: (context) => CartModel(),
child: const MaterialApp(home: ShopPage()),
),
);
}
class ShopPage extends StatelessWidget {
const ShopPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Shop'),
actions: const [CartCount()],
),
body: ListView(
children: [
for (final name in ['Pen', 'Notebook', 'Bag'])
ListTile(
title: Text(name),
trailing: IconButton(
icon: const Icon(Icons.add_shopping_cart),
// 3a. read: call a method, do not listen
onPressed: () => context.read<CartModel>().add(name),
),
),
],
),
);
}
}
class CartCount extends StatelessWidget {
const CartCount({super.key});
@override
Widget build(BuildContext context) {
// 3b. watch: rebuild this widget whenever the cart changes
final count = context.watch<CartModel>().count;
return Padding(
padding: const EdgeInsets.only(right: 16),
child: Badge(label: Text('$count'), child: const Icon(Icons.shopping_cart)),
);
}
}
No constructor passes the cart anywhere. CartCount could be on a different screen and would work the same. When an item is added, only CartCount rebuilds. The list does not, because it never watched.
watch, read and select #
| Call | Rebuilds the widget? | Use in |
|---|---|---|
context.watch<T>() | Yes, on every change | build, to display data |
context.read<T>() | No | Callbacks such as onPressed, to call methods |
context.select<T, R>((m) => m.field) | Only when that one value changes | build, to listen to part of a model |
// Rebuilds only when the count changes, not when anything else in the cart does.
final count = context.select<CartModel, int>((cart) => cart.count);
The rule that prevents most bugs: watch in build, read in callbacks. Using watch inside onPressed throws. Using read in build shows stale data.
Consumer #
Consumer rebuilds only the part of a build method inside its builder. It is useful when you cannot easily split out a widget, or when the provider is created in the same build method.
Consumer<CartModel>(
builder: (context, cart, child) => Text('${cart.count} items'),
)
Several providers #
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthModel()),
ChangeNotifierProvider(create: (_) => CartModel()),
Provider(create: (_) => ApiClient()), // a plain object that never changes
],
child: const MyApp(),
)
Kinds of provider #
| Provider | Supplies |
|---|---|
Provider | A value that does not change, such as a service or repository |
ChangeNotifierProvider | A ChangeNotifier, and rebuilds listeners when it notifies |
FutureProvider | The result of a future |
StreamProvider | The latest value from a stream |
ProxyProvider | A value built from other providers |
ChangeNotifierProvider also calls dispose() on the model when it is removed from the tree.
One model depending on another #
MultiProvider(
providers: [
Provider(create: (_) => ApiClient()),
ChangeNotifierProvider(
create: (context) => ProductsModel(context.read<ApiClient>()),
),
],
child: const MyApp(),
)
Scoping #
A provider is visible only to widgets below it. Put app-wide state above MaterialApp. Put state for one screen above that screen, and it is created when the screen opens and disposed when it closes.
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (_) => ChangeNotifierProvider(
create: (_) => CheckoutModel(),
child: const CheckoutPage(),
),
),
);
The error everyone meets #
Error: Could not find the correct Provider<CartModel> above this Widget
It means the context you used is not below the provider. The usual causes:
- The provider is inside a page, and you pushed a new route. Routes are siblings under the navigator, not children of the page. Move the provider above
MaterialApp. - You created the provider and read it in the same
buildmethod, with the samecontext. Use aConsumer, or split the reading part into its own widget.
Loading data in a model #
class ProductsModel extends ChangeNotifier {
ProductsModel(this._api);
final ApiClient _api;
List<Product> products = [];
bool loading = false;
String? error;
Future<void> load() async {
loading = true;
error = null;
notifyListeners();
try {
products = await _api.fetchProducts();
} catch (e) {
error = 'Could not load products';
} finally {
loading = false;
notifyListeners();
}
}
}
// Start loading as soon as the model is created:
ChangeNotifierProvider(create: (context) => ProductsModel(context.read<ApiClient>())..load())
Try it yourself #
Build a favourites feature. A FavouritesModel holds a set of product ids with a toggle(id) method. A product list shows a heart on each row, filled when the product is a favourite. The app bar shows the number of favourites, and a second screen lists them.