The provider package is a pub.dev dependency for Flutter that wraps InheritedWidget and ChangeNotifier in a simpler API, giving apps context.watch, context.read, and MultiProvider for everyday state management.
It isn't part of the Flutter SDK, so it needs adding to pubspec.yaml before use. Note that path_provider is a different, unrelated package for finding filesystem directories – the name overlap with "provider" is a common source of confusion.
pubspec.yaml; it isn't bundled with the Flutter SDK.create and value constructors use them for different lifecycle scenarios: create is for creating a new value that the provider can manage and dispose, while value is for exposing an existing value whose lifecycle is managed elsewhere.context.watch<T>() subscribes the calling widget to changes and rebuilds it when the provided value changes; context.read<T>() accesses the value without subscribing to changes.context.read<T>() should be called in callbacks such as onPressed when you need to access a provider without rebuilding the widget in response to its changes.MultiProvider lets you declare multiple providers in a list instead of manually nesting them; internally, it builds the equivalent nested provider structure.Wrap the relevant part of the widget tree in a ChangeNotifierProvider, then read the model with context.watch or context.read.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CartModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void add() {
_count++;
notifyListeners();
}
}
class CartScreen extends StatelessWidget {
const CartScreen({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => CartModel(),
child: const CartView(),
);
}
}
class CartView extends StatelessWidget {
const CartView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const CartHeader(),
),
body: Center(
child: Consumer<CartModel>(
builder: (context, cart, child) {
return Text('${cart.count} items');
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CartModel>().add(),
child: const Icon(Icons.add),
),
);
}
}
class CartHeader extends StatelessWidget {
const CartHeader({super.key});
@override
Widget build(BuildContext context) {
final count = context.watch<CartModel>().count;
return Text('Cart ($count)');
}
}CartView is below the provider but doesn't subscribe to its changes. The Scaffold itself therefore doesn't rebuild when CartModel changes.
context.watch is used in CartHeader. Only CartHeader subscribes to CartModel, so it rebuilds when count changes.
context.read is used by the FloatingActionButton. It accesses the model to call add() when tapped without subscribing the button to changes.
The BuildContext matters. The context used by CartView is below ChangeNotifierProvider, so it can access CartModel. A context belonging to a widget above the provider cannot access it.
Wrap the relevant part of the widget tree in a ChangeNotifierProvider, then read the model with context.watch or context.read.
MultiProvider(
providers: [
Provider<CartModel>(create: (_) => CartModel()),
Provider<UserModel>(create: (_) => UserModel()),
],
child: const MyApp(),
),Each provider in the list is independent – widgets below can read CartModel and UserModel separately, in any order.
context.watch inside build, and context.read inside callbacks – calling context.read from build won't rebuild the widget when the value changes later.create own disposal – models constructed through a provider's create callback are disposed automatically; don't call dispose() on them yourself.Consumer or context.select to scope rebuilds to just the widget that needs the data, instead of rebuilding a large subtree.MultiProvider as soon as a screen needs more than one provider, rather than nesting them by hand.BuildContext you use is below the provider – a context can only access providers that are ancestors of its widget. If you try to call context.watch or context.read from a widget above the Provider, the provider won't be found.Provider around the part of the widget tree that actually needs the value, rather than providing it higher in the tree without a reason.Provider for values that don't need to notify dependents, ListenableProvider for general Listenable objects, ChangeNotifierProvider for ChangeNotifier objects, ValueListenableProvider for ValueListenable objects, StreamProvider for values emitted by a Stream, and FutureProvider for values produced by a Future..value constructor when exposing an existing instance. Use Provider.value when the value is created and managed outside the provider, rather than by its create callback.context.read inside the build method. It reads the value once and never rebuilds when it changes – use context.watch for anything the UI needs to react to.BuildContext that isn't below the widget that created it – for example, calling context.watch<CartModel>() in the same build method that creates the ChangeNotifierProvider<CartModel> above it. context.watch, context.read, and Provider.of all search upward from the given context, so if that context sits at or above the provider, the package throws a ProviderNotFoundException. Fix it by accessing the provider from a descendant context – for example, extract the code into a child widget, wrap it in a Builder, or use Consumer so that the context used to access the provider is below it in the widget tree.create callback. The provider owns its lifecycle and disposes of it automatically when removed from the widget tree, so don't dispose of the model yourself.
Flutter is known for its seamless UI/UX features and robust design elements. It allowed us to deliver a unique customer experience in the “CA24 Mobile” banking app. This article shares some of the learnings and pain points behind implementing it in this large project.