Руководства

Объектный граф на чистом Dart

Асинхронные клиенты, Repository, сервисы, дочерние переопределения и завершение по зависимостям.

v1.1.0Средний уровеньark_di

Объектный граф на чистом Dart

В этом руководстве создаётся граф приложения с асинхронно инициализируемым клиентом, Repository и сервисом. Затем добавляется дочерний Scope предпросмотра с Repository в памяти и проверяется завершение в порядке зависимостей.

Перед началом

shell
dart pub add ark_di:^1.1.0

Используются обычные классы с внедрением через конструкторы:

dart
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() {}
}

Эти классы не импортируют Ark DI. Способ создания конкретных объектов известен только корню композиции.

1. Создайте контейнер приложения

dart
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');
    },
  );
});

Регистрация описывает создание, хранение и закрытие объекта. Factory пока не выполняются: каждый Async Lazy Singleton начинает инициализацию при первом разрешении.

2. Разрешите входную зависимость приложения

dart
final service = await application.getAsync<CatalogService>();
final results = await service.search('dart');

Разрешение CatalogService создаёт зависимости по порядку:

text
AppConfig → ApiClient → CatalogRepository → CatalogService

Параллельные вызовы разделяют одну выполняющуюся инициализацию каждого Async Lazy Singleton. Неудачная попытка не кэшируется навсегда: следующий запрос может повторить её.

3. Добавьте ограниченный Scope предпросмотра

Предпросмотру нужен Repository в памяти без изменения контейнера приложения:

dart
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');

Вместе с Repository переопределяется потребляющий сервис. Сервис родительского Scope разрешает зависимости в родителе и не может захватить объект с более коротким дочерним жизненным циклом.

4. Закройте дочерний Scope до родителя

dart
await preview.close();
await application.close();

print(lifecycle);

Ожидаемый порядок:

text
[preview-service, preview-repository, service, repository, api-client]

Ark DI фиксирует рёбра зависимостей во время выполнения Factory. При закрытии он ждёт активные разрешения, затем закрывает потребителей раньше зависимостей. Ошибка одного Disposer не прерывает независимую очистку; все ошибки возвращаются в DiCloseException.

Что показывает пример

  • Контейнер настраивается один раз и не изменяется во время работы.
  • Прикладные объекты используют внедрение через конструкторы.
  • Асинхронное разрешение остаётся явным через getAsync.
  • Дочерний Scope изменяет ограниченную часть графа без изменения родителя.
  • Владение и завершение задаются регистрацией, а не угадываются по именам методов.

Полные реализации ApiClient, Remote Repository и Memory Repository доступны в исходном примере по ссылке выше.