ark_data_layer 0.1.0-dev.1 copy "ark_data_layer: ^0.1.0-dev.1" to clipboard
ark_data_layer: ^0.1.0-dev.1 copied to clipboard

Pure Dart foundations for repositories, data sources, and observable business data.

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.

0
likes
160
points
--
downloads

Documentation

Documentation
API reference

Publisher

verified publisherarktelos.dev

Pure Dart foundations for repositories, data sources, and observable business data.

Homepage
Repository (GitLab)
View/report issues

Topics

#architecture #data-layer #repository

License

Apache-2.0 (license)

Dependencies

meta

More

Packages that depend on ark_data_layer