Managing dependencies manually with get_it is straightforward, but as your app grows, registration code can become repetitive and error‑prone. Injectable is a code‑generator that automates dependency injection setup, working seamlessly with get_it and Bloc. This guide shows how to use injectable to write clean, maintainable DI code in your Bloc applications.
What is Injectable?
Injectable is a set of annotations and a code generator that creates the registration code for get_it. It allows you to mark classes with annotations like @injectable, @singleton, @lazySingleton, and the generator produces the registration file. This reduces boilerplate, ensures consistent registration, and makes refactoring safer.
Setup and Installation
This creates injection.config.dart in the same folder. Never edit this file manually.
Core Annotations
| Annotation | Description | get_it Equivalent |
|---|---|---|
| `@injectable` | Registers as a factory (new instance each time) | `registerFactory` |
| `@singleton` | Registers as a singleton, created eagerly | `registerSingleton` |
| `@lazySingleton` | Registers as a singleton, created on first use | `registerLazySingleton` |
| `@factoryParam` | Marks a parameter that will be passed at creation time | `registerFactoryParam` |
| `@preResolve` | For async singletons, ensures initialisation completes before registration | `registerSingletonAsync` |
Basic Usage with Bloc
Advanced Patterns
When a dependency needs parameters that are only known at runtime, use @factoryParam.
If you have multiple implementations of the same interface, use @Named.
Use @Environment to register different implementations based on the build environment (e.g., dev, prod).
For dependencies that require async initialisation (like SharedPreferences), use @preResolve with @singleton.
Testing with Injectable
Testing becomes simpler because you can easily replace dependencies with mocks. In your test file, reset getIt and register mocks before each test.
Best Practices
- Use
@lazySingletonfor most services – They are created only when needed and reused throughout the app. - Use
@injectablefor blocs – Blocs are often created per screen; factories ensure a new instance each time. - Group related registrations – Keep your DI code near the classes they register; injectable works across the whole project.
- Use named bindings for multiple implementations – Makes the intention clear and avoids conflicts.
- Leverage environments – Use
@Environmentto separate dev and prod configurations, especially for API clients and mock data. - Avoid circular dependencies – The generator will complain, but you can resolve by using
@injectable(as: ...)or redesigning. - Commit generated files – It's safe to commit
injection.config.dartto version control; it ensures consistent setup for all developers.
Common Mistakes
- ❌ Forgetting to run build_runner – Changes to annotations won't be reflected until regeneration.
- ❌ Using
@singletonfor stateful objects – State can persist across screens unexpectedly. - ❌ Not resetting getIt in tests – Causes test pollution; always call
getIt.reset()insetUportearDown. - ❌ Over‑using
@factoryParam– It's powerful but can make dependencies hard to trace. Prefer passing parameters via bloc events when possible. - ❌ Ignoring circular dependencies – Injectable will fail to generate; restructure your code to avoid circular references.
What's Next?
Now that you have a robust DI setup, you can build a modular, testable application. Next, explore how to structure your app with modular architecture and clean layers.
Next, explore Modular architecture with Bloc and Repository pattern.