Ark Data Layer

Pure Dart foundations for concrete repository implementations that own current business data and coordinate typed DataSource contracts.

Ark Data Layer provides a narrow reusable base for the data layer of a Clean Architecture application. Domain code remains free to declare its own repository interfaces. A concrete data-layer implementation extends Repository<Data>, implements the domain interface, resolves its technical sources once, and publishes committed business data to interested consumers.

Русская версия

0.1.0-dev.1 is a prerelease. The core responsibilities are defined, but public API details may still change before the first stable version.

Why this package exists

Clean Architecture defines the repository boundary but intentionally leaves many runtime details open. Applications still need consistent answers to practical questions:

  • Where does the current business data live?
  • How does a repository receive several local or remote data sources?
  • When is a missing source reported?
  • How do background application processes observe committed data?
  • Who closes repositories, subscriptions, database clients, and API clients?

Ark Data Layer makes those decisions explicit without becoming an ORM, a networking library, a cache, an event store, or a dependency-injection container.

Core model

Domain layer
  └── declares a business repository interface

Data layer
  ├── declares typed DataSource contracts
  ├── implements technical sources
  └── extends Repository<Data> and implements the domain interface

Application layer
  └── reads data or subscribes to Repository.stream

Composition scope
  └── constructs and closes DataSources, repositories, and subscriptions

The public responsibilities are deliberately small:

  • DataSource marks a technical source contract.
  • DataSourceContainer stores an immutable set of already constructed sources and resolves exactly one implementation of a requested contract.
  • Repository<Data> owns the current business data and its observable lifecycle.

Installation

dependencies:
  ark_data_layer: ^0.1.0-dev.1

Import the public library:

import 'package:ark_data_layer/ark_data_layer.dart';

Define DataSource contracts

DataSource is a marker interface. Application-specific contracts extend it and expose only the technical operations needed by repositories.

abstract interface class UserRemoteDataSource implements DataSource {
  Future<List<UserDto>> fetchUsers();
}

abstract interface class UserCacheDataSource implements DataSource {
  Future<List<UserDto>> readUsers();

  Future<void> writeUsers(List<UserDto> users);
}

Use distinct contract types for distinct responsibilities. A remote source and a cache should not be selected through string qualifiers when their behavior is different.

Declare the domain repository interface

The domain interface belongs to the application, not to Ark Data Layer:

abstract interface class UserRepository {
  UsersData get data;

  Stream<UsersData> get stream;

  Future<void> refresh();
}

Domain code does not need to expose DataSourceContainer or depend on a specific HTTP, database, or storage implementation.

Implement the repository

Resolve required sources in the concrete constructor. Configuration errors then fail while the object graph is being built rather than during a business operation.

final class UserRepositoryImpl extends Repository<UsersData>
    implements UserRepository {
  UserRepositoryImpl({
    required DataSourceContainer dataSources,
  }) : _remote = dataSources.get<UserRemoteDataSource>(),
       _cache = dataSources.get<UserCacheDataSource>(),
       super(
         initialData: const UsersData.empty(),
         dataSources: dataSources,
       );

  final UserRemoteDataSource _remote;
  final UserCacheDataSource _cache;

  @override
  Future<void> refresh() async {
    final List<UserDto> records = await _remote.fetchUsers();
    await _cache.writeUsers(records);
    setData(UsersData.fromDtos(records));
  }
}

setData replaces and publishes a value. updateData performs a synchronous read-transform-commit operation:

updateData(
  (current) => current.copyWith(selectedUserId: userId),
);

Keep Data immutable. Returning a mutable collection from data would allow external code to alter repository state without a commit or notification.

Build the DataSourceContainer

The container receives already constructed objects:

final DataSourceContainer dataSources = DataSourceContainer(<DataSource>[
  HttpUserRemoteDataSource(client),
  SqliteUserCacheDataSource(database),
]);

final UserRepository repository = UserRepositoryImpl(
  dataSources: dataSources,
);

Resolution is strict:

  • no matching implementation: DataSourceNotFoundException;
  • exactly one matching implementation: returned as the requested type;
  • more than one matching implementation: AmbiguousDataSourceException.

get<T>() never silently chooses the first match. maybeGet<T>() returns null only when no source matches and still rejects ambiguity.

The container is not a general service locator. Do not place repositories, UseCases, presentation objects, or arbitrary services in it.

Observe repository data

Every repository exposes a replay-latest broadcast stream:

final StreamSubscription<UsersData> subscription = repository.stream.listen(
  (data) {
    // React to this committed value.
  },
);

Observable guarantees:

  • a new subscriber receives the current value first;
  • committed values are delivered asynchronously in commit order;
  • each explicit commit is published, even when the new value compares equal;
  • subscriptions are independent and may be paused, resumed, or cancelled;
  • the stream completes when the repository closes;
  • a subscription created after close completes without replay;
  • data remains readable after close.

The stream represents current repository state. It is not a durable domain event log and does not promise exactly-once processing. Use an event store or outbox when every transition must survive process termination and be handled exactly once.

Lifecycle and ownership

Close application bindings before the objects they connect:

await bindingSubscription.cancel();
await useCase.close();
await repository.close();
await remoteDataSource.close();
await cacheDataSource.close();

The recommended ownership order is:

  1. the composition scope constructs DataSources;
  2. it passes them through a repository-specific DataSourceContainer;
  3. a repository owns subscriptions or resources it creates itself;
  4. the container never closes its contents;
  5. Repository.close() invokes closeRepository() once and then completes the repository stream;
  6. the composition scope closes shared DataSources after repositories.

After closing starts, setData and updateData throw RepositoryClosedException. Repeated close() calls return the same Future.

UseCase Forge integration

Ark Data Layer does not depend on UseCase Forge. An application-level binding can connect both packages without coupling either core library to the other:

final subscription = repository.stream.skip(1).listen(
  (data) => useCase.add(UserRepositoryDataChanged(data)),
);

The listener should submit a Command, not invoke business processing directly. The update then follows the regular UseCase Forge admission, scheduling, cancellation, history, diagnostics, and error lifecycle.

The binding owns the subscription. A DI or composition scope creates the binding only after the repository and UseCase are ready, and cancels it before closing either object.

See the complete application integration guide.

Public API

API Responsibility
DataSource Marker for a technical source contract
DataSourceContainer Immutable, strict resolution of repository sources
Repository<Data> Current data, observable commits, and repository lifecycle
DataSourceNotFoundException Missing source configuration
AmbiguousDataSourceException Multiple sources match one requested contract
RepositoryClosedException Mutation attempted after closing starts

Documentation and project

License

Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE.

Libraries

ark_data_layer
Pure Dart foundations for repository-based data layers.