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.1is 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:
DataSourcemarks a technical source contract.DataSourceContainerstores 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;
dataremains 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:
- the composition scope constructs DataSources;
- it passes them through a repository-specific
DataSourceContainer; - a repository owns subscriptions or resources it creates itself;
- the container never closes its contents;
Repository.close()invokescloseRepository()once and then completes the repository stream;- 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
- Runnable example
- Architecture and boundaries
- Stream and lifecycle contract
- UseCase Forge integration
- Changelog
- Contributing
- Security policy
- GitLab work items
- Verified ArkTelos publisher
- ArkTelos
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.