Skip to content

Core concepts ​

Complete Dependency Injector uses three building blocks: providers, factories and injectors. If you've used Angular's injector, the model will feel familiar.

Injectors ​

An Injector holds a list of providers and caches the instances it creates. Injectors can have a parent. When an injector doesn't provide something itself, it asks its parent, and so on up to the root.

Providers: what is available ​

A provider maps a token to a way of getting a value. Usually the token is a class type. Services carry their usual provider as a static DefaultProvider (see the convention below); one-off values are written inline.

dart
final root = Injector([
  // A type built by its registered factory.
  ApiClient.DefaultProvider,
  // An interface backed by an implementation.
  Clock.DefaultProvider,
  // A ready-made value. No factory needed.
  Provider.value(ApiConfig, ApiConfig('https://api.example.com')),
  // A value under a string token, for non-class settings.
  Provider.value(apiBaseUrl, 'https://api.example.com'),
]);
ProviderWhat it doesNeeds a factory?
Provider(T)Build T with its registered factory.Yes
Provider.value(token, value)Always return value.No
Provider.existing(token, other)Return whatever other resolves to, so two tokens share one instance.No
Provider.factory(token, fromDerived: Impl)Build Impl with its factory and return it when token is asked for. Good for interfaces.Yes, for Impl

Every provider also accepts aliasTokens, a list of extra tokens that resolve to the same instance.

Factories: how to build it ​

A factory describes how to construct one type: what it depends on, and the function that builds it. Factories are registered once and shared by every injector in the hierarchy.

dart
root.registerFactories([
  ApiClient.Factory,
  SystemClock.Factory,
  SessionState.Factory,
]);

Each service exposes its factory as a static Factory, so registration is just a list.

  • InjectableFactory.withoutDependencies<T>(() => ...) is for types with no dependencies.
  • InjectableFactory.withDependencies<T>(dependencyData, (deps) => ...) declares dependencies with a DependencyDataBuilder. The injector resolves them first and passes them in as deps.

When a service needs asynchronous setup, give it InjectableWithReadyMixin. The injector waits for it to be ready before handing it out or building anything that depends on it. See Async services.

Tokens ​

When a type alone isn't enough, for example a String setting, use an InjectorToken:

dart
final apiBaseUrl = InjectorToken.fromString('apiBaseUrl');

Then provide it with Provider.value(apiBaseUrl, '...') and read it with injector.getService<String>(apiBaseUrl).

Scopes: where instances live ​

Each factory has an InjectableScope:

  • InjectableScope.root (the default) caches the instance in the root injector, so the whole app shares one.
  • InjectableScope.instance caches the instance in the injector that provides it. Two child injectors that both provide the type each get their own.
dart
class SessionState {
  // One SessionState per injector that provides it.
  static final Factory = InjectableFactory.withoutDependencies<SessionState>(
    () => SessionState(),
    scope: InjectableScope.instance,
  );
  static final DefaultProvider = Provider(SessionState);
}
dart
final screenA = Injector([SessionState.DefaultProvider], parent: root);
final screenB = Injector([SessionState.DefaultProvider], parent: root);

// Both children share the root's ApiClient...
final clientA = await screenA.getService<ApiClient>();
final clientB = await screenB.getService<ApiClient>();
print(identical(clientA, clientB)); // true

// ...but each has its own SessionState.
final sessionA = await screenA.getService<SessionState>();
final sessionB = await screenB.getService<SessionState>();
print(identical(sessionA, sessionB)); // false

In Flutter, this is how you give each screen its own state while sharing app-wide services.

Give every service a static Factory and DefaultProvider, built with the InjectableFactory and Provider helpers:

dart
class ApiClient {
  static final Factory = InjectableFactory.withoutDependencies<ApiClient>(
    () => ApiClient(),
  );
  static final DefaultProvider = Provider(ApiClient);
}

Then registration never contains construction logic:

dart
injector.registerFactories([ApiClient.Factory, SessionState.Factory]);
final child = Injector([SessionState.DefaultProvider], parent: injector);

This keeps everything about a service in one file. For an interface, put the DefaultProvider on the interface and the Factory on the implementation:

dart
abstract class Clock {
  // Ask for a Clock, get a SystemClock.
  static final DefaultProvider = Provider.factory(Clock, fromDerived: SystemClock);

  DateTime now();
}

class SystemClock implements Clock {
  static final Factory = InjectableFactory.withoutDependencies<SystemClock>(
    () => SystemClock(),
  );

  @override
  DateTime now() => DateTime.now();
}

The member is called DefaultProvider rather than Provider because a static named Provider would hide the Provider class inside the service. "Default" also signals that a different provider, such as a Provider.value fake in a test, can replace it.

Lifecycle ​

  • Services that implement InjectableWithDisposeInterface get dispose() called when their injector is disposed.
  • Services that use InjectableWithReadyMixin can finish asynchronous setup after construction and call setInjectableReadyResult(true) when ready. Dependents wait until then.

Debug options ​

InjectorDebugOptions controls how strict the injector is, for example whether a factory can be registered twice. The defaults are strict and suit most apps.

Released under the MIT License.