Learn
Getting started
Configure, resolve, and close a small Ark DI container.
Getting started
This page builds a small object graph with three different lifetimes. The
important result is not merely obtaining CatalogService: it is knowing which
scope creates, retains, and closes every object involved.
1. Add the package
dart pub add ark_di
import 'package:ark_di/ark_di.dart';
2. Keep application objects unaware of the container
Dependencies remain ordinary constructor parameters:
final class AppConfig {
const AppConfig(this.apiBaseUrl);
final Uri apiBaseUrl;
}
final class ApiClient {
ApiClient(this.config);
final AppConfig config;
Future<void> close() async {}
}
final class CatalogService {
CatalogService(this.client);
final ApiClient client;
}
None of these classes imports Ark DI or asks a global service locator for its dependencies. Only the application composition root knows how the graph is built.
3. Configure the graph once
final AppConfig config = AppConfig(Uri.parse('https://api.example.com'));
final DiContainer container = DiContainer.build((binder) {
binder.bindInstance<AppConfig>(config);
binder.bindLazySingleton<ApiClient>(
(resolver) => ApiClient(resolver.get<AppConfig>()),
dispose: (client) => client.close(),
);
binder.bindFactory<CatalogService>(
(resolver) => CatalogService(resolver.get<ApiClient>()),
);
});
Each registration expresses a different lifecycle:
| Registration | What happens |
|---|---|
bindInstance | Stores the supplied AppConfig; the container owns the registration by default. |
bindLazySingleton | Creates one ApiClient on first resolution, caches it, and invokes its disposer during close. |
bindFactory | Creates a new CatalogService for every resolution and does not retain it. |
The configuration callback is one-shot. DiBinder expires when it returns, so
runtime code cannot silently mutate registrations.
4. Resolve from the composition boundary
final CatalogService first = container.get<CatalogService>();
final CatalogService second = container.get<CatalogService>();
assert(!identical(first, second));
assert(identical(first.client, second.client));
The services are transient, while both receive the same container-owned
client. Application code can now pass first to the feature that needs it
through an ordinary constructor.
5. Close the owner
await container.close();
Closing the container waits for active resolutions, closes child scopes, and disposes owned cached objects in dependency order. It does not dispose factory results because it never retained them; the consumer owns any resources held by a transient result.
Next, read the composition-root guide before introducing child scopes or asynchronous registrations.