Learn

Getting started

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

v1.2.1Beginnerark_di

SDK security baseline

Build and distribute applications with Dart 3.13.7+ or Flutter 3.47.8+. These are the first patched stable versions for the eMudhra incident; for later builds, use a currently supported SDK with security fixes.

The pubspec minimum describes API compatibility, not the security of an older runtime. Packages were validated with Dart 3.13.7 and Flutter 3.47.8 without changing their public API. Updating a library does not repair an installed application: affected Android, Windows, and Linux applications must be rebuilt and redistributed.

Check the SDK selected by the IDE, local builds, and CI. Updating a global SDK does not ensure that a particular project or CI uses it. Do not disable certificate verification to work around TLS errors.

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.