Getting Started with Riverpod 3
🎯 Learning Objective
Understand why Riverpod replaced legacy Flutter state management, install Riverpod 3 dependencies, bootstrap your app with ProviderScope, and master the three core ref operations: ref.watch(), ref.read(), and ref.listen().
📋 Prerequisites
- Basic knowledge of Flutter widgets (
StatelessWidget,StatefulWidget). - Basic Dart programming syntax (classes, async/await, streams).
- Zero prior Riverpod knowledge required.
1. Why State Management & Why Riverpod?
In Flutter, UI is a function of state: UI = f(State). When state changes, Flutter rebuilds the corresponding widget subtree. However, as applications grow, passing state down constructor chains (prop drilling) becomes unmaintainable.
The original provider package relied on InheritedWidget under the hood. This caused three major pain points:
1. Runtime Crashes: Missing a provider ancestor triggered ProviderNotFoundException at runtime.
2. BuildContext Coupling: Reading state outside the widget tree (e.g. inside repositories or background services) was impossible.
3. Combination Complexity: Combining multiple providers required verbose ProxyProvider boilerplate.
Riverpod (an anagram of Provider) was created by Remi Rousselet to solve these fundamental flaws. Riverpod providers are compile-time safe, declared globally as final top-level variables, and do not depend on Flutter's BuildContext.
ProviderNotFoundException runtime errors.BuildContext, automatic disposal, and full code generation.2. Installing Riverpod 3 & ProviderScope
To use Riverpod 3 with code generation, add the following packages to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.5.1
riverpod_annotation: ^2.3.5
dev_dependencies:
build_runner: ^2.4.9
riverpod_generator: ^2.4.0
Bootstrapping with ProviderScope
For Riverpod to store provider states, wrap your entire application root in a ProviderScope widget inside main.dart:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
// ProviderScope stores the state of all providers
const ProviderScope(
child: MyApp(),
),
);
}
If you attempt to read a Riverpod provider without wrapping your application root in ProviderScope, Flutter will throw a fatal initialization error at launch.
3. ConsumerWidget & The Three Ref Operations
To consume providers inside Flutter UI, replace standard StatelessWidget with ConsumerWidget. This provides a WidgetRef ref parameter inside the build() method.
The Three Core Ref Operations
ref.watch(provider) — Reactive Subscription
WHAT: Subscribes the widget to a provider and returns its current state value.
WHY: Automatically triggers a widget rebuild whenever the provider's state changes.
WHERE TO USE: Inside the build() method of a ConsumerWidget.
WHERE NOT TO USE: Never inside button click handlers, initState(), or asynchronous callbacks.
ref.read(provider) — One-Time Non-Reactive Read
WHAT: Reads the current value of a provider once without subscribing to changes.
WHY: Avoids unnecessary widget rebuilds when you only need to trigger a method or read state once.
WHERE TO USE: Inside button onPressed callbacks, form submit handlers, or lifecycle listeners.
WHERE NOT TO USE: Never directly inside a widget's build() method.
ref.listen(provider, (previous, next) { ... }) — Side-Effect Listener
WHAT: Executes a callback function whenever a provider's state changes.
WHY: Handles side-effects like showing Snackbars, displaying Dialogs, or navigating to a new screen without rebuilding UI.
WHERE TO USE: Inside the build() method of a ConsumerWidget.
4. Real Flutter Examples
Example 1: Simple Counter Application
Below is a complete, compilable Flutter counter app using Riverpod 3:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 1. Declare a Notifier Provider for State
final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
class CounterNotifier extends Notifier<int> {
@override
int build() => 0; // Initial state
void increment() => state++;
}
// 2. Consume Provider inside ConsumerWidget
class CounterScreen extends ConsumerWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch counter state reactively
final count = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(title: const Text('Riverpod Counter')),
body: Center(
child: Text('Count: $count', style: const TextStyle(fontSize: 24)),
),
floatingActionButton: FloatingActionButton(
// Read notifier method on click
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: const Icon(Icons.add),
),
);
}
}
Example 2: Production Theme Mode Switcher App
A production-ready theme mode switcher demonstrating ref.watch() for UI theme rebuilds and ref.listen() for showing snackbar notifications:
final themeModeProvider = NotifierProvider<ThemeNotifier, ThemeMode>(ThemeNotifier.new);
class ThemeNotifier extends Notifier<ThemeMode> {
@override
ThemeMode build() => ThemeMode.light;
void toggleTheme() {
state = state == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
}
}
class ThemeSettingsScreen extends ConsumerWidget {
const ThemeSettingsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 1. Listen for side-effects (Snackbar)
ref.listen<ThemeMode>(themeModeProvider, (previous, next) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Theme changed to ${next.name}')),
);
});
// 2. Watch state for UI rebuild
final themeMode = ref.watch(themeModeProvider);
return Scaffold(
appBar: AppBar(title: const Text('Theme Settings')),
body: ListTile(
title: const Text('Dark Mode'),
trailing: Switch(
value: themeMode == ThemeMode.dark,
onChanged: (_) => ref.read(themeModeProvider.notifier).toggleTheme(),
),
),
);
}
}
5. Common Mistakes & Best Practices
Replacing ref.watch() with ref.read() inside build() breaks reactivity. The UI will fail to update when provider state changes.
Always default to ref.watch() inside build() methods. Use ref.read() exclusively inside event callbacks (e.g. onPressed).
6. Knowledge Check & Mini Challenge
🏋️ Mini Challenge: Toggle Switcher Notifier
Build a boolean ToggleNotifier provider using Notifier<bool> that defaults to false and exposes a toggle() method. Consume it inside a ConsumerWidget with a Switch.
final toggleProvider = NotifierProvider<ToggleNotifier, bool>(ToggleNotifier.new);
class ToggleNotifier extends Notifier<bool> {
@override
bool build() => false;
void toggle() => state = !state;
}
Phase 01 Summary
You have learned why Riverpod replaces legacy Provider, how to initialize ProviderScope, and how to use ref.watch(), ref.read(), and ref.listen() correctly in ConsumerWidget.