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.
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'),
]);| Provider | What it does | Needs 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.
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 aDependencyDataBuilder. The injector resolves them first and passes them in asdeps.
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:
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.instancecaches the instance in the injector that provides it. Two child injectors that both provide the type each get their own.
class SessionState {
// One SessionState per injector that provides it.
static final Factory = InjectableFactory.withoutDependencies<SessionState>(
() => SessionState(),
scope: InjectableScope.instance,
);
static final DefaultProvider = Provider(SessionState);
}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)); // falseIn Flutter, this is how you give each screen its own state while sharing app-wide services.
Recommended convention
Give every service a static Factory and DefaultProvider, built with the InjectableFactory and Provider helpers:
class ApiClient {
static final Factory = InjectableFactory.withoutDependencies<ApiClient>(
() => ApiClient(),
);
static final DefaultProvider = Provider(ApiClient);
}Then registration never contains construction logic:
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:
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
InjectableWithDisposeInterfacegetdispose()called when their injector is disposed. - Services that use
InjectableWithReadyMixincan finish asynchronous setup after construction and callsetInjectableReadyResult(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.