smart_repository_generator 0.6.0
smart_repository_generator: ^0.6.0 copied to clipboard
Optional code generator for smart_repository data-layer adapters.
smart_repository_generator #
Optional declarative and adapter-mixin codegen for smart_repository.
Generated code owns coordination boilerplate; application code owns source
contracts, model mapping, domain errors, and mutations.
Installation #
dependencies:
smart_repository: ^0.6.0
dev_dependencies:
build_runner: ^2.0.0
smart_repository_generator: ^0.6.0
Generator is optional. Manual SmartRepository, MappedRepository, and family
construction remains supported.
Recommended Clean Architecture structure #
lib/
domain/
user.dart
user_repository.dart # No smart_repository dependency
data/
user_models.dart # UserDto, UserBox
user_sources.dart # Remote/local source ports
user_repository_impl.dart # Definition + concrete domain adapter
user_repository_impl.g.dart # Generated coordinator
Domain port stays ordinary:
abstract interface class UserRepository {
Future<User> getUser(int id);
}
Data layer declares coordination:
part 'user_repository_impl.g.dart';
User userFromRemote(UserDto value) => value.toEntity();
User userFromLocal(UserBox value) => value.toEntity();
UserBox userToLocal(User value) => value.toBox();
@GenerateSmartRepository<UserRemoteDataSource, UserLocalDataSource>()
abstract class UserRepositoryDefinition {
@CacheFirst(
key: #id,
mapRemote: #userFromRemote,
mapLocal: #userFromLocal,
mapToLocal: #userToLocal,
maxAge: Duration(minutes: 10),
)
Future<User> getUser(int id);
}
Concrete domain adapter uses generated coordinator:
final class UserRepositoryImpl implements UserRepository {
UserRepositoryImpl({required remote, required local})
: _coordinator = _$UserRepositoryDefinition(
remote: remote,
local: local,
);
final _$UserRepositoryDefinition _coordinator;
@override
Future<User> getUser(int id) async =>
(await _coordinator.getUser(id)).getOrThrow();
Future<void> dispose() => _coordinator.dispose();
}
Use fold instead of getOrThrow when domain uses Either, Result, or a
custom failure type. Full runnable version lives in the
Clean Architecture example.
Policies #
Annotate each method with one policy:
@GenerateSmartRepository<UserRemoteDataSource, UserLocalDataSource>()
abstract class UserQueries {
@NetworkFirst()
Future<User> getCurrentUser();
@CacheFirst(maxAge: Duration(minutes: 10))
Future<List<User>> getUsers();
@StaleWhileRevalidate()
Future<Profile> getProfile();
}
@RepositoryMethod() uses definition's defaultPolicy. Source method names
default to definition method name. Override with remote: #fetchUser or
local: #readUser. Local writer defaults from getUser to saveUser; override
with saveLocal: #putUser.
Mapper symbols are required only when source models differ from domain return
type. Generated constructor accepts optional fallbackWhen and mapError.
Generated coordinator exposes each core repository as <method>Repository and
provides dispose().
Methods may use positional, named, optional, and default parameters. One parameter becomes family key. Multiple parameters become generated composite record key, then expand back into original source method call:
@CacheFirst(maxAge: Duration(minutes: 10))
Future<List<User>> getUsers(
String name, {
required DateTime date,
int page = 1,
});
Every argument participates in cache identity and request deduplication. Local writer may accept value only, or same query parameters followed by value.
Sensitive one-shot operations should release their key after completion:
@NetworkOnly(
mapRemote: #sessionFromRemote,
persistRemote: false,
retainKeyedRepository: false,
)
Future<Session> login(String email, String password);
Direct contract implementations #
Use @GenerateRepositoryImplementation when separate definition plus concrete
adapter would duplicate every method. Annotated class becomes public contract;
generated _$ClassName becomes redirecting-factory target.
Class-level options supply repeated mapping, error, result, persistence, and
lifecycle behavior. remoteBody maps method parameters into API body keys.
Unannotated inherited methods automatically delegate to equally named local
source methods.
@GenerateRepositoryImplementation<AuthApi, AuthLocalSource>(
defaultPolicy: RepositoryPolicy.networkOnly,
resultType: Session,
mapRemote: #sessionFromRemote,
mapError: #mapFailure,
resultAdapter: #toEither,
persistRemote: false,
retainKeyedRepository: false,
)
abstract class AuthRepositoryImpl implements AuthRepository {
factory AuthRepositoryImpl({
required AuthApi remote,
required AuthLocalSource local,
}) = _\$AuthRepositoryImpl;
@RepositoryMethod(
remoteBody: {'email': #email, 'password': #password},
)
@override
Future<Either<Failure, Session>> login(String email, String password);
Future<void> dispose();
}
resultAdapter receives RepositoryResult<resultType>. Package stays unaware
of Either, app-specific failures, or other domain result abstractions.
Sync, CRUD, and database streams #
Use @RepositoryOperation when public result differs from remote response and
persisted value. This is common in paginated repositories where sync returns
bool, remote returns a page DTO, and local source stores entity boxes.
@GenerateRepositoryImplementation<
VehicleRemoteDataSource,
VehicleLocalDataSource
>(
mapError: #mapFailure,
resultAdapter: #toEither,
)
abstract class VehicleRepositoryImpl implements VehicleRepository {
factory VehicleRepositoryImpl({
required VehicleRemoteDataSource remote,
required VehicleLocalDataSource local,
}) = _$VehicleRepositoryImpl;
@RepositoryOperation(
resultType: bool,
remote: #getVehicles,
request: #vehicleQuery,
commit: #saveVehiclePage,
mapResult: #vehicleHasNext,
recover: #recoverVehiclePage,
keyBy: [#page],
)
Future<Either<Failure, bool>> syncVehicles({
required int page,
required int perPage,
});
@RepositoryOperation(
remote: #deleteVehicle,
request: #vehicleId,
commit: #deleteLocalVehicle,
)
Future<Either<Failure, void>> deleteVehicle({required int id});
@RepositoryLocalStream(map: #vehiclesFromBoxes)
Stream<List<Vehicle>> streamVehicles(int page);
}
Callback conventions:
request: receives repository method arguments and returns sole remote argument.mapResult: receives raw remote value followed by method arguments.commit: receives local source, raw remote value, then method arguments. ForFuture<void>remote methods, raw value is omitted.recover: receives error, stack trace, then method arguments and returnsRepositoryRecovery<RemoteType>.resultAdapter: convertsRepositoryResult<T>into application result type.keyBy: selects parameters used only for in-flight deduplication; completed keys are removed immediately.
@RepositoryLocalStream forwards local database streams and optionally maps
each emitted local value to domain value. Full compiled example:
example/lib/data/vehicle_repository_impl.dart.
Adapter mixins #
Use @SmartRepositoryAdapter<T, R, L>() for one value or
@SmartRepositoryFamilyAdapter<K, T, R, L>() for keyed values. Adapter mixins
remain useful when source selection or mapping cannot follow declarative method
conventions.
Generated mixins provide smartGet, smartRefresh, smartSetLocal,
smartWatch, invalidation, clearing, removal for families, and disposal.
Override smartFallbackWhen or smartRepositoryConfig as needed.
Generation #
Generate once:
dart run build_runner build
Regenerate continuously:
dart run build_runner watch