Module 1
Topic 5

Provider

Understanding the foundation of Riverpod — providers are the building blocks that hold and expose state.

Riverpod Docs – Providers Riverpod – Provider Concepts
What Is a Provider?

In Riverpod, a provider is a piece of logic that holds a value and can be listened to by widgets. Providers are the fundamental building blocks of Riverpod. They are:

  • Global — they can be accessed from anywhere in your app
  • Type-safe — they hold a specific type of value
  • Reactive — widgets can listen to them and rebuild when the value changes
  • Composable — providers can depend on other providers

✅ The Big Idea

Think of a provider as a container for state that lives outside your widget tree. You define providers globally, and then your widgets can watch them to get the current value and rebuild when it changes.

How Providers Work

A provider is defined using a simple function that returns a value. Here's the basic structure:

1. final myProvider

The provider is declared as a top-level final variable. This makes it globally accessible.

2. Provider<String>

The type parameter tells Riverpod what type of value this provider holds.

3. (ref) => ...

The creation function. It receives a ref object that can be used to read other providers.

4. return 'Hello, World!';

The value that the provider exposes. This can be any Dart object.

Reading Providers

There are three main ways to interact with providers in your widgets:

👀

ref.watch()

Listens to a provider and rebuilds the widget when the provider's value changes.

Use in build() method
📖

ref.read()

Reads a provider once without listening for changes. The widget does not rebuild.

Use in callbacks, event handlers
🔔

ref.listen()

Runs a callback when the provider's value changes. The widget does not rebuild.

Use for side effects (navigation, logging)
🔗

ref.read().notifier

Gets the notifier of a provider (for StateProvider, StateNotifierProvider) to update state.

Use to update state from callbacks
Types of Providers

Riverpod offers several provider types, each designed for a specific use case:

📦

Provider

The simplest provider. Holds a value that never changes after initialization. Good for dependencies, configuration, and services.

🔄

StateProvider

Holds a mutable piece of state. Use for simple state like counters, booleans, or strings.

FutureProvider

Holds a Future . Perfect for asynchronous data fetching like API calls. Handles loading, data, and error states.

🌊

StreamProvider

Holds a Stream . Ideal for real-time data like Firebase Firestore streams.

🎯

StateNotifierProvider

The most powerful provider. Holds a StateNotifier that manages complex state with methods. Best for forms, lists, and complex business logic.

Complete Example: Dependency Injection

A common use case for the basic Provider is dependency injection . You can use providers to make services, repositories, or configuration objects available throughout your app.

Step-by-Step Explanation
1.
Define ApiService – A simple Dart class that simulates an API service. In a real app, this might make HTTP requests or interact with a database.
2.
apiServiceProvider – A Provider that creates and holds an ApiService instance. This ensures the service is created once and reused throughout the app.
3.
greetingProvider – This provider depends on apiServiceProvider . It uses ref.watch() to get the service and creates a greeting string.
4.
ProviderScope – The required root widget for Riverpod. It stores all provider states.
5.
ConsumerWidget – Instead of StatelessWidget , we use ConsumerWidget to get access to ref . We watch greetingProvider and display its value.
Provider Dependencies (Composition)

One of the most powerful features of Riverpod is that providers can depend on other providers. This is called provider composition .

🔗 How Provider Composition Works

When a provider depends on another provider, it uses ref.watch() to read the value. If the depended-upon provider changes, the dependent provider also updates. This creates a reactive dependency graph .

✅ Why This Is Powerful

  • Reactive updates – When counterProvider changes, both doubledCounterProvider and messageProvider update automatically.
  • Clean separation – Each provider has a single responsibility.
  • Testability – Each provider can be tested in isolation.
  • Performance – Only the widgets that watch specific providers rebuild.
Common Mistakes
❌ Mistake 1: Using ref.watch() inside event handlers

ref.watch() is only valid inside the build() method of a ConsumerWidget or inside a provider's creation function. Using it in callbacks will throw an error.

✅ Correct: Use ref.read() in event handlers

For buttons, taps, and other callbacks, use ref.read() to read the current value without listening for changes.

❌ Mistake 2: Creating providers inside widgets

Providers should be declared as top-level final variables , not inside widgets. Declaring them inside widgets breaks the global accessibility and can lead to unexpected behavior.

✅ Correct: Declare providers at top level

final myProvider = Provider<String>((ref) => 'value'); should be at the top of your file, outside any class.

❌ Mistake 3: Not wrapping the app with ProviderScope

Without ProviderScope , Riverpod cannot store provider states. This will cause runtime errors.

✅ Correct: Always use ProviderScope at the root

runApp(ProviderScope(child: MyApp())); is the standard way to initialize Riverpod.

🎯 Key Takeaway

Providers are the building blocks of Riverpod. They hold state, services, or dependencies and make them available throughout your app. Understanding the different types of providers and when to use each is essential for effective state management. Start with Provider for dependencies and services, and use StateProvider for simple state that changes.