Sparround

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: nameProvider and titleProvider both returning String never 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.

dart
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 treeOne `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