Dependency Injection
AngularDart has a powerful dependency injection (DI) system that helps you manage services and their dependencies.
What is Dependency Injection?
DI is a design pattern where a class receives its dependencies from external sources rather than creating them itself. AngularDart's DI system provides:
- Loose coupling between components and services
- Testability by allowing mock services
- Singleton management for shared services
- Hierarchical injectors for scoped services
Creating a Service
import 'package:angulardart/angular.dart';
@Injectable()
class UserService {
List<User> _users = [];
List<User> getUsers() => _users;
void addUser(User user) {
_users.add(user);
}
Future<User?> getUserById(int id) async {
return _users.where((u) => u.id == id).firstOrNull;
}
}
Providing Services
Application-wide (root level)
Provide a service at the root level to make it a singleton across the entire app:
@Component(
selector: 'app-component',
template: '<router-outlet></router-outlet>',
providers: [ClassProvider(UserService)],
)
class AppComponent {}
Component-level
Provide a service at the component level to create a new instance for each component:
@Component(
selector: 'user-list',
template: '<ul>...</ul>',
providers: [ClassProvider(UserService)],
)
class UserListComponent {
final UserService _userService;
UserListComponent(this._userService);
}
Module-level
Group related providers:
const userServiceProviders = [
ClassProvider(UserService),
ClassProvider(AuthService),
ClassProvider(LoggerService),
];
@Component(
selector: 'app-component',
providers: [userServiceProviders],
)
Provider Types
ClassProvider
Maps a token to a class:
providers: [ClassProvider(UserService)]
ClassProvider with useClass
Maps a token to a different implementation:
providers: [
ClassProvider(BaseService, useClass: RealService),
]
ValueProvider
Provides a static value:
const API_URL = OpaqueToken<String>('apiUrl');
providers: [
ValueProvider(API_URL, 'https://api.example.com'),
]
// Usage
@Component(...)
class MyComponent {
final String _apiUrl;
MyComponent(@Inject(API_URL) this._apiUrl);
}
FactoryProvider
Creates the value using a factory function:
providers: [
FactoryProvider(Config, (injector) {
final apiUrl = injector.get(API_URL) as String;
return Config(apiUrl: apiUrl);
}),
]
ExistingProvider
Aliases one token to another:
providers: [
ExistingProvider(Logger, ConsoleLogger),
]
Injecting Services
Constructor Injection
The most common way to inject services:
@Component(
selector: 'user-list',
template: '...',
providers: [ClassProvider(UserService)],
)
class UserListComponent implements OnInit {
final UserService _userService;
List<User> users = [];
UserListComponent(this._userService);
@override
void ngOnInit() {
users = _userService.getUsers();
}
}
Optional Injection
Use @Optional() to inject a service that may not be provided:
class MyComponent {
final LoggerService? _logger;
MyComponent(@Optional() this._logger);
void doSomething() {
_logger?.log('Doing something');
}
}
Injecting with Tokens
const API_URL = OpaqueToken<String>('apiUrl');
class MyComponent {
final String _apiUrl;
MyComponent(@Inject(API_URL) this._apiUrl);
}
Hierarchical Injectors
AngularDart has a hierarchy of injectors:
- Root injector - Application-wide singletons
- Component injectors - Component-level services
- Element injectors - Template-level services
@Component(
selector: 'parent',
template: '<child></child>',
providers: [ClassProvider(UserService)],
)
class ParentComponent {}
@Component(
selector: 'child',
template: '...',
)
class ChildComponent {
// Gets the UserService from ParentComponent's injector
final UserService _userService;
ChildComponent(this._userService);
}
Resolution Order
When a component requests a dependency, AngularDart searches:
- The component's own injector
- Parent component injectors (walking up the tree)
- The root injector
Best Practices
- Provide services at the root level for singletons
- Use component-level providers for isolated state
- Use
@Optional()for optional dependencies - Use
OpaqueTokenfor configuration values - Keep services focused on a single responsibility
- Use interfaces (abstract classes) for service contracts
Testing with DI
DI makes testing easy by allowing mock services:
@Injectable()
abstract class DataService {
Future<List<Item>> getItems();
}
@Injectable()
class RealDataService implements DataService {
@override
Future<List<Item>> getItems() async {
// Real API call
}
}
@Injectable()
class MockDataService implements DataService {
@override
Future<List<Item>> getItems() async {
return [Item('Mock 1'), Item('Mock 2')];
}
}
// In tests
test('should use mock data', () {
final injector = Injector.map({
DataService: MockDataService(),
});
final service = injector.get(DataService);
// service is MockDataService
});