- Removed data_connect package from mobile pubspec.yaml. - Added documentation for V2 profile migration status and QA findings. - Implemented new session management with ClientSessionStore and StaffSessionStore. - Created V2SessionService for handling user sessions via the V2 API. - Developed use cases for cancelling late worker assignments and submitting worker reviews. - Added arguments and use cases for payment chart retrieval and profile completion checks. - Implemented repository interfaces and their implementations for staff main and profile features. - Ensured proper error handling and validation in use cases.
648 lines
19 KiB
Markdown
648 lines
19 KiB
Markdown
---
|
|
name: krow-mobile-development-rules
|
|
description: Enforce KROW mobile app development standards including file structure, naming conventions, logic placement boundaries, localization, V2 REST API integration, and prototype migration rules. Use this skill whenever working on KROW Flutter mobile features, creating new packages, implementing BLoCs, integrating with backend, or migrating from prototypes. Critical for maintaining clean architecture and preventing architectural degradation.
|
|
---
|
|
|
|
# KROW Mobile Development Rules
|
|
|
|
These rules are **NON-NEGOTIABLE** enforcement guidelines for the KROW mobile application. They prevent architectural degradation and ensure consistency across the codebase.
|
|
|
|
## When to Use This Skill
|
|
|
|
- Creating new mobile features or packages
|
|
- Implementing BLoCs, Use Cases, or Repositories
|
|
- Integrating with V2 REST API backend
|
|
- Migrating code from prototypes
|
|
- Reviewing mobile code for compliance
|
|
- Setting up new feature modules
|
|
- Handling user sessions and authentication
|
|
- Implementing navigation flows
|
|
|
|
## 1. File Creation & Package Structure
|
|
|
|
### Feature-First Packaging
|
|
|
|
**✅ DO:**
|
|
- Create new features as independent packages:
|
|
```
|
|
apps/mobile/packages/features/<app_name>/<feature_name>/
|
|
├── lib/
|
|
│ ├── src/
|
|
│ │ ├── domain/
|
|
│ │ │ ├── repositories/
|
|
│ │ │ └── usecases/
|
|
│ │ ├── data/
|
|
│ │ │ └── repositories_impl/
|
|
│ │ └── presentation/
|
|
│ │ ├── blocs/
|
|
│ │ ├── pages/
|
|
│ │ └── widgets/
|
|
│ └── <feature_name>.dart # Barrel file
|
|
└── pubspec.yaml
|
|
```
|
|
|
|
**❌ DON'T:**
|
|
- Add features to `apps/mobile/packages/core` directly
|
|
- Create files in app directories (`apps/mobile/apps/client/` or `apps/mobile/apps/staff/`)
|
|
- Create cross-feature or cross-app dependencies (features must not import other features)
|
|
|
|
### Path Conventions (Strict)
|
|
|
|
Follow these exact paths:
|
|
|
|
| Layer | Path Pattern | Example |
|
|
|-------|-------------|---------|
|
|
| **Entities** | `apps/mobile/packages/domain/lib/src/entities/<entity>.dart` | `user.dart`, `shift.dart` |
|
|
| **Repository Interface** | `.../features/<app>/<feature>/lib/src/domain/repositories/<name>_repository_interface.dart` | `auth_repository_interface.dart` |
|
|
| **Repository Impl** | `.../features/<app>/<feature>/lib/src/data/repositories_impl/<name>_repository_impl.dart` | `auth_repository_impl.dart` |
|
|
| **Use Cases** | `.../features/<app>/<feature>/lib/src/application/<name>_usecase.dart` | `login_usecase.dart` |
|
|
| **BLoCs** | `.../features/<app>/<feature>/lib/src/presentation/blocs/<name>_bloc.dart` | `auth_bloc.dart` |
|
|
| **Pages** | `.../features/<app>/<feature>/lib/src/presentation/pages/<name>_page.dart` | `login_page.dart` |
|
|
| **Widgets** | `.../features/<app>/<feature>/lib/src/presentation/widgets/<name>_widget.dart` | `password_field.dart` |
|
|
|
|
### Barrel Files
|
|
|
|
**✅ DO:**
|
|
```dart
|
|
// lib/auth_feature.dart
|
|
export 'src/presentation/pages/login_page.dart';
|
|
export 'src/domain/repositories/auth_repository_interface.dart';
|
|
// Only export PUBLIC API
|
|
```
|
|
|
|
**❌ DON'T:**
|
|
```dart
|
|
// Don't export internal implementation details
|
|
export 'src/data/repositories_impl/auth_repository_impl.dart';
|
|
export 'src/presentation/blocs/auth_bloc.dart';
|
|
```
|
|
|
|
## 2. Naming Conventions (Dart Standard)
|
|
|
|
| Type | Convention | Example | File Name |
|
|
|------|-----------|---------|-----------|
|
|
| **Files** | `snake_case` | `user_profile_page.dart` | - |
|
|
| **Classes** | `PascalCase` | `UserProfilePage` | - |
|
|
| **Variables** | `camelCase` | `userProfile` | - |
|
|
| **Interfaces** | End with `Interface` | `AuthRepositoryInterface` | `auth_repository_interface.dart` |
|
|
| **Implementations** | End with `Impl` | `AuthRepositoryImpl` | `auth_repository_impl.dart` |
|
|
| **BLoCs** | End with `Bloc` or `Cubit` | `AuthBloc`, `ProfileCubit` | `auth_bloc.dart` |
|
|
| **Use Cases** | End with `UseCase` | `LoginUseCase` | `login_usecase.dart` |
|
|
|
|
## 3. Logic Placement (Zero Tolerance Boundaries)
|
|
|
|
### Business Rules → Use Cases ONLY
|
|
|
|
**✅ CORRECT:**
|
|
```dart
|
|
// login_usecase.dart
|
|
class LoginUseCase extends UseCase<User, LoginParams> {
|
|
@override
|
|
Future<Either<Failure, User>> call(LoginParams params) async {
|
|
// Business logic here: validation, transformation, orchestration
|
|
if (params.email.isEmpty) {
|
|
return Left(ValidationFailure('Email required'));
|
|
}
|
|
return await repository.login(params);
|
|
}
|
|
}
|
|
```
|
|
|
|
**❌ FORBIDDEN:**
|
|
```dart
|
|
// ❌ Business logic in BLoC
|
|
class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|
on<LoginRequested>((event, emit) {
|
|
if (event.email.isEmpty) { // ← NO! This is business logic
|
|
emit(AuthError('Email required'));
|
|
}
|
|
});
|
|
}
|
|
|
|
// ❌ Business logic in Widget
|
|
class LoginPage extends StatelessWidget {
|
|
void _login() {
|
|
if (_emailController.text.isEmpty) { // ← NO! This is business logic
|
|
showSnackbar('Email required');
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### State Logic → BLoCs ONLY
|
|
|
|
**✅ CORRECT:**
|
|
```dart
|
|
// auth_bloc.dart
|
|
class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|
on<LoginRequested>((event, emit) async {
|
|
emit(AuthLoading());
|
|
final result = await loginUseCase(LoginParams(email: event.email));
|
|
result.fold(
|
|
(failure) => emit(AuthError(failure)),
|
|
(user) => emit(AuthAuthenticated(user)),
|
|
);
|
|
});
|
|
}
|
|
|
|
// login_page.dart (StatelessWidget)
|
|
class LoginPage extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocBuilder<AuthBloc, AuthState>(
|
|
builder: (context, state) {
|
|
if (state is AuthLoading) return LoadingIndicator();
|
|
if (state is AuthError) return ErrorWidget(state.message);
|
|
return LoginForm();
|
|
},
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
**❌ FORBIDDEN:**
|
|
```dart
|
|
// ❌ setState in Pages for complex state
|
|
class LoginPage extends StatefulWidget {
|
|
@override
|
|
State<LoginPage> createState() => _LoginPageState();
|
|
}
|
|
|
|
class _LoginPageState extends State<LoginPage> {
|
|
bool _isLoading = false; // ← NO! Use BLoC
|
|
String? _error; // ← NO! Use BLoC
|
|
|
|
void _login() {
|
|
setState(() => _isLoading = true); // ← NO! Use BLoC
|
|
}
|
|
}
|
|
```
|
|
|
|
**RECOMMENDATION:** Pages should be `StatelessWidget` with state delegated to BLoCs.
|
|
|
|
### Data Transformation → Repositories
|
|
|
|
**✅ CORRECT:**
|
|
```dart
|
|
// profile_repository_impl.dart
|
|
class ProfileRepositoryImpl implements ProfileRepositoryInterface {
|
|
ProfileRepositoryImpl({required BaseApiService apiService})
|
|
: _apiService = apiService;
|
|
final BaseApiService _apiService;
|
|
|
|
@override
|
|
Future<Staff> getProfile(String id) async {
|
|
final ApiResponse response = await _apiService.get(
|
|
V2ApiEndpoints.staffProfile(id),
|
|
);
|
|
// Data transformation happens here
|
|
return Staff.fromJson(response.data as Map<String, dynamic>);
|
|
}
|
|
}
|
|
```
|
|
|
|
**❌ FORBIDDEN:**
|
|
```dart
|
|
// ❌ JSON parsing in UI
|
|
class ProfilePage extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final json = jsonDecode(response.body); // ← NO!
|
|
final name = json['name'];
|
|
}
|
|
}
|
|
|
|
// ❌ JSON parsing in Domain Use Case
|
|
class GetProfileUseCase extends UseCase<Staff, String> {
|
|
@override
|
|
Future<Either<Failure, Staff>> call(String id) async {
|
|
final response = await http.get('/staff/$id');
|
|
final json = jsonDecode(response.body); // ← NO!
|
|
}
|
|
}
|
|
```
|
|
|
|
### Navigation → Flutter Modular + Safe Extensions
|
|
|
|
**✅ CORRECT:**
|
|
```dart
|
|
// Use Safe Navigation Extensions
|
|
import 'package:krow_core/krow_core.dart';
|
|
|
|
// In widget/BLoC:
|
|
Modular.to.safePush('/profile');
|
|
Modular.to.safeNavigate('/home');
|
|
Modular.to.popSafe();
|
|
|
|
// Even better: Use Typed Navigators
|
|
Modular.to.toStaffHome(); // Defined in StaffNavigator
|
|
Modular.to.toShiftDetails(shiftId: '123');
|
|
```
|
|
|
|
**❌ FORBIDDEN:**
|
|
```dart
|
|
// ❌ Direct Navigator.push
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => ProfilePage()),
|
|
);
|
|
|
|
// ❌ Direct Modular navigation without safety
|
|
Modular.to.navigate('/profile'); // ← Can cause blank screens
|
|
Modular.to.pop(); // ← Can crash if stack is empty
|
|
```
|
|
|
|
**PATTERN:** All navigation MUST have fallback to Home page. Safe extensions automatically handle this.
|
|
|
|
### Session Management → V2SessionService + SessionHandlerMixin
|
|
|
|
**✅ CORRECT:**
|
|
```dart
|
|
// In main.dart:
|
|
void main() async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
|
|
// Initialize session listener (pick allowed roles for app)
|
|
V2SessionService.instance.initializeAuthListener(
|
|
allowedRoles: ['STAFF', 'BOTH'], // for staff app
|
|
);
|
|
|
|
runApp(
|
|
SessionListener( // Wraps entire app
|
|
child: ModularApp(module: AppModule(), child: AppWidget()),
|
|
),
|
|
);
|
|
}
|
|
|
|
// In repository:
|
|
class ProfileRepositoryImpl implements ProfileRepositoryInterface {
|
|
ProfileRepositoryImpl({required BaseApiService apiService})
|
|
: _apiService = apiService;
|
|
final BaseApiService _apiService;
|
|
|
|
@override
|
|
Future<Staff> getProfile(String id) async {
|
|
final ApiResponse response = await _apiService.get(
|
|
V2ApiEndpoints.staffProfile(id),
|
|
);
|
|
return Staff.fromJson(response.data as Map<String, dynamic>);
|
|
}
|
|
}
|
|
```
|
|
|
|
**PATTERN:**
|
|
- **SessionListener** widget wraps app and shows dialogs for session errors
|
|
- **V2SessionService** provides automatic token refresh and auth management
|
|
- **ApiService** handles HTTP requests with automatic auth headers
|
|
- **Role validation** configurable per app
|
|
|
|
## 4. Localization Integration (core_localization)
|
|
|
|
All user-facing text MUST be localized.
|
|
|
|
### String Management
|
|
|
|
**✅ CORRECT:**
|
|
```dart
|
|
// In presentation layer:
|
|
import 'package:core_localization/core_localization.dart';
|
|
|
|
class LoginPage extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Text(context.strings.loginButton); // ← From localization
|
|
return ElevatedButton(
|
|
onPressed: _login,
|
|
child: Text(context.strings.submit),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
**❌ FORBIDDEN:**
|
|
```dart
|
|
// ❌ Hardcoded English strings
|
|
Text('Login')
|
|
Text('Submit')
|
|
ElevatedButton(child: Text('Click here'))
|
|
```
|
|
|
|
### BLoC Integration
|
|
|
|
**✅ CORRECT:**
|
|
```dart
|
|
// BLoCs emit domain failures (not localized strings)
|
|
class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|
on<LoginRequested>((event, emit) async {
|
|
final result = await loginUseCase(params);
|
|
result.fold(
|
|
(failure) => emit(AuthError(failure)), // ← Domain failure
|
|
(user) => emit(AuthAuthenticated(user)),
|
|
);
|
|
});
|
|
}
|
|
|
|
// UI translates failures to user-friendly messages
|
|
class LoginPage extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocBuilder<AuthBloc, AuthState>(
|
|
builder: (context, state) {
|
|
if (state is AuthError) {
|
|
final message = ErrorTranslator.translate(
|
|
state.failure,
|
|
context.strings,
|
|
);
|
|
return ErrorWidget(message); // ← Localized
|
|
}
|
|
},
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### App Setup
|
|
|
|
Apps must import `LocalizationModule()`:
|
|
```dart
|
|
// app_module.dart
|
|
class AppModule extends Module {
|
|
@override
|
|
List<Module> get imports => [
|
|
LocalizationModule(), // ← Required
|
|
CoreModule(),
|
|
];
|
|
}
|
|
|
|
// main.dart
|
|
runApp(
|
|
BlocProvider<LocaleBloc>( // ← Expose locale state
|
|
create: (_) => Modular.get<LocaleBloc>(),
|
|
child: TranslationProvider( // ← Enable context.strings
|
|
child: MaterialApp.router(...),
|
|
),
|
|
),
|
|
);
|
|
```
|
|
|
|
## 5. V2 API Integration
|
|
|
|
All backend access goes through `ApiService` with `V2ApiEndpoints`.
|
|
|
|
### Repository Pattern
|
|
|
|
**Step 1:** Define interface in feature domain (optional — feature-level domain layer is optional if entities from `krow_domain` suffice):
|
|
```dart
|
|
// domain/repositories/shifts_repository_interface.dart
|
|
abstract interface class ShiftsRepositoryInterface {
|
|
Future<List<AssignedShift>> getAssignedShifts();
|
|
Future<AssignedShift> getShiftById(String id);
|
|
}
|
|
```
|
|
|
|
**Step 2:** Implement using `ApiService` + `V2ApiEndpoints`:
|
|
```dart
|
|
// data/repositories_impl/shifts_repository_impl.dart
|
|
class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|
ShiftsRepositoryImpl({required BaseApiService apiService})
|
|
: _apiService = apiService;
|
|
final BaseApiService _apiService;
|
|
|
|
@override
|
|
Future<List<AssignedShift>> getAssignedShifts() async {
|
|
final ApiResponse response = await _apiService.get(V2ApiEndpoints.staffShiftsAssigned);
|
|
final List<dynamic> items = response.data['items'] as List<dynamic>;
|
|
return items.map((dynamic json) => AssignedShift.fromJson(json as Map<String, dynamic>)).toList();
|
|
}
|
|
|
|
@override
|
|
Future<AssignedShift> getShiftById(String id) async {
|
|
final ApiResponse response = await _apiService.get(V2ApiEndpoints.staffShift(id));
|
|
return AssignedShift.fromJson(response.data as Map<String, dynamic>);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Key Conventions
|
|
|
|
- **Domain entities** have `fromJson` / `toJson` factory methods for serialization
|
|
- **Status fields** use enums from `krow_domain` (e.g., `ShiftStatus`, `OrderStatus`)
|
|
- **Money** is represented in cents as `int` (never `double`)
|
|
- **Timestamps** are `DateTime` objects (parsed from ISO 8601 strings)
|
|
- **Feature-level domain layer** is optional when `krow_domain` entities cover the need
|
|
|
|
### Session Store Pattern
|
|
|
|
After successful auth, populate session stores:
|
|
```dart
|
|
// For Staff App:
|
|
StaffSessionStore.instance.setSession(
|
|
StaffSession(
|
|
user: user,
|
|
staff: staff,
|
|
ownerId: ownerId,
|
|
),
|
|
);
|
|
|
|
// For Client App:
|
|
ClientSessionStore.instance.setSession(
|
|
ClientSession(
|
|
user: user,
|
|
business: business,
|
|
),
|
|
);
|
|
```
|
|
|
|
**Lazy Loading:** If session is null, fetch via the appropriate `ApiService.get()` endpoint and update store.
|
|
|
|
## 6. Prototype Migration Rules
|
|
|
|
When migrating from `prototypes/`:
|
|
|
|
### ✅ MAY Copy
|
|
- Icons, images, assets (but match to design system)
|
|
- `build` methods for UI layout structure
|
|
- Screen flow and navigation patterns
|
|
|
|
### ❌ MUST REJECT & REFACTOR
|
|
- `GetX`, `Provider`, or `MVC` patterns
|
|
- Any state management not using BLoC
|
|
- Direct HTTP calls (must use ApiService with V2ApiEndpoints)
|
|
- Hardcoded colors/typography (must use design system)
|
|
- Global state variables
|
|
- Navigation without Modular
|
|
|
|
### Colors & Typography Migration
|
|
**When matching POC to production:**
|
|
1. Find closest color in `UiColors` (don't add new colors without approval)
|
|
2. Find closest text style in `UiTypography`
|
|
3. Use design system constants, NOT POC hardcoded values
|
|
|
|
**DO NOT change the design system itself.** Colors and typography are FINAL. Match your feature to the system, not the other way around.
|
|
|
|
## 7. Handling Ambiguity
|
|
|
|
If requirements are unclear:
|
|
|
|
1. **STOP** - Don't guess domain fields or workflows
|
|
2. **ANALYZE** - Refer to:
|
|
- Architecture: `apps/mobile/docs/01-architecture-principles.md`
|
|
- Design System: `apps/mobile/docs/02-design-system-usage.md`
|
|
- Existing features for patterns
|
|
3. **DOCUMENT** - Add `// ASSUMPTION: <explanation>` if you must proceed
|
|
4. **ASK** - Prefer asking user for clarification on business rules
|
|
|
|
## 8. Dependencies
|
|
|
|
### DO NOT
|
|
- Add 3rd party packages without checking `apps/mobile/packages/core` first
|
|
- Add `firebase_auth` or `firebase_data_connect` to Feature packages (they belong in `core` only)
|
|
|
|
### DO
|
|
- Use `ApiService` with `V2ApiEndpoints` for backend operations
|
|
- Use Flutter Modular for dependency injection
|
|
- Register BLoCs with `i.add<CubitType>(() => CubitType(...))` (transient)
|
|
- Register Use Cases as factories or singletons as needed
|
|
|
|
## 9. Error Handling Pattern
|
|
|
|
### Domain Failures
|
|
```dart
|
|
// domain/failures/auth_failure.dart
|
|
abstract class AuthFailure extends Failure {
|
|
const AuthFailure(String message) : super(message);
|
|
}
|
|
|
|
class InvalidCredentialsFailure extends AuthFailure {
|
|
const InvalidCredentialsFailure() : super('Invalid credentials');
|
|
}
|
|
```
|
|
|
|
### Repository Error Mapping
|
|
```dart
|
|
// Map API errors to Domain failures using ApiErrorHandler
|
|
try {
|
|
final response = await _apiService.get(V2ApiEndpoints.staffProfile(id));
|
|
return Right(Staff.fromJson(response.data as Map<String, dynamic>));
|
|
} catch (e) {
|
|
return Left(ApiErrorHandler.mapToFailure(e));
|
|
}
|
|
```
|
|
|
|
### UI Feedback
|
|
```dart
|
|
// BLoC emits error state
|
|
emit(AuthError(failure));
|
|
|
|
// UI shows user-friendly message
|
|
if (state is AuthError) {
|
|
final message = ErrorTranslator.translate(state.failure, context.strings);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(message)),
|
|
);
|
|
}
|
|
```
|
|
|
|
### Session Errors
|
|
`SessionListener` automatically shows dialogs for:
|
|
- Session expiration
|
|
- Token refresh failures
|
|
- Network errors during auth
|
|
|
|
## 10. Testing Requirements
|
|
|
|
### Unit Tests
|
|
```dart
|
|
// Test use cases with real repository implementations
|
|
test('login with valid credentials returns user', () async {
|
|
final useCase = LoginUseCase(repository: mockRepository);
|
|
final result = await useCase(LoginParams(email: 'test@test.com'));
|
|
expect(result.isRight(), true);
|
|
});
|
|
```
|
|
|
|
### Widget Tests
|
|
```dart
|
|
// Test UI widgets and BLoC interactions
|
|
testWidgets('shows loading indicator when logging in', (tester) async {
|
|
await tester.pumpWidget(
|
|
BlocProvider<AuthBloc>(
|
|
create: (_) => authBloc,
|
|
child: LoginPage(),
|
|
),
|
|
);
|
|
|
|
authBloc.add(LoginRequested(email: 'test@test.com'));
|
|
await tester.pump();
|
|
|
|
expect(find.byType(LoadingIndicator), findsOneWidget);
|
|
});
|
|
```
|
|
|
|
### Integration Tests
|
|
- Test full feature flows end-to-end with V2 API
|
|
- Use dependency injection to swap implementations if needed
|
|
|
|
## 11. Clean Code Principles
|
|
|
|
### Documentation
|
|
- ✅ Add human readable doc comments for `dartdoc` for all classes and methods.
|
|
```dart
|
|
/// Authenticates user with email and password.
|
|
///
|
|
/// Returns [User] on success or [AuthFailure] on failure.
|
|
/// Throws [NetworkException] if connection fails.
|
|
class LoginUseCase extends UseCase<User, LoginParams> {
|
|
// ...
|
|
}
|
|
```
|
|
|
|
### Single Responsibility
|
|
- Keep methods focused on one task
|
|
- Extract complex logic to separate methods
|
|
- Keep widget build methods concise
|
|
- Extract complex widgets to separate files
|
|
|
|
### Meaningful Names
|
|
```dart
|
|
// ✅ GOOD
|
|
final isProfileComplete = await checkProfileCompletion();
|
|
final userShifts = await fetchUserShifts();
|
|
|
|
// ❌ BAD
|
|
final flag = await check();
|
|
final data = await fetch();
|
|
```
|
|
|
|
## Enforcement Checklist
|
|
|
|
Before merging any mobile feature code:
|
|
|
|
### Architecture Compliance
|
|
- [ ] Feature follows package structure (domain/data/presentation)
|
|
- [ ] No business logic in BLoCs or Widgets
|
|
- [ ] All state management via BLoCs
|
|
- [ ] All backend access via repositories
|
|
- [ ] Session accessed via SessionStore, not global state
|
|
- [ ] Navigation uses Flutter Modular safe extensions
|
|
- [ ] No feature-to-feature imports
|
|
|
|
### Code Quality
|
|
- [ ] No hardcoded strings (use localization)
|
|
- [ ] No hardcoded colors/typography (use design system)
|
|
- [ ] All spacing uses UiConstants
|
|
- [ ] Doc comments on public APIs
|
|
- [ ] Meaningful variable names
|
|
- [ ] Zero analyzer warnings
|
|
|
|
### Integration
|
|
- [ ] V2 API calls via `ApiService` + `V2ApiEndpoints`
|
|
- [ ] Error handling with domain failures
|
|
- [ ] Proper dependency injection in modules
|
|
|
|
## Summary
|
|
|
|
The key principle: **Clean Architecture with zero tolerance for violations.** Business logic in Use Cases, state in BLoCs, data access in Repositories (via `ApiService` + `V2ApiEndpoints`), UI in Widgets. Features are isolated, backend access is centralized through the V2 REST API layer, localization is mandatory, and design system is immutable.
|
|
|
|
When in doubt, refer to existing features following these patterns or ask for clarification. It's better to ask than to introduce architectural debt.
|