Overview
Nest Dart uses a powerful dependency injection (DI) system built on top of GetIt, enhanced with module-based access control. The DI system provides type-safe service resolution while enforcing export restrictions to maintain proper encapsulation.
Unlike plain GetIt, Nest Dart’s DI system enforces module boundaries. Services can only be accessed if they are properly exported by the providing module and imported by the consuming module.
The Locator Interface
The Locator interface is the primary way to interact with the DI container:
abstract class Locator {
/// Get a service instance
T get<T extends Object>({
String? instanceName,
dynamic param1,
dynamic param2,
});
/// Get a service instance asynchronously
Future<T> getAsync<T extends Object>({
String? instanceName,
dynamic param1,
dynamic param2,
});
/// Call method for syntactic sugar (same as get)
T call<T extends Object>({
String? instanceName,
dynamic param1,
dynamic param2,
});
/// Check if a service is registered
bool isRegistered<T extends Object>({Object? instance, String? instanceName});
}
Registering Services
Services are registered within a module’s providers method using the Locator interface:
Basic Registration
class AppModule extends Module {
@override
Future<void> providers(Locator locator) async {
// Register a singleton
locator.registerSingleton<ApiClient>(
ApiClient(baseUrl: 'https://api.example.com'),
);
// Register a lazy singleton
locator.registerLazySingleton<DatabaseService>(
() => DatabaseService(),
);
// Register a factory
locator.registerFactory<RequestLogger>(
() => RequestLogger(),
);
}
}
With Dependencies
class UserModule extends Module {
@override
List<Module> get imports => [DatabaseModule()];
@override
Future<void> providers(Locator locator) async {
// Inject dependencies using locator.get() or locator()
locator.registerLazySingleton<UserRepository>(
() => UserRepository(
database: locator<DatabaseService>(),
),
);
locator.registerLazySingleton<UserService>(
() => UserService(
repository: locator<UserRepository>(),
logger: locator<LoggerService>(),
),
);
}
}
Retrieving Services
From the Application Container
After modules are registered, retrieve services from the ApplicationContainer:
final container = ApplicationContainer();
await container.registerModule(AppModule());
// Get a service
final userService = container.get<UserService>();
// With parameters (for factory-registered services)
final logger = container.getWithParams<Logger>('UserModule');
// Async services
final config = await container.getAsync<ConfigService>();
// Check if registered
if (container.isRegistered<AuthService>()) {
final auth = container.get<AuthService>();
}
Within a Module
Use the Locator interface within lifecycle hooks:
class AppModule extends Module {
@override
Future<void> onModuleInit(Locator locator, ModuleContext context) async {
// Access services during initialization
final db = locator<DatabaseService>();
await db.connect();
final config = locator<ConfigService>();
print('App initialized with config: ${config.environment}');
}
}
Access Control and Scoping
Nest Dart enforces module boundaries through scoped containers:
Scoped GetIt Implementation
From module.dart:186-215:
class _ScopedGetIt implements Locator {
final GetIt _delegate;
final ModuleContext _context;
final Type _moduleType;
@override
T get<T extends Object>({
String? instanceName,
dynamic param1,
dynamic param2,
}) {
// Verify this module has permission to access the requested service
if (!_context.canAccess(_moduleType, T)) {
final providerModule = _context.serviceToModule[T];
throw ServiceNotExportedException(
T,
providerModule ?? Object,
_moduleType,
);
}
return _delegate.get<T>(
instanceName: instanceName,
param1: param1,
param2: param2,
);
}
}
Access Rules
A module can access a service if:
- Global Service: The service is marked as global (from root module)
- Own Service: The module itself provides the service
- Imported and Exported: The module imports another module that exports the service
From module.dart:49-75:
bool canAccess(Type requestingModule, Type serviceType) {
// Global services are always accessible
if (_globalServices.contains(serviceType)) {
return true;
}
// Find the module that provides this service
final providerModule = _serviceToModule[serviceType];
if (providerModule == null) {
return false;
}
// If the requesting module is the same as the provider, allow access
if (requestingModule == providerModule) {
return true;
}
// Check if the requesting module imports the provider module
final imports = _moduleImports[requestingModule] ?? <Type>{};
if (!imports.contains(providerModule)) {
return false;
}
// Check if the provider module exports this service
final exports = _moduleExports[providerModule] ?? <Type>{};
return exports.contains(serviceType);
}
Named Instances
Register multiple instances of the same type with different names:
class ConfigModule extends Module {
@override
Future<void> providers(Locator locator) async {
locator.registerSingleton<ApiClient>(
ApiClient(baseUrl: 'https://api.example.com'),
instanceName: 'production',
);
locator.registerSingleton<ApiClient>(
ApiClient(baseUrl: 'https://staging.example.com'),
instanceName: 'staging',
);
}
}
// Retrieve named instances
final prodApi = container.get<ApiClient>(instanceName: 'production');
final stagingApi = container.get<ApiClient>(instanceName: 'staging');
Parameterized Factories
Create services that require runtime parameters:
class LoggerModule extends Module {
@override
Future<void> providers(Locator locator) async {
locator.registerFactory<Logger>(
() => Logger(
category: locator<String>(instanceName: 'category'),
),
);
}
}
// Get with parameters
final logger = container.getWithParams<Logger>('UserService');
Async Service Resolution
For services that require asynchronous initialization:
class DatabaseModule extends Module {
@override
Future<void> providers(Locator locator) async {
// Initialize async service during module setup
final prefs = await SharedPreferences.getInstance();
locator.registerSingleton<SharedPreferences>(prefs);
}
}
// Or use getAsync for lazy async services
final database = await container.getAsync<DatabaseConnection>();
Use the providers method’s async capability to initialize services that require async setup, like SharedPreferences, database connections, or API clients.
Service Not Exported Exception
When attempting to access a service that isn’t properly exported:
class ServiceNotExportedException implements Exception {
final Type serviceType;
final Type fromModule;
final Type toModule;
@override
String toString() {
return 'ServiceNotExportedException: Service $serviceType is not exported '
'by module $fromModule and cannot be accessed by module $toModule';
}
}
Example Error Scenario
class DatabaseModule extends Module {
@override
Future<void> providers(Locator locator) async {
locator.registerLazySingleton<DatabaseService>(
() => DatabaseService(),
);
}
// DatabaseService is NOT exported
@override
List<Type> get exports => [];
}
class UserModule extends Module {
@override
List<Module> get imports => [DatabaseModule()];
@override
Future<void> providers(Locator locator) async {
// This will throw ServiceNotExportedException!
locator.registerLazySingleton<UserService>(
() => UserService(locator<DatabaseService>()),
);
}
}
Always ensure that services you want to use from another module are included in that module’s exports list.
Type Safety
The DI system is fully type-safe:
// Compile-time type checking
final userService = container.get<UserService>(); // UserService
// Runtime type verification
try {
final service = container.get<NonExistentService>();
} catch (e) {
// GetIt will throw if service isn't registered
print('Service not found: $e');
}
Best Practices
Constructor Injection: Always use constructor injection rather than service locator pattern in your classes.
// Good: Constructor injection
class UserService {
final UserRepository repository;
final LoggerService logger;
UserService({required this.repository, required this.logger});
}
// Avoid: Service locator in class
class UserService {
late final repository = locator<UserRepository>();
late final logger = locator<LoggerService>();
}
Interface-based Design: Register interfaces/abstract classes and inject concrete implementations.
abstract class IUserRepository {
Future<User> findById(String id);
}
class UserRepositoryImpl implements IUserRepository {
@override
Future<User> findById(String id) async {
// Implementation
}
}
// Register
locator.registerLazySingleton<IUserRepository>(
() => UserRepositoryImpl(),
);
Test-Friendly: The module system makes it easy to replace implementations for testing.
Next Steps
- Explore Providers to learn about different registration strategies
- Understand Exports for controlling service visibility
- Learn about Modules to organize your application
- Implement Lifecycle Hooks for service initialization