Skill: Dependency Injection
Overview
In a Riverpod codebase the provider graph is the DI container. A provider declares how to build a dependency; ref.watch resolves it; ProviderScope(overrides: …) swaps it. That gives compile-checked wiring with no service locator, no runtime registration order, and a test seam on every edge. The rule from the rest of this toolkit still holds: depend on interfaces declared in Domain, never on concrete Data classes.
Use Cases
- Wiring a feature's repository → use case → notifier chain at the composition root.
- Replacing a real data source with a fake in unit and widget tests.
- Environment/flavor differences (staging base URL, mock backend) resolved at app start.
- Objects that must exist before
runApp(SharedPreferences, database handles) injected via a root override.
Best Practices
- Declare providers for interfaces, not implementations: the provider's type is
ArticleRepository, its body returnsArticleRepositoryImpl. - Keep the composition root at
ProviderScopeinmain.dart. Feature code never constructs its ownDioor database. - For dependencies that need
awaitbefore the app runs, create an unimplemented provider and override it at the root — this keeps callers synchronous. - Use
ref.watchfor dependencies inside providers so the graph rebuilds when a dependency changes (e.g. the auth token changes and the client must be recreated). - Use
ref.onDisposeto release sockets, controllers, and subscriptions. - Keep provider declarations next to the thing they build (Data providers in
data/, use case providers indomain/), not in one giantproviders.dart. - If the team wants DI decoupled from state management,
get_it+injectableis the accepted alternative — but then all wiring goes through it; do not run two containers.
Anti-Patterns
- ❌ A hand-rolled singleton (
static final instance = …) reachable from business logic. - ❌ Constructing
Dio(),Drift, or a repository inside a widget or notifier. - ❌ Providers typed as the concrete implementation, which defeats the test seam.
- ❌
ref.readof a dependency inside a provider body (it will not react to changes). - ❌ Globals for configuration read directly by Domain code.
- ❌ Running
get_itand Riverpod as parallel containers.
Checklist
- [ ] Every dependency is reachable through a provider; no singletons in business logic.
- [ ] Providers expose Domain interfaces, not Data implementations.
- [ ] Pre-
runAppasync dependencies are injected via rootProviderScopeoverrides. - [ ] Providers use
ref.watchfor their own dependencies. - [ ] Disposable resources register
ref.onDispose. - [ ] Tests override the outermost seam (repository or data source), not internals.
Dart Examples
dart
// data/providers.dart
final dioProvider = Provider<Dio>((ref) {
final dio = Dio(BaseOptions(baseUrl: ref.watch(configProvider).apiBaseUrl))
..interceptors.add(ref.watch(authInterceptorProvider));
ref.onDispose(dio.close);
return dio;
});
// The provider's TYPE is the Domain interface — that is the seam.
final articleRepositoryProvider = Provider<ArticleRepository>(
(ref) => ArticleRepositoryImpl(ArticleRemoteDataSource(ref.watch(dioProvider))),
);
// domain/providers.dart
final getArticlesProvider = Provider<GetArticles>(
(ref) => GetArticles(ref.watch(articleRepositoryProvider)),
);dart
// main.dart — the composition root, and the only place async setup happens
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final prefs = await SharedPreferences.getInstance();
runApp(
ProviderScope(
overrides: [sharedPreferencesProvider.overrideWithValue(prefs)],
child: const App(),
),
);
}
// core/providers.dart
final sharedPreferencesProvider = Provider<SharedPreferences>(
(ref) => throw UnimplementedError('overridden in main()'),
);dart
// test — override the seam, no mocking framework required
test('loads articles from the repository', () async {
final container = ProviderContainer(
overrides: [
articleRepositoryProvider.overrideWithValue(FakeArticleRepository(articles: [article])),
],
);
addTearDown(container.dispose);
final articles = await container.read(articlesProvider.future);
expect(articles, hasLength(1));
});Common Interview Questions
- Why type a provider as the interface rather than the implementation?
- How do you inject something that requires
awaitbeforerunApp? ref.watchvsref.readinside a provider body — what breaks if you get it wrong?- How does
ProviderScope(overrides:)compare with a service locator for testability? - When would you still reach for
get_it?
AI Implementation Notes
- When adding a data source, generate its provider in the same commit and type it as the Domain interface.
- Never generate a singleton or a top-level mutable global for a dependency.
- In generated tests, override at the repository boundary and use a hand-written fake before reaching for
mocktail. - iOS counterpart:
../ios/dependency_injection.md. - Related:
clean_architecture.md,state_management.md.