Imported from khanhhtapcode/HealthCare (
.agents/AGENTS.md). Install upstream withnpx skills add khanhhtapcode/HealthCare --skill .agents. Copyright stays with the author.
Quy tắc phát triển cho dự án Healthcare (Flutter)
[!IMPORTANT] BẮT BUỘC ĐỌC TRƯỚC KHI CODE TÍNH NĂNG MỚI HOẶC REFACTOR.
File này là điểm vào chung cho mọi agent. Nó chỉ giữ quy tắc nghiệp vụ và bản đồ codebase; quy tắc vận hành (lệnh, lint, test, các bất biến không được phá) nằm ở CLAUDE.md để tránh hai file rule trôi lệch nhau.
1. Tài liệu bắt buộc đọc
| File | Nội dung |
|---|---|
| CLAUDE.md | Canonical. Lệnh, ranh giới tầng, hợp đồng xử lý lỗi, 11 bất biến không được phá, quy ước UI/test/lint. |
| FLUTTER_API_GUIDE.md | Hợp đồng API: endpoint, enum wire format, shape response và lỗi, quy tắc PATCH, rate limit. |
| ARCHITECTURE.md | Vai trò từng package trong tech stack, luồng code chi tiết, cách xử lý API. |
2. Bản đồ codebase
Kiến trúc là 3 tầng Clean Architecture (presentation → domain ← data), cộng core/ cắt ngang. Phụ thuộc chỉ đi vào trong. Chi tiết ranh giới import xem CLAUDE.md.
lib/core/ dùng chung mọi tầng
error/ ApiException — điểm chuyển DioException → lỗi của app
network/ DioClient, AuthInterceptor, TokenStorage, SessionEvents
router/ app_router.dart — go_router + redirect guard theo auth state
theme/ app_theme.dart, app_tokens.dart, metric_style.dart
utils/ constants.dart (ApiConstants, MetricType), formatters.dart, validators.dart
lib/domain/ Dart thuần. Không import dio / flutter / riverpod.
entities/ HealthMetric, UserEntity, AuthSession, MetricsPage (@freezed hoặc class thường)
repositories/ abstract class — trả Either<ApiException, T>
lib/data/ nói chuyện với API
datasources/ dùng Dio, parse JSON, THROW ApiException
repositories/ *Impl — BẮT exception, đổi thành Either
lib/presentation/
providers/ Riverpod codegen (@riverpod / @Riverpod(keepAlive: true))
pages/ screen
widgets/ widget tái sử dụng
[!NOTE] Không có
lib/data/models/. Project không dùng lớpHealthMetricModel/UserModelriêng — entity trongdomain/entities/được parse trực tiếp bằngjson_serializable. Đừng tạo lớp model trung gian.
State management là Riverpod codegen, không phải StateNotifierProvider viết tay:
@riverpod
class HealthMetricsNotifier extends _$HealthMetricsNotifier { ... }
@Riverpod(keepAlive: true)
HealthRepository healthRepository(HealthRepositoryRef ref) { ... } // đây cũng là DI container
Sửa class @freezed / provider @riverpod / @JsonValue thì phải chạy lại dart run build_runner build --delete-conflicting-outputs.
3. Quy tắc nghiệp vụ bắt buộc đối với HealthMetric
3.1 Enum MetricType
Chỉ gồm 5 giá trị, và chuỗi gửi/nhận qua API phải viết hoa có gạch dưới:
| Wire value (API) | MetricType |
Đơn vị mặc định |
|---|---|---|
WEIGHT |
weight |
kg (hoặc lb) |
HEART_RATE |
heartRate |
bpm |
BLOOD_PRESSURE |
bloodPressure |
mmHg |
STEPS |
steps |
steps |
SLEEP_HOURS |
sleepHours |
h |
Enum trong constants.dart phải giữ @JsonValue trên từng giá trị. Thiếu annotation thì json_serializable sinh map theo tên Dart (heartRate) và HealthMetric.fromJson throw với mọi response thật.
Code lạ (loại chỉ số API thêm sau này) → MetricType.tryFromCode trả null và data source bỏ qua bản ghi đó. Tuyệt đối không gán mặc định về một loại nào — đó là relabel dữ liệu sai.
3.2 Ràng buộc BLOOD_PRESSURE
- Bắt buộc có cả
value(tâm thu / systolic) vàvalueSecondary(tâm trương / diastolic). valuePHẢI LỚN HƠNvalueSecondary.- Với PATCH, server kiểm tra luật này trên bản ghi sau khi merge, không phải riêng field bạn gửi. Ví dụ bản ghi đang là 120/80, PATCH chỉ
value: 70sẽ bị từ chối.
3.3 Ràng buộc các loại còn lại
WEIGHT, HEART_RATE, STEPS, SLEEP_HOURS: valueSecondary phải là null. Gửi kèm giá trị khác null → 400 VALIDATION_ERROR.
3.4 Validate ở đâu
| Nơi | Việc |
|---|---|
| Form validator (widget) | Phản hồi tức thì cho người dùng — kể cả luật tâm thu > tâm trương |
| Data source | Chỉ ràng buộc shape mà API bắt buộc: non-BP thì valueSecondary = null |
| Server | Quyền quyết định cuối cùng |
Đừng nhân bản luật nghiệp vụ vào data source. Client validate để UX tốt; server validate để đúng. Luật huyết áp từng bị kiểm tra ở cả sheet và data source — bản ở data source đã bỏ vì trùng lặp và vì client không biết trạng thái sau merge.
4. Authentication
- Mọi request cần auth mang header
Authorization: Bearer <accessToken>. Việc gắn header do AuthInterceptor làm — data source không được tự đọc token. - Token lưu qua
flutter_secure_storage, truy cập qua TokenStorage. Không dùngshared_preferencescho token. - Gặp
401→ interceptor tự gọiPOST /api/auth/refresh, lưu cả hai token mới (server rotate cả refresh token), rồi replay request gốc. - Refresh thất bại → xóa token và phát
SessionEvent.expired;authProviderlắng nghe và chuyển sangUnauthenticated, router tự đẩy về/login. - Không có endpoint logout. Đăng xuất = xóa token. Server không thể revoke token cũ trước hạn.
- Lỗi tập trung qua
ApiException, khớp shape server{ error: { code, message, details? } }.
[!WARNING] Cơ chế refresh có 4 bất biến dễ phá (single-flight, client riêng cho
/refresh, cờ retry-once, phátSessionEvent). Đọc mục "Bất biến không được phá" trong CLAUDE.md trước khi sửa file này. Mỗi bất biến đều có test trong test/core/auth_interceptor_test.dart.
5. Dữ liệu và UI
5.1 Không bao giờ bịa dữ liệu
Repository phải trả Left(ApiException) khi lỗi. Không fallback sang dữ liệu mẫu, không dữ liệu mock trong tầng data. Đây là app y tế: một chỉ số bịa hiển thị như chỉ số thật là sai nghiêm trọng, và mọi lỗi bị nuốt sẽ che luôn bug thật.
UI phải xử lý đủ bốn trạng thái, dùng widget có sẵn: MetricListSkeleton (loading), danh sách (có dữ liệu), EmptyState (rỗng), ErrorBanner (lỗi).
5.2 UI
- State:
flutter_riverpod+ codegen.watchtrongbuild,readtrong callback. - Màu/khoảng cách/bo góc lấy từ
Theme.of(context).colorSchemevàAppSpacing/AppRadius. Không hardcodeColors.grey, không tự nhánhisDark ? ... : ...trong widget. - Icon và màu của metric lấy từ extension
MetricType.icon/.colorOf(context)/.tintOf(context)trong metric_style.dart. - Font
Outfitquagoogle_fonts; icon dùnglucide_icons_flutter(không trộnIcons.*); toast dùngtoastification. - Hỗ trợ cả Light và Dark Mode — mọi màu accent có tông riêng cho từng brightness để đạt contrast AA.
- Chuỗi UI tiếng Việt; format số/ngày qua
Formatters(localevi_VN: nghìn., thập phân,). - Hành động phá hủy phải xác nhận qua
showConfirmDialog.
Chi tiết các bẫy về animation, ListTile/Material, và text overflow: xem CLAUDE.md.