Dependency Injection
Provide dependencies through Riverpod instead of creating them manually, making your application more modular, testable, and maintainable.
What is it?
Dependency Injection (DI) is the practice of supplying an object with the dependencies it needs instead of allowing it to create those dependencies itself.
In Riverpod, providers act as a dependency injection container. Services, repositories, clients, and other shared objects are exposed through providers and consumed wherever they are needed.
Unlike traditional DI frameworks that rely on code generation or reflection, Riverpod uses providers to create and manage dependencies in a type-safe and compile-time friendly way.
Why does it exist?
Without dependency injection, classes often create their own dependencies.
For example:
- A repository creates its own HTTP client.
- A notifier creates its own repository.
- A widget creates its own service.
This tightly couples classes together, making them difficult to:
- Test
- Replace
- Mock
- Reuse
- Maintain
Dependency injection separates object creation from object usage.
Syntax
Providing a dependency
final apiClientProvider = Provider<ApiClient>((ref) {
return ApiClient();
});
Explanation:
- The provider owns the
ApiClient. - Every consumer receives the same instance unless overridden.
Injecting a dependency into another provider
final userRepositoryProvider = Provider<UserRepository>((ref) {
final apiClient = ref.watch(apiClientProvider);
return UserRepository(apiClient);
});
Explanation:
UserRepositorydepends onApiClient.- Riverpod resolves the dependency automatically.
Using the injected dependency
final userProvider = FutureProvider<User>((ref) async {
final repository = ref.watch(userRepositoryProvider);
return repository.fetchUser();
});
Explanation:
- The provider doesn't know how the repository was created.
- It simply consumes the injected dependency.
Mental Model
Think of providers as a dependency graph.
ApiClientProvider
│
▼
UserRepositoryProvider
│
▼
UserProvider
│
▼
ConsumerWidget
Each provider depends only on the providers directly below it, creating a clear and maintainable dependency chain.
Examples
Simple Example
final loggerProvider = Provider<Logger>((ref) {
return Logger();
});
final analyticsProvider = Provider<Analytics>((ref) {
final logger = ref.watch(loggerProvider);
return Analytics(logger);
});
Explanation:
Analyticsreceives its dependency through Riverpod.- No object creates its own dependencies.
Real-World Example
final dioProvider = Provider<Dio>((ref) {
return Dio();
});
final authRepositoryProvider = Provider<AuthRepository>((ref) {
final dio = ref.watch(dioProvider);
return AuthRepository(dio);
});
final authProvider =
AsyncNotifierProvider<AuthNotifier, User?>(
AuthNotifier.new,
);
Explanation:
- The HTTP client is shared.
- The repository depends on the HTTP client.
- The notifier depends on the repository.
- Each layer has a single responsibility.
When to Use
Use dependency injection for:
- Repositories
- API clients
- Database clients
- Local storage
- Authentication services
- Analytics services
- Logging
- Shared business services
- Configuration objects
When NOT to Use
Avoid dependency injection for:
- Temporary local variables
- Simple helper methods
- Immutable constants
- Values used only within a single function
Not every object needs to become a provider.
Best Practices
- Inject dependencies through providers.
- Keep dependencies immutable whenever possible.
- Prefer constructor injection over global variables.
- Keep dependency chains simple.
- Place shared dependencies in the
coremodule. - Override dependencies during testing instead of modifying production code.
Common Mistakes
Creating dependencies manually
Wrong:
class UserRepository {
final api = ApiClient();
}
Explanation:
- The repository controls how its dependency is created.
- Replacing or mocking the API client becomes difficult.
Correct:
class UserRepository {
UserRepository(this.api);
final ApiClient api;
}
Explanation:
- The dependency is supplied from outside.
- The repository is easier to test and reuse.
Accessing global singletons
Wrong:
final api = ApiClient.instance;
Explanation:
- Global state creates hidden dependencies.
- Tests become more difficult to isolate.
Correct:
final api = ref.watch(apiClientProvider);
Explanation:
- Dependencies are explicit.
- Riverpod manages their lifecycle.
Skipping providers for shared services
Wrong:
final repository = UserRepository(ApiClient());
await repository.fetchUser();
Explanation:
- Every caller creates a new repository and API client.
- Resources are duplicated unnecessarily.
Correct:
final repository = ref.watch(userRepositoryProvider);
await repository.fetchUser();
Explanation:
- The repository is shared and managed by Riverpod.
- Consumers don't need to know how it was created.
Related APIs
- Provider
- ProviderScope
- ProviderContainer
ref.watch()ref.read().overrideWith().overrideWithValue()
Summary
Riverpod's providers form a powerful dependency injection system. Instead of creating objects manually, expose shared dependencies through providers and inject them where needed. This reduces coupling, improves testability, simplifies maintenance, and allows Riverpod to manage the lifecycle of your application's services and repositories.