Dependency Injection (DI) in Dart is a design pattern used to make our code more modular, testable, and maintainable.
It means instead of a class creating its own dependencies, those dependencies are provided (injected) from outside β usually through constructors, setters, or frameworks.
1. The core idea #
Normally, without DI, a class directly creates the object it depends on:
class UserService {
void fetchUser() => print('Fetching user...');
}
class UserController {
final service = UserService(); // tightly coupled
void loadUser() {
service.fetchUser();
}
}
Here, UserController depends directly on UserService.
If I later want to replace UserService (say, with a mock version for testing), I have to modify UserController. That breaks modularity.
2. With Dependency Injection #
Instead, we inject the dependency from outside:
class UserService {
void fetchUser() => print('Fetching user...');
}
class UserController {
final UserService service;
// dependency is injected via constructor
UserController(this.service);
void loadUser() {
service.fetchUser();
}
}
void main() {
final service = UserService();
final controller = UserController(service);
controller.loadUser();
}
Now, UserController does not create UserService β it just receives it.
This makes it easy to substitute a different implementation (like a mock or fake service).
3. Real-world Example (Flutter + DI Framework) #
In Flutter, I applied this pattern using the get_it package β a lightweight service locator for dependency injection.
import 'package:get_it/get_it.dart';
final getIt = GetIt.instance;
class ApiService {
void fetchData() => print('Data fetched');
}
class Repository {
final ApiService api;
Repository(this.api);
void getData() => api.fetchData();
}
void setupLocator() {
getIt.registerLazySingleton(() => ApiService());
getIt.registerFactory(() => Repository(getIt()));
}
void main() {
setupLocator();
final repo = getIt();
repo.getData();
}
Here:
- Dependencies are registered in one place (
setupLocator()). - Any class can retrieve its dependency easily.
- In testing, I can replace those dependencies with mocks β without changing the main code.
4. Where I applied this #
I used dependency injection heavily in Flutter state management (like BLoC or MVVM patterns).
For example, I registered services like AuthService, DatabaseService, and ApiClient globally and injected them into view models.
This improved testability β I could inject a mock database while unit testing.
5. Challenges faced #
Initially, I found it tricky managing object lifecycles β deciding which dependencies should be singleton (shared) vs factory (new instance each time).
Also, when dependencies depend on other dependencies (nested injections), I had to ensure correct registration order.
6. Limitations #
- Using a DI container like
get_itadds some indirection β it can make code harder to trace for newcomers. - Overusing DI can lead to over-engineering in small apps.
7. Alternatives #
If you donβt want to use a package like get_it, you can use:
- Constructor injection (simplest and cleanest)
- Provider pattern in Flutter (context-based DI)
- Riverpod, which provides compile-time safety for dependency injection
Summary #
Dependency Injection in Dart means giving a class its dependencies from outside instead of creating them internally.
It improves testability, flexibility, and maintainability β and in Flutter, itβs often implemented using frameworks like GetIt, Provider, or Riverpod.
