fl-data-layer
Builds the data layer with Freezed DTOs, Retrofit clients, the storage-seam local data manager, and repositories wired through injectable
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Builds the data layer with Freezed DTOs, Retrofit clients, the storage-seam local data manager, and repositories wired through injectable
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Behavioral guidelines for Flutter base tasks: clarify ambiguity, keep changes simple and surgical, and define verifiable success criteria before coding.
Scaffolds a new feature module under apps/main/lib/presentation/modules using the bundled module generator
Reviews UI-layer changes — screens, blocs, widgets, routes — against the template's StateBase + CoreBlocBase + fl_theme conventions
Awareness index of every reusable widget in fl_ui, fl_theme, fl_media, and core's common_widget — name, one-line purpose, when to reach for it instead of writing a new one
Teaches and applies Flutter/Dart dependency injection with Injectable + GetIt, grounded in this repo's Clean Architecture and code generation conventions. Use when changing DI wiring, adding BLoCs/use cases/repositories/modules, using @Named/@preResolve/@factoryParam/env registrations, reviewing DI best practices, or setting up DI tests.
Writes unit and widget tests for blocs, repositories, and screens using bloc_test + mocktail
| name | fl-data-layer |
| description | Builds the data layer with Freezed DTOs, Retrofit clients, the storage-seam local data manager, and repositories wired through injectable |
| license | MIT |
| metadata | {"audience":"flutter-developers","framework":"flutter","pattern":"data-layer"} |
| Concern | Package | Generator output |
|---|---|---|
| DTO + value classes | freezed + freezed_annotation + json_serializable | *.freezed.dart, *.g.dart |
| REST client | retrofit + dio + retrofit_generator | *.g.dart |
| Key/value persistence | shared_preferences + flutter_secure_storage (private state behind the storage seam) | — |
| Local store (optional, on demand) | hive_ce + hive_ce_generator | *.g.dart |
| DI | injectable + injectable_generator | *.config.dart |
The shared module modules/data_source/ exposes Retrofit plumbing; per-feature clients live there or, for app-specific endpoints, under apps/main/lib/data/data_source/. Models that are shared across apps go in core/lib/data/models/. Local persistence flows through the storage seam (see below) — go through it rather than reaching for raw SharedPreferences or FlutterSecureStorage instances in presentation or feature code.
Run make gen_all after edits.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user_model.freezed.dart';
part 'user_model.g.dart';
@freezed
sealed class UserModel with _$UserModel {
const factory UserModel({
required String id,
required String name,
@JsonKey(name: 'display_name') String? displayName,
@Default(false) bool isActive,
@Default([]) List<String> roles,
@JsonKey(name: 'updated_at') DateTime? updatedAt,
}) = _UserModel;
factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json);
}
Rules:
sealed class with @freezed.@Default([]) / @Default({}).@JsonKey(name: ...) for any field whose JSON key is not exact-match camelCase.DateTime only when the API doesn't emit ISO-8601; otherwise the default converter works.enum OrderStatus {
@JsonValue('pending') pending,
@JsonValue('shipped') shipped,
@JsonValue('delivered') delivered,
}
Always pin the wire string with @JsonValue — never rely on Dart's enum name matching.
import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';
import '../models/user_model.dart';
part 'user_api_client.g.dart';
@RestApi()
abstract class UserApiClient {
factory UserApiClient(Dio dio, {String? baseUrl}) = _UserApiClient;
@GET('/api/v1/users/{id}')
Future<UserModel> getUser(@Path('id') String id);
@GET('/api/v1/users')
Future<List<UserModel>> getUsers({
@Query('page') int? page,
@Query('limit') int? limit,
});
@POST('/api/v1/users')
Future<UserModel> createUser(@Body() UserModel user);
@PUT('/api/v1/users/{id}')
Future<UserModel> updateUser(@Path('id') String id, @Body() UserModel user);
@DELETE('/api/v1/users/{id}')
Future<void> deleteUser(@Path('id') String id);
}
Base URL flows from app config (apps/main/lib/main.dart + --dart-define-from-file), not from the client.
Multipart uploads:
@Multipart
@POST('/api/v1/upload')
Future<UploadResponse> upload(
@Part() String type,
@PartFile() MultipartFile file,
);
See CONTEXT.md §Storage seam for the definition. Operational rules:
LocalDataManager and the @module bridge that exposes CoreLocalDataManager as @lazySingleton. The seam holds in-memory caches (e.g. _memCacheToken); a factory binding silently desyncs every consumer.await injector<CoreLocalDataManager>().token;) so the synchronous isAuthenticated getter is usable from GoRoute.redirect.See CONTEXT.md §Mock remote source for the definition. MockAuthRemoteSource is the shipped example: an @injectable class with no separate interface, injected directly into AuthRepositoryImpl. Downstream apps swap it by defining RetrofitAuthRemoteSource implements MockAuthRemoteSource and rebinding — the repository depending on the mock keeps compiling.
Rules when introducing one:
Future<DomainModel?> (or domain entity) per operation.bool. Callers that need to update state shouldn't have to issue a second read.Reach for the storage seam first. Only introduce a Hive box when you need typed collections beyond key/value scope. Rules:
@HiveType(typeId: …); never re-use a deleted @HiveField(n) index.Expose storage behavior through the existing DAO → repository → usecase boundaries rather than letting presentation code reach into storage directly. Prefer stable table/row operations over replacing storage infrastructure. New public DAO/repository/usecase methods should have concise Dartdoc.
When a mutation produces data the caller needs, return the updated domain result instead of returning only a success flag and forcing an immediate duplicate query.
Repositories accept the API client (or a remote source) by constructor and are the only layer feature blocs depend on.
The module-generator's repository.impl.dart template uses the DataRepository mixin, which exposes restApi (a RestApiRepository reached via DI) for shared transport calls, and wraps the call in try/on Exception so the impl can translate transport errors into domain errors before they reach the use case:
import 'package:core/core.dart';
import 'package:injectable/injectable.dart';
import '../../domain/repositories/user_repository.dart';
import '../models/user_model.dart';
@Injectable(as: UserRepository)
class UserRepositoryImpl with DataRepository implements UserRepository {
@override
Future<User> getUser(String id) async {
try {
final dto = await restApi.getUser(id);
return dto.toEntity();
} on Exception catch (error, stackTrace) {
// Wrap as NotFoundError / NetworkError / etc. so use cases can
// switch on intent instead of catching raw DioException.
Error.throwWithStackTrace(error, stackTrace);
}
}
}
Two rules the template encodes:
with DataRepository implements XRepository — DataRepository is a mixin, not a base class. Don't extend it.toEntity() mapper or equivalent). When a repository forwards a single endpoint with no mapping needed, keep it shallow — don't invent an entity layer for its own sake.The injectable graph picks up @Injectable/@LazySingleton/@Singleton automatically once make gen_all is run. For Retrofit clients, register a @module somewhere under apps/main/lib/di/:
@module
abstract class ApiModule {
@lazySingleton
UserApiClient userApiClient(Dio dio) => UserApiClient(dio);
}
sealed class with @freezed, defaults provided for collections.@JsonKey set for every non-camelCase JSON field.@JsonValue.@RestApi, @Path, @Query, @Body correctly; no hardcoded base URL.@LazySingleton (or DI module).make gen_all run; generated files staged.Dio calls directly, bypassing Retrofit.part 'foo.g.dart'; — Retrofit won't compile.SharedPreferences / FlutterSecureStorage from presentation or feature code instead of going through the storage seam.LocalDataManager / CoreLocalDataManager as @Injectable() (factory) — the in-memory token cache silently desyncs across consumers. Must be @lazySingleton.@HiveType(typeId: …) across types when the optional Hive path is used.