get_it vs Riverpod vs Provider for Dependency Injection: What I Actually Ship
Three popular tools, three different philosophies, one real-world verdict from someone who has audited enough Flutter apps to have…
get_it vs Riverpod vs Provider for Dependency Injection: What I Actually Ship
Three popular tools, three different philosophies, one real-world verdict from someone who has audited enough Flutter apps to have opinions.
You are building a Flutter app and you need to wire up an AuthService, an ApiClient, and a AnalyticsRepository. These things are not state. They are infrastructure. They need to live somewhere, be created once, and be reachable from anywhere. You search pub.dev and end up staring at three options: get_it, provider, and riverpod. They all solve "dependency injection" on the tin, but they are doing fundamentally different things, and conflating them causes the kind of architectural mess I spend hours untangling in other people's codebases.

Let me explain what each one actually does, then tell you what I ship.
The Key Distinction Nobody Explains Up Front
get_it is a pure service locator. It has no opinions about state. It has no widget tree. It holds objects and gives them back when you ask. That is the entire product.
Provider and Riverpod are both DI solutions that also handle state. They tie object lifetime to the widget tree (Provider literally wraps InheritedWidget). When the data changes, widgets rebuild. That is a great property for UI state, but it is the wrong tool when you just want a single HttpClient to exist for the lifetime of the app, with zero widget involvement.
Mixing these up is how you get a RepositoryProvider wrapping your AuthService which has to stay alive above MaterialApp while something three screens deep reads it with context.read() and everyone wonders why a state management primitive is being used to pass a service around.
get_it 9.x: The Service Locator
The current stable version is 9.2.1. The mental model is a global registry. You register things during startup, you pull them out anywhere you need them, no BuildContext required.
import 'package:get_it/get_it.dart';
final getIt = GetIt.instance;
// Registration (call once at startup)
void setupLocator() {
getIt.registerSingleton<ApiClient>(ApiClient(baseUrl: 'https://api.example.com'));
getIt.registerFactory<AuthService>(() => AuthService(getIt<ApiClient>()));
}
// Resolution (anywhere in your app, no context needed)
final auth = getIt<AuthService>();
registerSingleton creates the instance immediately. registerLazySingleton defers creation until first access. registerFactory creates a new instance on each call. The type system is your index, which means you get a runtime crash if you forget to register something. More on that in a moment.
On its own, get_it means writing your setup function by hand. That gets tedious at scale. The standard pairing is injectable 3.x, a code generator that reads annotations and produces the registration code for you.
// injection.dart
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'injection.config.dart'; // generated
final getIt = GetIt.instance;
@InjectableInit()
void configureDependencies() => getIt.init();
// ----
// services/api_client.dart
@singleton
class ApiClient { ... }
// services/auth_service.dart
@injectable
class AuthService {
AuthService(this._api);
final ApiClient _api;
}
Run dart run build_runner build and injectable generates the wiring. Dependencies are resolved in the right order automatically. The annotation vocabulary is small: @singleton, @lazySingleton, @injectable, @module for third-party types. It reads like what it is.
The honest caveat: there is no compile-time safety on the lookup side. If you call getIt<SomeService>() and forgot to register it, you get a runtime exception, not a compile error. injectable reduces the risk significantly because the generator handles the registrations, but a missed @injectable annotation on a new class is a runtime surprise waiting for you in QA.
Provider 6.x: InheritedWidget with Training Wheels
Provider (current: 6.1.5+1) is a convenience layer over InheritedWidget. You wrap a widget subtree with a Provider, and descendants read it via context.read() or context.watch(). The object lives as long as that subtree lives.
Provider works fine. It is not going anywhere. But its design is inseparable from the widget tree. Providing something “globally” means putting it above MaterialApp and hoping nothing in your service layer ever needs it outside of a widget context. Testing requires a widget test harness or manual mocking. Navigation-aware scoping (creating a fresh object per route) is doable but conceptually awkward.
Most teams I audit who are using Provider are using it for two distinct jobs: wiring up long-lived services (where get_it would be cleaner) and managing ephemeral UI state (where Riverpod handles better). The result is an uneven mix of ChangeNotifierProvider and RepositoryProvider with no consistent logic about which is which.
Provider is also Remi Rousselet’s own package, the same author as Riverpod. He has said explicitly that Riverpod was written to fix the limitations he could not fix in Provider without breaking changes. That tells you the direction of travel.
Riverpod 3.x: DI and State, Together
Riverpod (flutter_riverpod 3.3.1 as of writing) takes a different path: providers live outside the widget tree entirely, but they are still reactive. No BuildContext required to create or read a provider. No widget wrapping needed. Everything is compile-safe through the @riverpod annotation and codegen (riverpod_generator).
// user_repository.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'user_repository.g.dart';
@riverpod
UserRepository userRepository(Ref ref) {
return UserRepository(apiClient: ref.watch(apiClientProvider));
}
That one annotation generates a userRepositoryProvider you can read anywhere. The dependency graph is declared explicitly via ref.watch(). If you reference a provider that does not exist, the build fails. Compile-time, not runtime.
Where Riverpod really earns its keep is when the dependency is also state. An authenticated user, a selected tenant, a feature flag loaded from a remote config. With get_it you would need a separate reactive layer for that (a ValueNotifier maybe, or watch_it on top of get_it). With Riverpod, the provider is both the service and the observable value, and everything downstream updates automatically.
Riverpod 3.0 added offline persistence support and mutations as first-class concepts. The package has matured well past its “experimental” phase.
The honest caveat: the learning curve is real. Providers, AsyncNotifier, Ref, scoping, ProviderScope.overrides for testing. It takes a few days to wire your brain to the model. And if you are building something simple (a three-screen utility app), this is a lot of machinery for the job.
What I Actually Ship
Here is my honest current default, shaped by having this argument with myself on a dozen different projects.
For pure service wiring (database, API clients, analytics, push notifications, crash reporters): get_it plus injectable. It is fast to set up, plays nicely with any architecture, and the generated code is readable. The service locator pattern has a reputation for making testing hard, but injectable’s module system handles mock overrides cleanly enough in practice.
For anything where the dependency is also observable state (auth session, user preferences, remote config, per-screen data fetching): Riverpod. The explicit dependency graph and compile-time safety pay for themselves as the app grows. I have migrated three client apps from a mix of Provider and BLoC to Riverpod and the test coverage always improves because ProviderScope.overrides is genuinely easy to use.
Provider I reach for when I inherit a codebase already using it and the cost of migrating outweighs the benefit. That is not a compliment, but it is an honest assessment. It works. I just do not start new projects on it.
The uncomfortable truth: most mid-size Flutter apps benefit from having both get_it and Riverpod in the same project. They are not competing solutions. get_it wires your infrastructure; Riverpod handles your reactive dependencies. The confusion comes from thinking you have to pick one for everything.
Further reading
Muhammad Usman is a senior Flutter developer who has shipped over 50 production apps and audits Flutter codebases for teams that want honest answers about their architecture choices.
메타데이터
- post_id
- 4d6bc1a9ab65
- slug
- get-it-vs-riverpod-vs-provider-for-dependency-injection-what-i-actually-ship-4d6bc1a9ab65
- url
- https://medium.com/@ottomancoder/get-it-vs-riverpod-vs-provider-for-dependency-injection-what-i-actually-ship-4d6bc1a9ab65
- canonical_url
- https://medium.com/@ottomancoder/get-it-vs-riverpod-vs-provider-for-dependency-injection-what-i-actually-ship-4d6bc1a9ab65
- author_url
- https://medium.com/@ottomancoder
- status
- ok
- fetched_at
- 2026-06-15 20:49:13