Setting Up Dependency Injection (GetIt) in a Flutter App
Setting up DI in a Flutter project often turns into chaos: dependencies are initialized in random places, code becomes untestable, and after architectural changes, the app crashes on startup. We solve this problem with GetIt — a simple and reliable service locator we've used in over 30 commercial projects. Our experience shows: proper GetIt configuration reduces dependency debugging time by 3–5 times and makes the code ready for Clean Architecture. Budget savings on debugging can reach 40% — that's hundreds of hours or tens of thousands of rubles in developer salaries.
GetIt is a service locator for Dart/Flutter, the de facto standard for DI in projects where code generation isn't needed. According to the official documentation, it's the recommended approach for managing dependencies outside widgets. The principle is simple: register dependencies once at app startup, request them anywhere via GetIt.instance<T>() or the shorthand sl<T>(). We guarantee that after our setup, you'll forget about manual parameter passing through constructors.
Why GetIt Instead of Provider or Riverpod?
Provider and Riverpod are state management tools with DI as a side effect. GetIt is a pure service locator without Flutter dependencies: it can be used in the domain layer without BuildContext. For Clean Architecture, where the domain layer knows nothing about Flutter, this is crucial. Additionally, GetIt runs 2–3 times faster at app startup because it doesn't need to build a widget tree.
The GetIt constructor supports three registration modes:
-
registerSingleton<T>— creates immediately upon registration, lives for the entire app lifetime. -
registerLazySingleton<T>— creates on first access, then returns the same instance. -
registerFactory<T>— creates a new instance on every access.
We recommend using registerLazySingleton for most services (ApiService, DatabaseHelper) and registerFactory for short-lived objects (e.g., BLoCs created via GetIt).
How to Avoid the Typical Async Initialization Error?
// Wrong — synchronous registration of an async dependency sl.registerLazySingleton<DatabaseHelper>(() => DatabaseHelper()..init()); // Correct — async init via registerSingletonAsync sl.registerSingletonAsync<DatabaseHelper>(() async { final db = DatabaseHelper(); await db.init(); return db; }); // And wait for readiness before runApp: await sl.allReady(); If you don't use registerSingletonAsync + allReady(), DatabaseHelper may be requested before async initialization completes — a crash on startup with StateError: Singleton is not ready yet. In our projects, we always wrap DB and SharedPreferences initialization in this pattern.
Comparison of GetIt Registration Types
| Type | Creation Time | Number of Instances | When to Use |
|---|---|---|---|
registerSingleton |
At registration | 1 (one for the whole app) | Configurations, loggers |
registerLazySingleton |
On first access | 1 | ApiService, DatabaseHelper, repositories |
registerFactory |
On every access | Many | Use Cases, BLoCs (if created via GetIt) |
Steps to Set Up the DI System
| Step | Tasks | Duration |
|---|---|---|
| Analysis and Design | Identify 'leaky' dependencies, draw DI diagram | 3–4 hours |
| Write injection_container | Register all services, async initialization | 1–2 days |
| Integration with feature modules | Split by features for large projects | 1–2 days |
| Testing and code review | Unit tests for registrations, peer review | 1 day |
How to Build a Modular DI System for Large Projects?
On large projects, a single injection_container.dart becomes 500 lines. The solution: split by features. Each feature module registers its own dependencies via separate functions like initAuthDependencies(), initProfileDependencies() — called from the main initDependencies(). We switched to this approach after a project grew to 15 features — now DI takes 3–4 days to set up, but maintenance requires half the time.
Organizing injection_container.dart
Standard practice is one file injection_container.dart (or di/) with an initDependencies() function:
Future<void> initDependencies() async { // External final sharedPrefs = await SharedPreferences.getInstance(); sl.registerLazySingleton(() => sharedPrefs); sl.registerLazySingleton(() => http.Client()); // Data sources sl.registerLazySingleton<AuthRemoteDataSource>( () => AuthRemoteDataSourceImpl(sl()), ); // Repositories sl.registerLazySingleton<AuthRepository>( () => AuthRepositoryImpl(sl()), ); // Use cases sl.registerLazySingleton(() => LoginUseCase(sl())); // BLoCs — if created via GetIt sl.registerFactory(() => AuthBloc(loginUseCase: sl())); } Register dependencies in bottom-up order: first external dependencies, then data sources, repositories, use cases, and finally the presentation layer. This rule guarantees all dependencies are available when accessed.
What's Included in a Turnkey GetIt Setup
- Analysis of current architecture and identification of 'leaky' dependencies (about 3–4 hours).
- Design the DI layer: choose registration types for each component (2–3 hours).
- Write
injection_containerwith async initialization for DB,SharedPreferences, Firebase (1–2 days). - Integrate with feature modules (if the project is large) — additional 1–2 days.
- Write unit tests to verify registrations (mock replacement in
setUp). - Documentation on adding new dependencies (1 hour).
Process
- Analysis: meeting with the team, studying the current architecture (3–4 hours).
- Design: dependency diagram, determine lifetimes (2–3 hours).
- Implementation: write
injection_containerand tests (1–2 days). - Code review and deployment (1 day).
- Knowledge transfer: documentation and a call (1 hour).
Timelines and Cost
Estimated timeline: 2 to 5 days depending on project size. Cost is calculated individually after a code audit. Debugging time for improper DI can account for up to 30% of team time — our setup pays for itself in the first month by reducing that time. Get a consultation — contact us and we'll offer the optimal solution. Order a turnkey GetIt setup — guaranteed results.







