Руководства
Создание слоя данных каталога
Доменный Repository, remote и cache DataSource, преобразование, наблюдение и завершение.
Создание слоя данных каталога
В этом руководстве создаётся полная граница одной функции. Каталог начинает с пустых доменных данных, получает транспортные записи из Remote DataSource, преобразует их в доменные сущности, публикует новый неизменяемый снимок и завершается без передачи Repository права владения источником.
Перед началом
Установите пакет в консольный проект Dart. В примере используется источник в памяти, чтобы не скрывать архитектурные границы за HTTP-клиентом.
dart pub add ark_data_layer:^1.1.0
1. Определите доменный API
Остальная часть приложения видит доменные данные и бизнес-операции, но не DTO или клиенты источников:
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-слою:
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
Обязательные источники разрешаются один раз в конструкторе. Операция преобразует технические данные до фиксации:
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. Добавьте конкретный источник
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. Соберите, подпишитесь и запустите
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. Закройте объекты по праву владения
await subscription.cancel();
await repository.close();
await source.close();
Repository закрывает собственный механизм публикации. Корень композиции закрывает source, потому что создал его и может разделять между потребителями. Собственные подписки или ресурсы Repository освобождаются в closeRepository().
Итоговая структура
- Domain-код зависит от
CatalogRepository,CatalogDataиCatalogItem. - Data-код владеет DTO, контрактами источников, преобразованием и конкретным Repository.
DataSourceContainerпроверяет компоновку конструктора, но не становится Service Locator.- Одна фиксация публикует полный доменный снимок.
- Владение жизненным циклом остаётся видимым в корне композиции.
Перед параллельными вызовами synchronize() прочитайте рецепт «Координация пересекающихся обновлений». Пакет сохраняет порядок фиксаций, но не решает за домен, какой сетевой результат остаётся актуальным.