Dependency injection (DI) is a fundamental technique for writing testable, maintainable code. In Bloc applications, you need to inject repositories, use cases, and other dependencies into your blocs. This guide covers the most effective DI approaches for Flutter Bloc, from simple BlocProvider to advanced setups with get_it and injectable.
Why Dependency Injection Matters
- Testability – Dependencies can be mocked or replaced in tests.
- Flexibility – Swap implementations (e.g., fake API vs real API) without changing blocs.
- Separation of concerns – Blocs don't know how dependencies are created.
- Reusability – Same service instance can be shared across multiple blocs.
- Lazy initialisation – Create dependencies only when needed.
DI Options in Flutter Bloc
| Approach | Use Case | Pros | Cons |
|---|---|---|---|
| `BlocProvider` / `RepositoryProvider` | Simple apps, widget‑scoped dependencies | Built‑in, automatic disposal | Limited to widget tree, no global access |
| `get_it` (service locator) | Medium to large apps, global services | Easy to use, fast access, testable | Service locator is a pattern, not true DI |
| `injectable` + `get_it` | Large apps, code‑generated registration | Type‑safe, minimal boilerplate, great for teams | Requires code generation setup |
Approach 1: BlocProvider and RepositoryProvider
BlocProvider is not only for providing blocs – you can also use RepositoryProvider to inject repositories or services into the widget tree. This approach ties dependencies to the widget lifecycle and is ideal for simple apps.
Approach 2: get_it – The Simple Service Locator
get_it is the most popular service locator in the Flutter ecosystem. It allows you to register dependencies globally and access them anywhere, without being tied to the widget tree.
You can combine get_it with BlocProvider – use get_it to create the bloc, then provide it via BlocProvider to ensure proper disposal.
Approach 3: injectable – Code‑Generated DI
injectable is a code‑generator that works on top of get_it. It automatically creates the registration code based on annotations, reducing boilerplate and preventing registration errors.
Scoping and Lifecycle
Different dependencies have different lifetimes. Here’s how to manage scoping:
- App‑wide singletons – Use
registerSingleton(get_it) or@singleton(injectable). Examples: HTTP client, database, shared preferences. - Feature‑scoped – Use
BlocProviderto create a new bloc when entering a feature and dispose it when leaving. Combine withget_itfor other dependencies. - Screen‑scoped – Create a new instance of a bloc for a specific screen using
BlocProviderwithout registering it globally. - Transient – Use
registerFactory(get_it) or@injectablewithout singleton annotation to get a new instance each time.
Testing with Dependency Injection
DI makes testing straightforward. In your test setup, you can replace real dependencies with mocks.
Best Practices
- Prefer
get_itfor infrastructure dependencies – Repositories, services, data sources – register them as singletons. - Use
BlocProviderfor blocs – Let the widget tree manage bloc lifecycle. Useget_itto create the bloc instance if needed. - Avoid using
get_itdirectly inside widgets – Instead, usecontext.read/context.watchonBlocProviderwhere possible. Useget_itfor services not tied to the UI. - Register with proper scope – Singletons for stateless services; factories for blocs or stateful objects.
- Use
injectablefor larger projects – It enforces a consistent DI setup and reduces manual registration errors. - Reset DI container between tests – Use
getIt.reset()intearDownto avoid test pollution.
Common Mistakes
- ❌ Registering everything as a singleton – Blocs registered as singletons retain state across screens, leading to bugs. Use factories for blocs.
- ❌ Using
get_itinside the build method for state – Won't cause rebuilds; usecontext.watchorBlocBuilderinstead. - ❌ Not resetting
get_itin tests – Causes tests to interfere with each other. - ❌ Creating circular dependencies – For example, a repository depending on a bloc, and the bloc depending on the repository. Break the cycle.
- ❌ Over‑using DI for simple values – For simple parameters, constructor injection is fine; DI is overkill for every tiny object.
What's Next?
Now that your dependencies are well‑managed, explore how to structure your app with modular architecture and Clean Architecture.
Next, explore Modular architecture with Bloc and Repository pattern.