Learn

Getting started

Configure, resolve, and close a small Ark DI container.

v1.1.0Beginnerark_di

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

Terminal
shell
dart pub add ark_di
dart
import 'package:ark_di/ark_di.dart';

2. Keep application objects unaware of the container

Dependencies remain ordinary constructor parameters:

dart
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

dart
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:

RegistrationWhat happens
bindInstanceStores the supplied AppConfig; the container owns the registration by default.
bindLazySingletonCreates one ApiClient on first resolution, caches it, and invokes its disposer during close.
bindFactoryCreates 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

dart
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

dart
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.