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.7.1

dev_dependencies:
  build_runner: ^2.0.0
  smart_repository_generator: ^0.7.1

Generator is optional. Manual SmartRepository, MappedRepository, and family construction remains supported.

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

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 with remoteResultType when 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. For Future<void> remote methods, raw value is omitted.
  • recover: receives error, stack trace, then method arguments and returns RepositoryRecovery<RemoteType>.
  • onError: receives local source, error, stack trace, then method arguments; runs a side effect while preserving original failure.
  • resultAdapter: converts RepositoryResult<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

Libraries

builder
smart_repository_generator
Code-generation implementation for smart_repository annotations.