Your First Provider
A provider is the fundamental building block of Riverpod. It exposes a piece of state, a value, or business logic that can be shared and consumed throughout your application.
What is it?
A provider is an object that tells Riverpod how to create a value.
Instead of manually creating objects and passing them through constructors or widget trees, you define a provider once and let Riverpod manage its lifecycle.
Providers can expose:
- Simple values
- Services
- Repositories
- Configuration
- Computed values
- Application state
- Asynchronous data
A provider does not store state by itself (except stateful provider types like NotifierProvider or StateProvider). It simply defines how a value should be created.
Why does it exist?
Without providers, shared objects often need to be:
- Passed through multiple widget constructors (prop drilling)
- Created as global variables
- Managed manually using dependency injection
For example:
App
├── Home
│ └── Profile
│ └── Settings
If Settings needs a UserRepository, you would normally pass it through every widget above it.
Providers eliminate this problem.
Instead, any widget can directly access the provider whenever needed.
Benefits include:
- No prop drilling
- Automatic dependency management
- Lazy initialization
- Cached values
- Easy testing
- Automatic rebuilding when dependencies change
Syntax
Creating a Provider
final greetingProvider = Provider<String>((ref) {
return 'Hello Riverpod!';
});
Explanation:
Provider<T>creates a provider that exposes a value of typeT.refallows the provider to read other providers or use lifecycle APIs.- The returned value is cached until the provider is invalidated.
Reading a Provider
class HomePage extends ConsumerWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final greeting = ref.watch(greetingProvider);
return Text(greeting);
}
}
Explanation:
ConsumerWidgetprovides access toWidgetRef.ref.watch()subscribes to the provider.- The widget rebuilds whenever the provider's value changes.
Provider Depending on Another Provider
final firstNameProvider = Provider((ref) => 'John');
final fullNameProvider = Provider((ref) {
final firstName = ref.watch(firstNameProvider);
return '$firstName Doe';
});
Explanation:
- Providers can depend on other providers.
ref.watch()creates a dependency relationship.- If
firstNameProviderchanges,fullNameProviderrecomputes automatically.
Mental Model
Think of providers as factories managed by Riverpod.
Provider
│
"How do I create this value?"
│
▼
Riverpod Container
│
Creates the value
│
Caches the value
│
Gives it to every consumer
You define how to create a value.
Riverpod decides:
- when to create it
- when to reuse it
- when to dispose it
- when to recreate it
Examples
Simple Example
final appNameProvider = Provider<String>((ref) {
return 'My Application';
});
Explanation:
- Provides a constant string.
- Every consumer receives the same cached value.
Repository Example
class UserRepository {
Future<String> getUser() async {
return 'John';
}
}
final userRepositoryProvider = Provider<UserRepository>((ref) {
return UserRepository();
});
Explanation:
- The provider creates a single
UserRepository. - Widgets and other providers can access the same instance.
Computed Provider
final firstNameProvider = Provider((ref) => 'John');
final lastNameProvider = Provider((ref) => 'Doe');
final fullNameProvider = Provider((ref) {
return '${ref.watch(firstNameProvider)} ${ref.watch(lastNameProvider)}';
});
Explanation:
- Combines values from multiple providers.
- Automatically updates whenever either dependency changes.
When to Use
Use Provider when you need:
- Read-only values
- Dependency injection
- Services
- Repositories
- Configuration objects
- Utility classes
- Computed values
- Shared business logic without mutable state
When NOT to Use
Do not use Provider when the value changes over time.
Instead use:
| Situation | Use |
|---|---|
| Mutable state | NotifierProvider |
| Simple mutable value | StateProvider |
| Future | FutureProvider |
| Stream | StreamProvider |
| Async business logic | AsyncNotifierProvider |
Best Practices
- Keep providers focused on one responsibility.
- Prefer immutable values.
- Compose providers instead of creating large providers.
- Use providers for dependency injection.
- Avoid putting UI logic inside providers.
- Let providers depend on other providers rather than manually creating dependencies.
Common Mistakes
1. Using Provider for mutable state
❌ Wrong
final counterProvider = Provider((ref) => 0);
Why it's wrong:
Provideris read-only.- The value cannot be updated.
✔ Correct
final counterProvider = StateProvider((ref) => 0);
2. Creating dependencies manually
❌ Wrong
final apiProvider = Provider((ref) {
final client = HttpClient();
return ApiService(client);
});
If another provider also creates HttpClient, multiple unnecessary instances exist.
✔ Correct
final httpClientProvider = Provider((ref) {
return HttpClient();
});
final apiProvider = Provider((ref) {
final client = ref.watch(httpClientProvider);
return ApiService(client);
});
Why it's better:
- Dependencies are reusable.
- Riverpod manages the dependency graph.
- Easier to override during testing.
3. Doing expensive work inside a Provider
❌ Wrong
final usersProvider = Provider((ref) {
return fetchUsers();
});
Why it's wrong:
Provideris intended for synchronous values.- Asynchronous operations belong in dedicated async providers.
✔ Correct
final usersProvider = FutureProvider((ref) async {
return fetchUsers();
});
Related APIs
- Provider
- StateProvider
- FutureProvider
- StreamProvider
- NotifierProvider
- AsyncNotifierProvider
- WidgetRef
- ref.watch()
- ref.read()
Summary
A Provider is the simplest Riverpod provider. It defines how to create a value, while Riverpod handles its creation, caching, dependency tracking, and lifecycle. Use it for read-only values, dependency injection, services, repositories, and computed data.