Skip to main content

smart_repository_generator 0.8.0 copy "smart_repository_generator: ^0.8.0" to clipboard
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.

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 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
0
likes
140
points
67
downloads

Documentation

API reference

Publisher

verified publisherpinz.dev

Weekly Downloads

Optional code generator for smart_repository data-layer adapters.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

analyzer, build, smart_repository, source_gen

More

Packages that depend on smart_repository_generator