Руководства

Создание слоя данных каталога

Доменный Repository, remote и cache DataSource, преобразование, наблюдение и завершение.

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

Создание слоя данных каталога

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

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

Установите пакет в консольный проект Dart. В примере используется источник в памяти, чтобы не скрывать архитектурные границы за HTTP-клиентом.

Терминал
shell
dart pub add ark_data_layer:^1.1.0

1. Определите доменный API

Остальная часть приложения видит доменные данные и бизнес-операции, но не DTO или клиенты источников:

dart
abstract interface class CatalogRepository {
  CatalogData get data;
  Stream<CatalogData> get stream;
  Future<void> synchronize();
}

final class CatalogData {
  const CatalogData(this.items);
  const CatalogData.empty() : items = const <CatalogItem>[];

  final List<CatalogItem> items;
}

final class CatalogItem {
  const CatalogItem({required this.id, required this.title});
  final String id;
  final String title;
}

Интерфейс может находиться в Domain-слое. Он не расширяет класс Repository пакета, поэтому потребители не зависят от Ark Data Layer.

2. Определите контракт для каждой технической задачи

DTO и контракты источников принадлежат Data-слою:

dart
abstract interface class CatalogRemoteDataSource implements DataSource {
  Future<List<CatalogItemDto>> fetchCatalog();
  Future<void> close();
}

final class CatalogItemDto {
  const CatalogItemDto({required this.id, required this.label});
  final String id;
  final String label;
}

При добавлении кэша объявите отдельный CatalogCacheDataSource. Не различайте несвязанные обязанности строковыми именами remote и cache в одном широком интерфейсе.

3. Реализуйте конкретный Repository

Обязательные источники разрешаются один раз в конструкторе. Операция преобразует технические данные до фиксации:

dart
final class CatalogRepositoryImpl extends Repository<CatalogData>
    implements CatalogRepository {
  CatalogRepositoryImpl({required super.dataSources})
    : _remote = dataSources.get<CatalogRemoteDataSource>(),
      super(initialData: const CatalogData.empty());

  final CatalogRemoteDataSource _remote;

  @override
  Future<void> synchronize() async {
    final records = await _remote.fetchCatalog();
    final items = <CatalogItem>[
      for (final record in records)
        CatalogItem(id: record.id, title: record.label),
    ];
    setData(CatalogData(List<CatalogItem>.unmodifiable(items)));
  }
}

До вызова setData подписчики продолжают видеть предыдущий полный снимок. Неудачный запрос не публикует частично преобразованный каталог.

4. Добавьте конкретный источник

dart
final class MemoryCatalogSource implements CatalogRemoteDataSource {
  MemoryCatalogSource(this._records);
  final List<CatalogItemDto> _records;
  bool _closed = false;

  @override
  Future<List<CatalogItemDto>> fetchCatalog() async {
    if (_closed) throw StateError('Catalog source is closed.');
    return List<CatalogItemDto>.unmodifiable(_records);
  }

  @override
  Future<void> close() async => _closed = true;
}

Источник отвечает только за транспорт. Он не публикует доменное состояние и не знает, какой Repository его использует.

5. Соберите, подпишитесь и запустите

dart
final source = MemoryCatalogSource(<CatalogItemDto>[
  const CatalogItemDto(id: 'ark-di', label: 'Ark DI'),
  const CatalogItemDto(id: 'usecase-forge', label: 'UseCase Forge'),
]);
final repository = CatalogRepositoryImpl(
  dataSources: DataSourceContainer(<DataSource>[source]),
);

final subscription = repository.stream.listen(
  (data) => print(data.items.map((item) => item.title).join(', ')),
);

await repository.synchronize();
await Future<void>.delayed(Duration.zero);

Новый подписчик сначала получает текущее пустое значение, а затем зафиксированный каталог. UI или UseCase может сразу отобразить текущие данные и реагировать на следующие фиксации через тот же контракт.

6. Закройте объекты по праву владения

dart
await subscription.cancel();
await repository.close();
await source.close();

Repository закрывает собственный механизм публикации. Корень композиции закрывает source, потому что создал его и может разделять между потребителями. Собственные подписки или ресурсы Repository освобождаются в closeRepository().

Итоговая структура

  • Domain-код зависит от CatalogRepository, CatalogData и CatalogItem.
  • Data-код владеет DTO, контрактами источников, преобразованием и конкретным Repository.
  • DataSourceContainer проверяет компоновку конструктора, но не становится Service Locator.
  • Одна фиксация публикует полный доменный снимок.
  • Владение жизненным циклом остаётся видимым в корне композиции.

Перед параллельными вызовами synchronize() прочитайте рецепт «Координация пересекающихся обновлений». Пакет сохраняет порядок фиксаций, но не решает за домен, какой сетевой результат остаётся актуальным.