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:

  1. Root injector - Application-wide singletons
  2. Component injectors - Component-level services
  3. 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:

  1. The component's own injector
  2. Parent component injectors (walking up the tree)
  3. 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 OpaqueToken for 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
});