In a project with 50+ services and repositories, manually registering dependencies via GetIt becomes chaos. injection_container.dart balloons to 500+ lines, the registration order gets messed up, and during refactoring developers forget to update registrations. The result: StateError: No instance of type ApiService found at runtime. We've encountered this dozens of times and learned to solve the problem automatically. On one project, manual registration took up to 3 days of work for every new feature.
Setting up injectable — a code generator on top of GetIt — reduces manual code by 70% and eliminates registration errors. You simply write @injectable or @lazySingleton annotations, run build_runner, and it generates the injection.config.dart file. That's it — DI becomes clean and declarative. This lowers maintenance effort by up to 80%.
How injectable simplifies dependency management
Manual registration in GetIt doesn't scale: with 50+ dependencies it's easy to lose track of the order, forget to update during refactoring, and someone else's injection_container often becomes a kind of black box. injectable eliminates this manual work — add an annotation to your class, run build_runner, and the file updates automatically. Time savings: up to 2 hours per week on DI maintenance. In practice, that means developers spend less time on routine tasks and make fewer mistakes.
How to set up injectable in a project
Basic setup takes 10-15 minutes. Add dependencies to pubspec.yaml:
dependencies: get_it: ^7.6.0 injectable: ^2.3.0 dev_dependencies: injectable_generator: ^2.4.0 build_runner: ^2.4.0 Create an entry point — a file with an initialization function:
import 'package:get_it/get_it.dart'; import 'package:injectable/injectable.dart'; import 'injection.config.dart'; final sl = GetIt.instance; @InjectableInit() Future<void> configureDependencies() => sl.init(); Annotate your service, for example:
@lazySingleton class ApiService { final Dio _dio; ApiService(this._dio); } Run generation: dart run build_runner build --delete-conflicting-outputs. For continuous generation use build_runner watch — it automatically updates code on every annotation change.
How to work with environments
injectable supports @Environment annotations — built-in support for dev, staging, production without manual conditionals. Example:
@dev @LazySingleton(as: ApiService) class MockApiService implements ApiService { ... } @prod @LazySingleton(as: ApiService) class RealApiService implements ApiService { ... } // Initialization with environment: await configureDependencies(environment: Environment.prod); In tests pass Environment.dev — mocks are automatically wired. No if statements in the injection container.
How to handle async dependencies
For asynchronous initialization use @factoryMethod:
@singleton class DatabaseService { late final Database _db; @factoryMethod static Future<DatabaseService> create() async { final service = DatabaseService(); service._db = await openDatabase('app.db'); return service; } } injectable generates registerSingletonAsync, and configureDependencies() returns a Future. Don't forget to await before runApp.
Why does the 'No instance of type' error occur?
The most common reason: a developer added a new annotated class but forgot to run build_runner. The app compiles with the old injection.config.dart, leaving the new class unregistered. Solution: include build_runner watch in your development workflow or set up a CI step to verify the generated code is up to date.
Tip for faster development
Run `build_runner watch` in a separate terminal. It automatically regenerates code when files with annotations change. This saves time and prevents errors.Comparison: manual GetIt vs Injectable
| Parameter | Manual GetIt | Injectable |
|---|---|---|
| Code volume | 1 file with 300+ lines | ~10 lines + annotations |
| Risk of registration errors | High (order forgotten) | Low (code generation) |
| Environment support | Manual logic | Built-in via @Environment |
| Refactoring | Need to edit injection_container |
Just change the annotation |
| Testing | Additional manual mocks | Easy environment switching |
Typical DI errors and their solutions
| Error | Cause | Solution |
|---|---|---|
| No instance of type | build_runner not run | Run build_runner or watch |
| Duplicate registration | Same type registered twice | Use @singleton or @lazySingleton |
| Circular dependency | Wrong architecture | Split services, use factories |
| Environment mismatch | Wrong environment | Specify Environment during initialization |
How we set up DI in your project
Dependency Injection setup is part of our standard Flutter project architecture. We handle it turnkey: install and configure dependencies (get_it, injectable, build_runner), create an entry point with environment support, annotate all services, repositories, and factories, set up async dependencies and factory methods, integrate code generation into CI/CD (automatic build_runner run), migrate existing manual injection containers to annotations, and document the DI architecture. After setup, your team gets a clear structure that's easy to scale when adding new features. A new developer can understand the DI layer in 30 minutes instead of several hours.
Our experience: 5+ years of Flutter development, over 30 completed mobile projects. We guarantee a stable and easily extensible DI layer. Contact us to discuss your task and get a consultation on your app's architecture. Order DI setup — we'll find the optimal solution for your project.
Work takes 1-3 days depending on project size. For a new project it's faster; for an existing one with manual DI it takes longer due to migration and verification of registration order. Result: clean, scalable DI architecture. We'll evaluate your project for free — just reach out.
Official Injectable documentation: pub.dev/packages/injectable







