Tutorials
Build a pure-Dart object graph
Configure async clients, repositories, services, child overrides, and dependency-ordered teardown.
Build a pure-Dart object graph
This tutorial builds an application graph with an asynchronously initialized client, a Repository, and a service. It then creates a child preview Scope with an in-memory Repository and proves that teardown follows dependency order.
Before you begin
dart pub add ark_di:^1.1.0
The example assumes ordinary constructor-injected classes:
final class AppConfig {
const AppConfig(this.apiBaseUrl);
final String apiBaseUrl;
}
abstract interface class CatalogRepository {
Future<List<String>> search(String query);
void close();
}
final class CatalogService {
CatalogService(this._repository);
final CatalogRepository _repository;
Future<List<String>> search(String query) => _repository.search(query);
void close() {}
}
None of these classes imports Ark DI. Only the Composition Root knows how concrete objects are constructed.
1. Build the application container
final lifecycle = <String>[];
final application = DiContainer.build((binder) {
binder.bindInstance<AppConfig>(
const AppConfig('https://api.example.com'),
);
binder.bindAsyncLazySingleton<ApiClient>(
(resolver) async {
final config = resolver.get<AppConfig>();
return ApiClient.connect(config.apiBaseUrl);
},
dispose: (client) {
client.close();
lifecycle.add('api-client');
},
);
binder.bindAsyncLazySingleton<CatalogRepository>(
(resolver) async =>
RemoteCatalogRepository(await resolver.getAsync<ApiClient>()),
dispose: (repository) {
repository.close();
lifecycle.add('repository');
},
);
binder.bindAsyncLazySingleton<CatalogService>(
(resolver) async =>
CatalogService(await resolver.getAsync<CatalogRepository>()),
dispose: (service) {
service.close();
lifecycle.add('service');
},
);
});
Registration states how an object is created, retained, and disposed. It does not execute the factories yet; each Async Lazy Singleton starts on first resolution.
2. Resolve the application entry point
final service = await application.getAsync<CatalogService>();
final results = await service.search('dart');
Resolving CatalogService initializes its dependencies in order:
AppConfig → ApiClient → CatalogRepository → CatalogService
Concurrent callers share the same in-flight initialization for each Async Lazy Singleton. A failed initialization is not cached permanently; a later request may retry.
3. Add a bounded preview Scope
The preview needs an in-memory Repository but must not mutate the application container:
final preview = application.createChild((binder) {
binder.bindInstance<CatalogRepository>(
MemoryCatalogRepository(<String>['Ark DI guide']),
overrideParent: true,
dispose: (repository) {
repository.close();
lifecycle.add('preview-repository');
},
);
binder.bindLazySingleton<CatalogService>(
(resolver) => CatalogService(resolver.get<CatalogRepository>()),
overrideParent: true,
dispose: (service) {
service.close();
lifecycle.add('preview-service');
},
);
});
final previewService = preview.get<CatalogService>();
final previewResults = await previewService.search('guide');
The service is overridden together with the Repository. A service already owned by the parent resolves dependencies in the parent Scope and cannot capture a shorter-lived child object.
4. Close child before parent
await preview.close();
await application.close();
print(lifecycle);
Expected order:
[preview-service, preview-repository, service, repository, api-client]
Ark DI records dependency edges while factories resolve. On close it waits for active resolutions, then disposes consumers before their dependencies. One disposer failure does not prevent unrelated cleanup; failures are returned together in DiCloseException.
What this example demonstrates
- The container is configured once and cannot be mutated by runtime code.
- Application objects still use constructor injection.
- Async resolution remains explicit through
getAsync. - A child Scope changes a bounded part of the graph without changing its parent.
- Ownership and teardown are part of each registration, not inferred from method names.
Open the source linked above for complete ApiClient, remote, and memory implementations.