The Riverpod model: ProviderScope and consumers
Riverpod's central structural decision: providers do not live in the widget tree. The official comparison page describes them as plain Dart objects — top-level final variables. Only one widget remains in the tree: ProviderScope at the app root, the container that holds provider state.
Two consequences follow:
- Reads target an object, not a type:
ref.watch(counterProvider). The runtime error "no provider of this type in the tree" disappears — if you don't import the provider, the code does not compile. - You can have any number of providers of the same type:
nameProviderandtitleProviderboth returningStringnever collide.
On the UI side there are three entry points: ConsumerWidget (build(context, ref)), ConsumerStatefulWidget with ConsumerState (where ref is available at class level), and the Consumer widget for a small area inside an existing widget.
void main() {
// ProviderScope provider-lərin state-ini saxlayan qabdır.
runApp(const ProviderScope(child: MyApp()));
}
// Provider top-level final dəyişəndir — ağacda deyil.
final counterProvider = NotifierProvider<Counter, int>(Counter.new);
class Counter extends Notifier<int> {
@override
int build() => 0; // ilkin state
void increment() => state++;
}
// 1) ConsumerWidget: build metodunda ref var.
class CounterText extends ConsumerWidget {
const CounterText({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}
// 2) Consumer: mövcud widget-in içində yalnız kiçik sahə.
Column(
children: [
const ExpensiveHeader(),
Consumer(
builder: (context, ref, child) => Text('${ref.watch(counterProvider)}'),
),
],
);
// 3) ConsumerStatefulWidget: initState/dispose lazım olduqda.
class ChartPage extends ConsumerStatefulWidget {
const ChartPage({super.key});
@override
ConsumerState<ChartPage> createState() => _ChartPageState();
}
class _ChartPageState extends ConsumerState<ChartPage> {
@override
void initState() {
super.initState();
// ConsumerState daxilində ref sinif səviyyəsindədir.
ref.read(counterProvider.notifier).increment();
}
@override
Widget build(BuildContext context) => Text('${ref.watch(counterProvider)}');
}The root, a provider and the three ways to read.
| Provider (package) | Riverpod |
|---|---|
| `MultiProvider` inside the tree | One `ProviderScope` at the root; providers are global `final` variables |
| `BuildContext.watch<T>()` | `WidgetRef.watch(provider)` |
| `BuildContext.read<T>()` | `WidgetRef.read(provider)` |
| `BuildContext.select<T, R>()` | `WidgetRef.watch(provider.select(...))` |
| `ProxyProvider` | `ref.watch()` inside a provider |
| `ChangeNotifierProxyProvider` | `ref.listen()` |
The strictest rule on the official DO/DON'T page: providers must be created only as top-level `final` variables. Creating them inside classes or dynamically — in the docs' own words — causes memory leaks and unexpected behaviour. If you need a parameter, use family; creating providers dynamically is not the way.
📚 Sources and documentation
- Riverpod: getting startedofficialriverpod.dev
ProviderScope, installation and the first provider — the 3.x docs.
- Consumersofficialriverpod.dev
The difference between ConsumerWidget, ConsumerStatefulWidget and Consumer.
- From Provider to Riverpod: comparisonofficialriverpod.dev
The source of the API mapping table.
- DO / DON'T rulesofficialriverpod.dev
Where the global final rule and the ban on dynamic creation live.