Core Concepts

Service Container & Dependency Injection

A powerful tool for managing class dependencies and performing dependency injection with type-safe bindings, singletons, and contextual resolution.

Core Features

Type-safe dependency registration and resolution
Singleton and factory binding patterns
Contextual binding for multi-implementation support
Lazy initialization for heavy services

Use Cases

Database connection management
Service layer architecture
Testing with mock implementations
Plugin and middleware systems

Binding Methods

Register dependencies in the container

bind()

Registers a factory function for type T with optional singleton control.

dart
// Basic factory binding (new instance each time)
container.bind<HttpClient>((c) => HttpClient());

// Singleton factory binding
container.bind<Database>((c) => Database.connect(), singleton: true);

Parameters:

  • factory: Function that creates the instance (receives container)
  • singleton: If true, same instance will be returned each time (default: false)

singleton()

Convenience method for registering a singleton binding.

dart
// Singleton database connection
container.singleton<Database>((c) => Database.connect());

// Singleton with dependencies
container.singleton<AuthService>((c) => AuthService(
  userRepo: c.resolve<UserRepository>(),
  config: c.resolve<Config>(),
));

Equivalent to bind(factory, singleton: true)

lazySingleton()

Registers a singleton that's only instantiated on first resolution.

dart
// Lazy singleton logger
container.lazySingleton<Logger>((c) => Logger(
  level: c.resolve<Config>().logLevel,
));

// Heavy service that might not be used
container.lazySingleton<ReportGenerator>((c) => ReportGenerator(
  database: c.resolve<Database>(),
));

Difference from regular singleton: Instantiation is deferred until first use

instance()

Registers an already created instance.

dart
// Pre-configured instance
final analytics = AnalyticsService(apiKey: 'key-123');
container.instance<AnalyticsService>(analytics);

// Mock instance for testing
final mockUserRepo = MockUserRepository();
container.instance<UserRepository>(mockUserRepo);

Useful for:

  • Pre-configured objects
  • Mock instances in testing
  • Objects created outside DI system

bindWhen()

Registers a contextual binding that only applies for specific context keys.

dart
// Different payment providers
container.bindWhen<PaymentGateway>(
  'stripe', 
  (c) => StripeGateway(c.resolve<Config>().stripeKey),
  singleton: true
);

container.bindWhen<PaymentGateway>(
  'paypal',
  (c) => PayPalGateway(c.resolve<Config>().paypalKey),
);

Parameters:

  • context: String key that identifies this binding
  • factory: Function that creates the instance
  • singleton: Whether to treat as singleton (default: false)

Resolution Methods

Retrieve dependencies from the container

resolve()

Resolves an instance of type T, optionally with context.

dart
// Basic resolution
final db = container.resolve<Database>();

// Contextual resolution
final paymentGateway = container.resolve<PaymentGateway>('stripe');

// With error handling
try {
  final service = container.resolve<SomeService>();
} catch (e) {
  // Handle missing binding
}

Parameters:

  • context: Optional context key for contextual bindings

Returns:

Instance of type T

Throws:

Exception if no binding found

resolveAll()

Resolves all registered instances of type T.

dart
// Get all validators
final validators = container.resolveAll<Validator>();

// Process all middleware
final middleware = container.resolveAll<Middleware>();
await Future.wait(middleware.map((m) => m.process(request)));

Useful for:

  • Plugin systems
  • Middleware pipelines
  • Event handlers

Management Methods

Control and inspect container bindings

has()

Checks if a binding exists for type T.

dart
// Check before resolving
if (container.has<FeatureService>()) {
  final feature = container.resolve<FeatureService>();
}

// Contextual check
if (container.has<PaymentGateway>('stripe')) {
  // Stripe-specific logic
}

Returns:

true if binding exists, false otherwise

unbind()

Removes the binding for type T.

dart
// Remove standard binding
container.unbind<Logger>();

// Remove contextual binding
container.unbind<PaymentGateway>('stripe');

// Testing pattern
container.unbind<Database>();
container.singleton<Database>(() => TestDatabase());

Useful for:

  • Testing scenarios
  • Runtime reconfiguration

flush()

Removes all bindings and resets the container.

dart
// Reset entire container
container.flush();

// Test setup example
void setUpTests() {
  container.flush();
  registerTestBindings();
}

Primary use case: Between tests to ensure clean state

Usage Patterns

Common implementation examples

Service Provider Example

dart
class AppServiceProvider {
  void register(ContainerInterface container) {
    // Singletons
    container.singleton<Config>((c) => Config.load());
    container.lazySingleton<Database>((c) => Database(
      config: c.resolve<Config>(),
    ));
    
    // Factories
    container.bind<HttpRequest>((c) => HttpRequest(
      client: c.resolve<HttpClient>(),
    ));
    
    // Contextual
    container.bindWhen<Cache>('redis', (c) => RedisCache());
  }
}

Contextual Binding Example

dart
// Register different storage implementations
container.bindWhen<Storage>(
  's3', 
  (c) => S3Storage(c.resolve<Config>().s3Config),
);
container.bindWhen<Storage>(
  'local', 
  (c) => LocalStorage('/storage'),
);

// Resolve based on context
final storage = isProduction
  ? container.resolve<Storage>('s3')
  : container.resolve<Storage>('local');

Best Practices

Guidelines for effective container usage

Do's

  • Use singletons for stateful services (Database, Config)
  • Lazy-load heavy services that might not be used
  • Register bindings in service providers
  • Use contextual bindings for multi-implementation support
  • Check binding existence before resolving conditionally

Don'ts

  • Don't resolve in constructors (causes circular dependencies)
  • Don't store container in instance variables
  • Don't use factory bindings for stateful services
  • Don't forget to flush container between tests
  • Don't resolve without error handling in production

On this page