smart_repository 0.4.1 copy "smart_repository: ^0.4.1" to clipboard
smart_repository: ^0.4.1 copied to clipboard

Policy-driven coordination for remote and local repository data.

smart_repository #

Policy-driven coordination for remote and local repository data. The package has no runtime dependencies and knows nothing about HTTP clients, databases, or state-management frameworks.

Installation #

dependencies:
  smart_repository: ^0.4.1

Quick start #

import 'package:smart_repository/smart_repository.dart';

final repository = SmartRepository<User>(
  remote: api.getUser,
  local: storage.getUser,
  saveLocal: storage.saveUser,
  deleteLocal: storage.deleteUser,
  localTimestamp: storage.getUserStoredAt,
  config: const SmartRepositoryConfig(
    defaultPolicy: RepositoryPolicy.networkFirst,
    maxAge: Duration(minutes: 15),
  ),
);

final result = await repository.get();
switch (result) {
  case RepositorySuccess(:final data, :final source, :final isStale):
    print('$data from $source (stale: $isStale)');
  case RepositoryFailure(:final error):
    print(error);
}

Callbacks are normalized internally. Larger projects may instead implement ReadDataSource<T>, WriteDataSource<T>, and DeleteDataSource and pass the corresponding *Source arguments.

Clean Architecture #

Keep package in data layer. Domain layer defines entities and repository ports; presentation depends only on domain. MappedRepository removes DTO/database mapping boilerplate without leaking either model outside data layer.

final users = MappedRepositoryFamily<int, User, UserDto, UserBox>(
  remote: api.getUser,
  local: database.getUser,
  saveLocal: database.saveUser,
  deleteLocal: database.deleteUser,
  mapRemote: (dto) => dto.toEntity(),
  mapLocal: (box) => box.toEntity(),
  mapToLocal: (entity) => entity.toBox(),
  fallbackWhen: (id, error) => error is OfflineException,
);

A data-layer implementation can adapt results to any application abstraction without this package depending on dartz or another result library:

Future<Either<Failure, User>> getUser(int id) async {
  final result = await users.get(
    id,
    policy: RepositoryPolicy.networkFirst,
  );

  return result.fold(
    onSuccess: Right.new,
    onFailure: (error, stackTrace) => Left(mapFailure(error, stackTrace)),
  );
}

For exception-based repository ports, use result.getOrThrow(). Use result.match() when source, freshness, or timestamps are needed.

After an external create/update mutation, coordinate local persistence and state emission through the same repository:

final updated = await api.updateUser(request);
await users.setLocal(id, updated.toEntity());

This package remains a data-source coordinator—not a domain repository base class, dependency-injection container, or state-management framework.

Optional generated wiring #

For applications with many data repositories, the companion smart_repository_generator package can generate the mechanical mapped repository initialization and delegation. Manual SmartRepository, MappedRepository, and family construction remains fully supported.

dev_dependencies:
  build_runner: ^2.0.0
  smart_repository_generator: ^0.4.1
part 'user_repository_impl.g.dart';

@SmartRepositoryFamilyAdapter<int, User, UserDto, UserBox>()
final class UserRepositoryImpl
    with _$UserRepositoryImpl
    implements UserRepository {
  UserRepositoryImpl(this.api, this.database);

  final UserApi api;
  final UserDatabase database;

  @override
  Future<UserDto> smartLoadRemote(int key) => api.getUser(key);

  @override
  Future<UserBox?> smartLoadLocal(int key) => database.getUser(key);

  @override
  Future<void> smartSaveLocal(int key, UserBox value) =>
      database.saveUser(value);

  @override
  User smartMapRemote(UserDto value) => value.toEntity();

  @override
  User smartMapLocal(UserBox value) => value.toEntity();

  @override
  UserBox smartMapToLocal(User value) => value.toBox();

  Future<Either<Failure, User>> getUser(int id) async =>
      (await smartGet(id)).fold(
        onSuccess: Right.new,
        onFailure: (error, stackTrace) =>
            Left(mapFailure(error, stackTrace)),
      );
}

Run dart run build_runner build. Override smartFallbackWhen or smartRepositoryConfig when application-specific fallback/freshness behavior is needed. Domain repository interfaces stay independent from this package.

Policies #

Policy Contract
networkOnly Read remote; optionally persist; fail on remote error.
cacheOnly Read local only; a miss returns RepositoryCacheMissException.
cacheFirst Return usable cache; stale/missing cache triggers remote; stale cache can be fallback.
networkFirst Read remote; allowed remote failures fall back to stale local data.
staleWhileRevalidate Return cache immediately; stale/unknown cache refreshes in background.
cacheAndNetwork Return any cache immediately and always refresh in background.

Unknown freshness is usable for cacheFirst, but refreshable for staleWhileRevalidate. Invalidation marks cached data stale.

Streams #

watch() replays current state to every new subscriber. It emits repository events only; it is not a state-management framework.

For stale-while-revalidate with stale cache, order is:

RepositoryLoading
RepositoryData(local)
RepositoryRefreshing(local)
RepositoryData(remote)

The operation result and stream state are separate models. To avoid a Dart name collision, one-shot errors use RepositoryFailure; stream errors use RepositoryFailureState.

Errors and fallback #

Errors remain generic. mapError can translate transport-specific exceptions, then fallbackWhen decides whether local fallback is safe. This prevents cases such as silently serving a cached user after an authorization failure.

final repository = SmartRepository<User>(
  remote: api.getUser,
  local: storage.getUser,
  mapError: (error, stackTrace) => parseApiError(error),
  fallbackWhen: (error) => error is OfflineFailure,
);

Concurrent remote reads are deduplicated by default. refresh(force: true) bypasses an in-flight request. Call dispose() when repository lifecycle ends.

Keyed repositories #

SmartRepositoryFamily<K, T> lazily owns one independent repository per key. Requests for the same key deduplicate; different keys run independently.

final users = SmartRepositoryFamily<int, User>(
  remote: (id) => api.getUser(id),
  local: (id) => storage.getUser(id),
  saveLocal: (id, user) => storage.saveUser(id, user),
  deleteLocal: storage.deleteUser,
);

final result = await users.get(42, policy: RepositoryPolicy.cacheFirst);
final states = users.watch(42);

await users.remove(42); // Dispose and forget only key 42.
await users.dispose();  // Dispose every active key.

Family retains created keys until remove(key) or dispose() is called. Persistent data is untouched by remove; use clear(key) to delete it.

Observability #

Extend RepositoryObserver<T> to connect logging, metrics, tracing, Sentry, or analytics without package dependencies. Hooks cover reads, local hits/misses, remote requests, fallback, persistence, invalidation, and clearing.

final repository = SmartRepository<User>(
  remote: api.getUser,
  observers: [RepositoryMetrics()],
);

Observers used by a family receive repositoryKey. Observer exceptions are isolated and never affect repository results.

Scope #

Version 0.2 focuses on single-value, basic-list, and keyed reads. Pagination, remote mutation execution, optimistic updates, and cancellation remain future additions.

Flutter example #

Full cross-platform demo lives in the example app. No backend needed:

cd example
flutter pub get
flutter run
0
likes
0
points
476
downloads

Publisher

verified publisherpinz.dev

Weekly Downloads

Policy-driven coordination for remote and local repository data.

Repository (GitHub)
View/report issues

License

unknown (license)

More

Packages that depend on smart_repository