Phase 01

Getting Started with Riverpod 3

Calculating reading time... Module 1: Foundations

🎯 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.

📖 Why Legacy Provider Was Replaced

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.

Evolution of Flutter State Management
1
setState()
Ephemeral local widget state. Unsuited for shared global application state across screens.
2
InheritedWidget / Legacy Provider
Context-bound state lookup. Suffered from ProviderNotFoundException runtime errors.
3
Riverpod 3 (Current Gold Standard)
Compile-time safe, decoupled from 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:

YAML 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:

Dart lib/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(),
    ),
  );
}
⚠️ Mandatory Scope Rule

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.

Which ref operation should I use?
Inside Widget build()
ref.watch()
Rebuilds UI when state updates. Use ref.listen() for Snackbars/Navigation.
Inside Event Callback (onPressed)
ref.read()
One-time read to invoke methods on notifiers (e.g. ref.read(p.notifier).submit()).

4. Real Flutter Examples

Example 1: Simple Counter Application

Below is a complete, compilable Flutter counter app using Riverpod 3:

Dart lib/main.dart (Counter Example)
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:

Dart lib/features/settings/theme_screen.dart
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

❌ Mistake 1: Calling ref.read() inside build()

Replacing ref.watch() with ref.read() inside build() breaks reactivity. The UI will fail to update when provider state changes.

✨ Best Practice: Use ref.watch() by default in build()

Always default to ref.watch() inside build() methods. Use ref.read() exclusively inside event callbacks (e.g. onPressed).

6. Knowledge Check & Mini Challenge

❓ Question: Where should ref.read() be used?
A) Inside widget build() method to read state
B) Inside button click handlers (onPressed) to invoke methods
C) Inside main() before ProviderScope

🏋️ 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.

Click to reveal solution code 🔍
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.