smart_repository_generator 0.8.0
smart_repository_generator: ^0.8.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.8.0
dev_dependencies:
build_runner: ^2.0.0
smart_repository_generator: ^0.8.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_binding.dart # Source-routing recipe
user_repository_binding.g.dart # Generated implementation
Domain port stays ordinary:
abstract interface class UserRepository {
Future<AppResult<User>> getUser(int id);
Future<AppResult<List<User>>> getUsers({String? name});
}
Data layer declares only source routing. Contract method signatures are not repeated:
part 'user_repository_binding.g.dart';
User userFromRemote(UserDto value) => value.toEntity();
User userFromLocal(UserBox value) => value.toEntity();
UserBox userToLocal(User value) => value.toBox();
List<User> usersFromRemote(List<UserDto> values) =>
values.map((value) => value.toEntity()).toList();
@GenerateRepository(
contract: UserRepository,
remote: UserRemoteDataSource,
cache: UserLocalDataSource,
mapError: #mapFailure,
resultAdapter: #adaptResult,
methods: {
#getUser: RepoMethod.networkFirst(
mapRemote: #userFromRemote,
mapLocal: #userFromLocal,
mapToLocal: #userToLocal,
maxAge: Duration(minutes: 10),
),
#getUsers: RepoMethod.remoteOnly(
remote: #searchUsers,
mapRemote: #usersFromRemote,
),
},
)
abstract class UserRepositoryBinding {}
Generator emits UserRepositoryImpl, including constructor, fields, method
signatures, coordination, result adaptation, and matching cache delegates:
final repository = UserRepositoryImpl(remote: api, cache: database);
Each configured contract method accepts RepositoryMethod,
RepositoryOperation, or RepositoryLocalStream. Read and operation success
types are inferred from the last generic argument of Future<Result<..., T>>,
supporting both AppResult<T> and Either<Failure, T>. 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 #
Prefer value recipes for conventional page/CRUD repositories. They generate
request construction, mapping, persistence, Either adaptation, and error
cleanup without top-level helper callbacks:
@GenerateRepository(
contract: VehicleRepository,
remote: VehicleRemoteDataSource,
cache: VehicleLocalDataSource,
mapError: toFailure,
methods: {
#syncVehicles: RepositoryOperation.pageSync(
remote: #getVehicles,
query: {
'page': RepositoryArgument(#page),
'page_size': RepositoryArgument(#perPage),
'type': 'vehicle',
},
items: #results,
itemToLocal: #toBox,
saveLocal: #saveVehicleList,
page: #page,
pageSize: #perPage,
clearOn: RepositoryHttpError(DioException, statusCode: 404),
keyBy: [#page],
),
#addVehicle: RepositoryOperation.create(
body: {'name': RepositoryArgument(#name)},
itemToLocal: #toBox,
saveLocal: #saveVehicleList,
),
#streamVehicles: RepositoryLocalStream(mapEach: #toEntity),
},
)
abstract class VehicleRepositoryBinding {}
RepositoryArgument(..., omitIfNull: true) produces conditional map entries.
update forwards configured arguments before its generated body; delete
forwards configured arguments to both remote and local methods. Callback-based
RepositoryOperation() remains available for exceptional workflows.
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.remoteCall: receives remote source followed by repository arguments. Use withremoteResultTypewhen remote invocation needs multiple transformed arguments.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>.onError: receives local source, error, stack trace, then method arguments; runs a side effect while preserving original failure.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