Dependency Injection in Flutter
3 min read
Architecture
Constructor injection is the underlying habit — DI tools (Riverpod, get_it, InheritedWidget) just hide the wiring. Pick based on team familiarity and complexity; default to Riverpod for new projects.
| Approach | Resolution | Scoping | Test override | Best for |
|---|---|---|---|---|
| Constructor injection | Manual | Manual | Pass mock in constructor | Small apps, libraries |
| Riverpod providers | ref.watch(provider) | Per ProviderScope | overrideWithValue | Most production apps |
| get_it (service locator) | getIt<T>() | Singleton / factory / lazy | getIt.registerSingleton<T>(mock) in test setUp | Simple, framework-agnostic |
| InheritedWidget DI | Deps.of(context) | Scoped to subtree | Wrap in alternative widget | Tiny apps, no extra dep |
Default to Riverpod if you're picking fresh today. get_it is fine for simpler apps; just be aware it's a global registry, not scoped.
Code in action — same dependencies, three styles
// 1️⃣ Riverpod — typed, scoped, override in tests
final apiClientProvider = Provider<ApiClient>((ref) => ApiClient());
final userRepoProvider = Provider<UserRepository>((ref) {
final api = ref.watch(apiClientProvider); // dependency auto-resolved
return UserRepository(api);
});
// In a widget
final repo = ref.watch(userRepoProvider);
// 2️⃣ get_it — service locator, easy to start with
final getIt = GetIt.instance;
void registerServices() {
getIt.registerSingleton<ApiClient>(ApiClient());
getIt.registerLazySingleton<Database>(() => Database());
getIt.registerFactory<Logger>(() => Logger());
getIt.registerFactoryParam<UserRepository, String, void>(
(userId, _) => UserRepository(userId),
);
}
// Usage
final api = getIt<ApiClient>();
// 3️⃣ InheritedWidget DI — no dependency at all
class Deps extends InheritedWidget {
const Deps({super.key, required this.api, required this.db, required super.child});
final ApiClient api;
final Database db;
static Deps of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<Deps>()!;
@override
bool updateShouldNotify(Deps _) => false; // deps don't change
}
// Provide once at the root
Deps(api: ApiClient(), db: Database(), child: const MyApp());
When to use which
| Situation | Pick |
|---|---|
| Greenfield production app | Riverpod |
| Small app, team unfamiliar with state mgmt | get_it |
| You want zero dependencies | InheritedWidget |
| Library author exposing services | Constructor injection — let consumers wire it |
| Modular plugin-style architecture | get_it with pushNewScope per module |
Common mistakes to avoid
// ❌ Reaching into a global singleton everywhere
class WidgetA { final api = getIt<ApiClient>(); }
class WidgetB { final api = getIt<ApiClient>(); }
// → hidden dependencies, brittle tests
// ✅ Inject through constructor, OR scope via Riverpod providers
// ❌ Treating get_it singletons like they live forever
// They DO until you call .reset() — perfect for tests, easy to forget in code
// ❌ Mixing DI approaches in one app for "comparison"
// Cognitive overhead, inconsistent test setup — pick one
// ❌ Using BuildContext-based DI outside widgets
// (Provider.of, Deps.of) — not accessible from pure Dart layers
// ✅ Constructor injection or Riverpod (no context required)
// ❌ Forgetting to override providers in widget tests
// Tests hit the real network — flaky, slow
// ✅ Always wrap test widgets with ProviderScope(overrides: [...])
Interview follow-ups
-
What's the difference between a service locator and dependency injection? DI is the pattern: dependencies are provided to a class from outside. A service locator (like
get_it) is one implementation of DI — a global registry you pull from. "True" DI passes deps into constructors; service locators trade purity for ergonomics. -
Why is Riverpod preferred over get_it in many teams? Compile-time safety (missing providers fail to compile, not at runtime), automatic scoping/disposal, override-for-test built in, and provider dependencies are explicit (
ref.watch). get_it is global and untyped — easier to start, easier to abuse. -
How do you override dependencies in tests? Riverpod:
ProviderScope(overrides: [apiProvider.overrideWithValue(MockApi())], child: ...). get_it:getIt.registerSingleton<ApiClient>(MockApiClient())insetUp, thengetIt.reset()intearDown. InheritedWidget DI: wrap with an alternativeDepsinstance for the test. -
When would you NOT use a DI framework at all? For very small apps, plain constructor injection is fine —
MyApp(api: ApiClient())is explicit, type-safe, and zero-cost. Skip the framework until you feel the pain of threading dependencies through five widget layers.
How helpful was this content?
Please sign in to rate this article.