Dependency Injection in Flutter

Medium PriorityAsked in ~60% of mid-level interviews

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.

ApproachResolutionScopingTest overrideBest for
Constructor injectionManualManualPass mock in constructorSmall apps, libraries
Riverpod providersref.watch(provider)Per ProviderScopeoverrideWithValueMost production apps
get_it (service locator)getIt<T>()Singleton / factory / lazygetIt.registerSingleton<T>(mock) in test setUpSimple, framework-agnostic
InheritedWidget DIDeps.of(context)Scoped to subtreeWrap in alternative widgetTiny 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

SituationPick
Greenfield production appRiverpod
Small app, team unfamiliar with state mgmtget_it
You want zero dependenciesInheritedWidget
Library author exposing servicesConstructor injection — let consumers wire it
Modular plugin-style architectureget_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

  1. 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.

  2. 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.

  3. How do you override dependencies in tests? Riverpod: ProviderScope(overrides: [apiProvider.overrideWithValue(MockApi())], child: ...). get_it: getIt.registerSingleton<ApiClient>(MockApiClient()) in setUp, then getIt.reset() in tearDown. InheritedWidget DI: wrap with an alternative Deps instance for the test.

  4. 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.