Feature Implementation Automation Skill
Skill Overview
You are an expert Flutter developer specializing in Clean Architecture implementation. This skill automates the complete implementation of Flutter features from specifications in IMPLEMENTATION_PLAN.md, following PRPROMPTS methodology with comprehensive testing and security validation.
What This Skill Does:
- Reads feature specifications from IMPLEMENTATION_PLAN.md
- Implements complete Clean Architecture structure (domain/data/presentation layers)
- Generates entities, use cases, repositories, data sources
- Creates BLoC/Cubit state management
- Builds UI components (screens, widgets)
- Writes comprehensive tests (unit, widget, integration)
- Validates security patterns and compliance requirements
- Ensures 70%+ test coverage
Execution Time: 3-8 minutes per feature
Prerequisites
Before running this skill, verify:
Flutter Project Initialized:
# Check Flutter project exists test -f pubspec.yaml || echo "❌ Not a Flutter project"IMPLEMENTATION_PLAN.md Exists:
# Check implementation plan test -f docs/IMPLEMENTATION_PLAN.md || echo "❌ IMPLEMENTATION_PLAN.md not found"PRPROMPTS Files Present:
# Check PRPROMPTS directory test -d PRPROMPTS || echo "❌ PRPROMPTS directory not found" ls PRPROMPTS/*.md | wc -l # Should show 32 filesFlutter Bootstrapper Completed:
- Clean Architecture folder structure exists:
lib/features/,lib/core/ - Core utilities configured (dependency injection, error handling, network)
- Clean Architecture folder structure exists:
If prerequisites not met:
- Run
@claude use skill automation/flutter-bootstrapperfirst
Step 1: Parse Feature Specification
1.1 Read IMPLEMENTATION_PLAN.md
Read the entire implementation plan:
# Display implementation plan
cat docs/IMPLEMENTATION_PLAN.md
Expected Structure:
# Implementation Plan
## Phase 1: Core Features (Week 1-2)
### Feature 1: Authentication
**Priority:** HIGH
**Estimated Time:** 6-8 hours
**Dependencies:** None
**Requirements:**
- Email/password authentication
- JWT token management
- Biometric authentication (iOS/Android)
- Session persistence
**User Stories:**
- As a user, I can register with email/password
- As a user, I can log in with email/password
- As a user, I can use Face ID/Touch ID to log in
- As a user, I stay logged in after closing the app
**Acceptance Criteria:**
- [ ] User can register with valid email/password
- [ ] User receives email verification
- [ ] User can log in with verified credentials
- [ ] JWT token stored securely in FlutterSecureStorage
- [ ] Biometric auth enabled after first login
- [ ] Session persists across app restarts
- [ ] Login screen shows proper validation errors
**Technical Details:**
- Use firebase_auth or custom backend
- Store JWT in FlutterSecureStorage (encrypted)
- Use local_auth package for biometrics
- Implement auto-logout after 30 minutes inactivity
- Hash passwords with bcrypt (backend)
**Security Requirements:**
- NEVER store passwords in plain text
- Use HTTPS for all auth endpoints
- Implement rate limiting (5 attempts/15min)
- JWT expires after 24 hours
- Refresh token rotation
**API Endpoints:**
- POST /api/auth/register
- POST /api/auth/login
- POST /api/auth/refresh
- POST /api/auth/logout
- GET /api/auth/verify-email
**Data Models:**
```dart
class User {
final String id;
final String email;
final String? displayName;
final String? photoUrl;
final bool emailVerified;
final DateTime createdAt;
}
class AuthTokens {
final String accessToken;
final String refreshToken;
final DateTime expiresAt;
}
UI Screens:
- LoginScreen (lib/features/auth/presentation/pages/login_screen.dart)
- RegisterScreen (lib/features/auth/presentation/pages/register_screen.dart)
- ForgotPasswordScreen (lib/features/auth/presentation/pages/forgot_password_screen.dart)
Tests Required:
- Unit tests for use cases (login, register, logout)
- Unit tests for repositories
- Widget tests for login/register screens
- Integration test for complete auth flow
### 1.2 Extract Feature Information
**Parse the feature specified in input:**
From `{{feature_name}}` input, extract:
- Feature name (e.g., "authentication")
- Priority level
- Requirements list
- User stories
- Acceptance criteria
- Technical details
- Security requirements
- API endpoints
- Data models
- UI screens
- Test requirements
**Create a feature summary:**
```markdown
# Feature: {{feature_name}}
## Key Information
- **Priority:** [HIGH/MEDIUM/LOW]
- **Estimated Time:** [X hours]
- **Dependencies:** [List or None]
## Requirements
[Bulleted list from plan]
## Security Requirements
[Critical security patterns to implement]
## Architecture Components
### Domain Layer
- Entities: [List entities to create]
- Use Cases: [List use cases to create]
- Repository Contracts: [List repository interfaces]
### Data Layer
- Models: [List data models]
- Data Sources: [Remote/Local data sources]
- Repository Implementations: [Concrete repositories]
### Presentation Layer
- BLoC/Cubit: [State management files]
- Screens: [UI screens to build]
- Widgets: [Reusable widgets]
## Test Strategy
- Unit Tests: [X files]
- Widget Tests: [Y files]
- Integration Tests: [Z files]
- Target Coverage: {{test_coverage_target}}%
Validation:
- ✅ Feature exists in IMPLEMENTATION_PLAN.md
- ✅ All required sections present
- ✅ Data models defined
- ✅ API endpoints documented
- ✅ Security requirements specified
If feature not found in plan:
❌ ERROR: Feature "{{feature_name}}" not found in IMPLEMENTATION_PLAN.md
Available features:
[List all features from plan with priorities]
Please specify a valid feature name or update IMPLEMENTATION_PLAN.md.
Step 2: Implement Domain Layer
The domain layer contains business logic and is independent of frameworks, UI, and external dependencies.
2.1 Create Entities
Entities are pure Dart classes representing core business objects.
For each entity in the feature:
File: lib/features/{{feature_name}}/domain/entities/{{entity_name}}.dart
Template:
import 'package:equatable/equatable.dart';
/// {{Entity description from plan}}
///
/// This entity represents {{business concept explanation}}.
///
/// **Business Rules:**
/// - {{Rule 1}}
/// - {{Rule 2}}
///
/// **Immutability:** This entity is immutable to ensure data consistency
/// and prevent accidental mutations across the application.
class {{EntityName}} extends Equatable {
/// {{Field description}}
final {{Type}} {{fieldName}};
const {{EntityName}}({
required this.{{fieldName}},
// ... other fields
});
@override
List<Object?> get props => [{{fieldName}}, /* other fields */];
@override
bool get stringify => true;
/// Creates a copy of this entity with updated fields
{{EntityName}} copyWith({
{{Type}}? {{fieldName}},
// ... other fields
}) {
return {{EntityName}}(
{{fieldName}}: {{fieldName}} ?? this.{{fieldName}},
// ... other fields
);
}
}
Example for Authentication Feature:
File: lib/features/auth/domain/entities/user.dart
import 'package:equatable/equatable.dart';
/// User entity representing an authenticated user in the system.
///
/// This entity contains core user information after successful authentication.
/// It is used throughout the domain layer for authorization and user-specific operations.
///
/// **Business Rules:**
/// - User ID must be unique and non-empty
/// - Email must be verified for full access
/// - Display name is optional but recommended for UX
///
/// **Security Considerations:**
/// - Password is NEVER stored in this entity (authentication backend only)
/// - Sensitive data (email) should be handled according to GDPR/compliance
class User extends Equatable {
/// Unique identifier for the user (from backend)
final String id;
/// User's email address (verified or unverified)
final String email;
/// Display name for UI (optional)
final String? displayName;
/// Profile photo URL (optional)
final String? photoUrl;
/// Whether email has been verified
final bool emailVerified;
/// Account creation timestamp
final DateTime createdAt;
const User({
required this.id,
required this.email,
required this.emailVerified,
required this.createdAt,
this.displayName,
this.photoUrl,
});
@override
List<Object?> get props => [
id,
email,
displayName,
photoUrl,
emailVerified,
createdAt,
];
@override
bool get stringify => true;
/// Creates a copy of this user with updated fields
User copyWith({
String? id,
String? email,
String? displayName,
String? photoUrl,
bool? emailVerified,
DateTime? createdAt,
}) {
return User(
id: id ?? this.id,
email: email ?? this.email,
displayName: displayName ?? this.displayName,
photoUrl: photoUrl ?? this.photoUrl,
emailVerified: emailVerified ?? this.emailVerified,
createdAt: createdAt ?? this.createdAt,
);
}
}
File: lib/features/auth/domain/entities/auth_tokens.dart
import 'package:equatable/equatable.dart';
/// Authentication tokens for session management.
///
/// Contains JWT access and refresh tokens for maintaining authenticated sessions.
///
/// **Security Rules:**
/// - Tokens are stored in FlutterSecureStorage (encrypted)
/// - Access token expires after 24 hours (configurable)
/// - Refresh token rotates on each use
/// - NEVER log tokens in production
class AuthTokens extends Equatable {
/// JWT access token for API requests
final String accessToken;
/// JWT refresh token for obtaining new access tokens
final String refreshToken;
/// Expiration timestamp for access token
final DateTime expiresAt;
const AuthTokens({
required this.accessToken,
required this.refreshToken,
required this.expiresAt,
});
/// Whether the access token has expired
bool get isExpired => DateTime.now().isAfter(expiresAt);
/// Time remaining until expiration
Duration get timeUntilExpiration => expiresAt.difference(DateTime.now());
@override
List<Object?> get props => [accessToken, refreshToken, expiresAt];
@override
bool get stringify => true;
AuthTokens copyWith({
String? accessToken,
String? refreshToken,
DateTime? expiresAt,
}) {
return AuthTokens(
accessToken: accessToken ?? this.accessToken,
refreshToken: refreshToken ?? this.refreshToken,
expiresAt: expiresAt ?? this.expiresAt,
);
}
}
2.2 Create Use Cases
Use cases contain single-responsibility business operations.
For each use case in the feature:
File: lib/features/{{feature_name}}/domain/usecases/{{use_case_name}}.dart
Template:
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import '../../../../core/error/failures.dart';
import '../../../../core/usecases/usecase.dart';
import '../entities/{{entity_name}}.dart';
import '../repositories/{{repository_name}}.dart';
/// {{Use case description}}
///
/// **Business Logic:**
/// {{Explain what this use case accomplishes}}
///
/// **Parameters:**
/// - {{param1}}: {{description}}
///
/// **Returns:**
/// - Success: {{EntityName}} object
/// - Failure: {{FailureType}} with error details
///
/// **Validation Rules:**
/// - {{Rule 1}}
/// - {{Rule 2}}
class {{UseCaseName}} implements UseCase<{{ReturnType}}, {{ParamsType}}> {
final {{RepositoryName}} repository;
{{UseCaseName}}(this.repository);
@override
Future<Either<Failure, {{ReturnType}}>> call({{ParamsType}} params) async {
// Input validation
final validationResult = params.validate();
if (validationResult != null) {
return Left(ValidationFailure(validationResult));
}
// Call repository
return await repository.{{methodName}}(
params.{{field1}},
params.{{field2}},
);
}
}
/// Parameters for {{UseCaseName}}
class {{ParamsName}} extends Equatable {
final {{Type}} {{fieldName}};
const {{ParamsName}}({
required this.{{fieldName}},
});
/// Validates parameters before use case execution
String? validate() {
// Validation logic
if ({{fieldName}}.isEmpty) {
return '{{FieldName}} cannot be empty';
}
return null;
}
@override
List<Object?> get props => [{{fieldName}}];
}
Example for Authentication Feature:
File: lib/features/auth/domain/usecases/login_with_email.dart
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import '../../../../core/error/failures.dart';
import '../../../../core/usecases/usecase.dart';
import '../entities/user.dart';
import '../repositories/auth_repository.dart';
/// Authenticates a user with email and password.
///
/// **Business Logic:**
/// 1. Validates email format and password strength
/// 2. Sends credentials to authentication backend
/// 3. Stores JWT tokens securely on success
/// 4. Returns authenticated User entity
///
/// **Security Considerations:**
/// - Password is transmitted over HTTPS only
/// - Rate limiting applied (5 attempts per 15 minutes)
/// - Account locked after 10 failed attempts
/// - Passwords are NEVER stored locally
///
/// **Error Handling:**
/// - Invalid credentials: AuthFailure with "Invalid email or password"
/// - Network error: NetworkFailure
/// - Server error: ServerFailure
/// - Account locked: AuthFailure with "Account locked, try again in X minutes"
class LoginWithEmail implements UseCase<User, LoginParams> {
final AuthRepository repository;
LoginWithEmail(this.repository);
@override
Future<Either<Failure, User>> call(LoginParams params) async {
// Validate input parameters
final validationError = params.validate();
if (validationError != null) {
return Left(ValidationFailure(validationError));
}
// Attempt login via repository
return await repository.loginWithEmail(
email: params.email,
password: params.password,
);
}
}
/// Parameters for email/password login
class LoginParams extends Equatable {
final String email;
final String password;
const LoginParams({
required this.email,
required this.password,
});
/// Validates login parameters
///
/// **Validation Rules:**
/// - Email must be valid format (regex)
/// - Password must be at least 8 characters
/// - Email cannot be empty
/// - Password cannot be empty
String? validate() {
if (email.isEmpty) {
return 'Email cannot be empty';
}
// Email regex validation
final emailRegex = RegExp(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
);
if (!emailRegex.hasMatch(email)) {
return 'Please enter a valid email address';
}
if (password.isEmpty) {
return 'Password cannot be empty';
}
if (password.length < 8) {
return 'Password must be at least 8 characters';
}
return null; // Valid
}
@override
List<Object?> get props => [email, password];
}
File: lib/features/auth/domain/usecases/register_with_email.dart
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import '../../../../core/error/failures.dart';
import '../../../../core/usecases/usecase.dart';
import '../entities/user.dart';
import '../repositories/auth_repository.dart';
/// Registers a new user with email and password.
///
/// **Business Logic:**
/// 1. Validates email format and password strength
/// 2. Checks if email already exists (via backend)
/// 3. Creates new user account
/// 4. Sends email verification link
/// 5. Returns User entity (emailVerified = false)
///
/// **Password Requirements (PRPROMPTS Security Standard):**
/// - Minimum 8 characters
/// - At least 1 uppercase letter
/// - At least 1 lowercase letter
/// - At least 1 number
/// - At least 1 special character
///
/// **Compliance:**
/// - GDPR: User consent obtained during registration
/// - COPPA: Age verification if required
/// - Data minimization: Only collect necessary fields
class RegisterWithEmail implements UseCase<User, RegisterParams> {
final AuthRepository repository;
RegisterWithEmail(this.repository);
@override
Future<Either<Failure, User>> call(RegisterParams params) async {
// Validate input parameters
final validationError = params.validate();
if (validationError != null) {
return Left(ValidationFailure(validationError));
}
// Attempt registration via repository
return await repository.registerWithEmail(
email: params.email,
password: params.password,
displayName: params.displayName,
);
}
}
/// Parameters for email/password registration
class RegisterParams extends Equatable {
final String email;
final String password;
final String? displayName;
const RegisterParams({
required this.email,
required this.password,
this.displayName,
});
/// Validates registration parameters
///
/// Enforces strong password policy and email format validation
String? validate() {
// Email validation
if (email.isEmpty) {
return 'Email cannot be empty';
}
final emailRegex = RegExp(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
);
if (!emailRegex.hasMatch(email)) {
return 'Please enter a valid email address';
}
// Password validation
if (password.isEmpty) {
return 'Password cannot be empty';
}
if (password.length < 8) {
return 'Password must be at least 8 characters';
}
// Password strength requirements
final hasUppercase = password.contains(RegExp(r'[A-Z]'));
final hasLowercase = password.contains(RegExp(r'[a-z]'));
final hasDigit = password.contains(RegExp(r'[0-9]'));
final hasSpecialChar = password.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'));
if (!hasUppercase) {
return 'Password must contain at least one uppercase letter';
}
if (!hasLowercase) {
return 'Password must contain at least one lowercase letter';
}
if (!hasDigit) {
return 'Password must contain at least one number';
}
if (!hasSpecialChar) {
return 'Password must contain at least one special character';
}
// Display name validation (optional)
if (displayName != null && displayName!.length > 50) {
return 'Display name must be 50 characters or less';
}
return null; // Valid
}
@override
List<Object?> get props => [email, password, displayName];
}
Create additional use cases:
logout.dart- Logs out user, clears tokensget_current_user.dart- Retrieves currently authenticated userrefresh_token.dart- Refreshes expired access tokenverify_email.dart- Verifies email with tokenreset_password.dart- Initiates password reset flow
2.3 Create Repository Interface
Repository contracts define data access operations without implementation details.
File: lib/features/{{feature_name}}/domain/repositories/{{repository_name}}.dart
Template:
import 'package:dartz/dartz.dart';
import '../../../../core/error/failures.dart';
import '../entities/{{entity_name}}.dart';
/// Abstract repository defining data operations for {{feature_name}}.
///
/// This interface is implemented by the data layer and used by use cases.
/// It follows the Repository Pattern and Dependency Inversion Principle.
///
/// **Responsibilities:**
/// - Define contracts for data operations
/// - Return Either<Failure, Success> for error handling
/// - Remain agnostic to data source (API, database, cache)
///
/// **Implementation:**
/// See `lib/features/{{feature_name}}/data/repositories/{{repository_name}}_impl.dart`
abstract class {{RepositoryName}} {
/// {{Method description}}
///
/// **Parameters:**
/// - {{param1}}: {{description}}
///
/// **Returns:**
/// - Right({{ReturnType}}): Success
/// - Left(Failure): Error occurred
///
/// **Possible Failures:**
/// - NetworkFailure: No internet connection
/// - ServerFailure: Backend error
/// - ValidationFailure: Invalid input
Future<Either<Failure, {{ReturnType}}>> {{methodName}}({{params}});
}
Example for Authentication Feature:
File: lib/features/auth/domain/repositories/auth_repository.dart
import 'package:dartz/dartz.dart';
import '../../../../core/error/failures.dart';
import '../entities/user.dart';
import '../entities/auth_tokens.dart';
/// Abstract repository for authentication operations.
///
/// Defines the contract for all authentication-related data operations.
/// This interface is implemented by AuthRepositoryImpl in the data layer.
///
/// **Design Principles:**
/// - Dependency Inversion: Domain doesn't depend on data layer
/// - Single Responsibility: Only authentication operations
/// - Interface Segregation: Focused, cohesive interface
///
/// **Error Handling:**
/// All methods return Either<Failure, T> for explicit error handling:
/// - Left(Failure): Operation failed with specific error type
/// - Right(T): Operation succeeded with result
abstract class AuthRepository {
/// Authenticates user with email and password.
///
/// **Flow:**
/// 1. Sends credentials to backend via AuthRemoteDataSource
/// 2. Receives JWT tokens on success
/// 3. Stores tokens in FlutterSecureStorage via AuthLocalDataSource
/// 4. Returns User entity
///
/// **Security:**
/// - Credentials sent over HTTPS only
/// - Password NEVER stored locally
/// - Tokens encrypted in secure storage
///
/// **Possible Failures:**
/// - AuthFailure: Invalid credentials or account locked
/// - NetworkFailure: No internet connection
/// - ServerFailure: Backend error (500, 502, etc.)
Future<Either<Failure, User>> loginWithEmail({
required String email,
required String password,
});
/// Registers new user with email and password.
///
/// **Flow:**
/// 1. Sends registration data to backend
/// 2. Backend creates user account and sends verification email
/// 3. Returns User entity with emailVerified = false
///
/// **Possible Failures:**
/// - AuthFailure: Email already exists
/// - ValidationFailure: Invalid email or weak password
/// - NetworkFailure: No internet connection
/// - ServerFailure: Backend error
Future<Either<Failure, User>> registerWithEmail({
required String email,
required String password,
String? displayName,
});
/// Logs out current user and clears stored tokens.
///
/// **Flow:**
/// 1. Notifies backend to invalidate tokens (optional)
/// 2. Deletes tokens from FlutterSecureStorage
/// 3. Clears any cached user data
///
/// **Note:** This should always succeed locally, even if backend call fails
Future<Either<Failure, void>> logout();
/// Retrieves currently authenticated user.
///
/// **Flow:**
/// 1. Checks if tokens exist in secure storage
/// 2. Validates token expiration
/// 3. Fetches user profile from backend or cache
///
/// **Possible Failures:**
/// - AuthFailure: No user logged in or token expired
/// - NetworkFailure: Cannot reach backend
/// - CacheFailure: Local data corrupted
Future<Either<Failure, User>> getCurrentUser();
/// Refreshes expired access token using refresh token.
///
/// **Flow:**
/// 1. Retrieves refresh token from secure storage
/// 2. Exchanges refresh token for new access token
/// 3. Stores new tokens (refresh token may rotate)
///
/// **Security:**
/// - Refresh token rotation (new refresh token on each use)
/// - Old refresh token immediately invalidated
///
/// **Possible Failures:**
/// - AuthFailure: Refresh token invalid or expired
/// - NetworkFailure: Cannot reach backend
Future<Either<Failure, AuthTokens>> refreshAccessToken();
/// Verifies user's email address with token.
///
/// **Flow:**
/// 1. Sends verification token to backend
/// 2. Backend marks email as verified
/// 3. Returns updated User entity
///
/// **Possible Failures:**
/// - AuthFailure: Invalid or expired verification token
/// - NetworkFailure: Cannot reach backend
Future<Either<Failure, User>> verifyEmail(String token);
/// Initiates password reset flow.
///
/// **Flow:**
/// 1. Sends password reset request to backend
/// 2. Backend sends reset email with token
/// 3. Returns success (void)
///
/// **Security:**
/// - Reset token expires after 1 hour
/// - Email sent to registered address only
///
/// **Possible Failures:**
/// - AuthFailure: Email not found
/// - NetworkFailure: Cannot reach backend
Future<Either<Failure, void>> resetPassword(String email);
}
Domain Layer Summary:
After Step 2, you should have:
- ✅ Entities (pure business objects)
- ✅ Use Cases (single-responsibility operations)
- ✅ Repository Interface (data access contract)
- ✅ All files in
lib/features/{{feature_name}}/domain/
Step 3: Implement Data Layer
The data layer handles data retrieval from various sources (API, database, cache) and implements repository contracts.
3.1 Create Data Models
Models are data transfer objects that convert between JSON and entities.
For each entity, create a corresponding model:
File: lib/features/{{feature_name}}/data/models/{{model_name}}.dart
Template:
import '../../domain/entities/{{entity_name}}.dart';
/// Data model for {{EntityName}} entity.
///
/// Handles JSON serialization/deserialization for API communication.
/// Extends the domain entity to inherit business logic and properties.
///
/// **Responsibilities:**
/// - Convert JSON to Entity (fromJson)
/// - Convert Entity to JSON (toJson)
/// - Handle nullable fields from API
/// - Provide default values when needed
class {{ModelName}} extends {{EntityName}} {
const {{ModelName}}({
required super.{{field1}},
required super.{{field2}},
// ... other fields
});
/// Creates a {{ModelName}} from JSON received from API.
///
/// **JSON Structure:**
/// ```json
/// {
/// "{{jsonKey1}}": "{{value}}",
/// "{{jsonKey2}}": "{{value}}"
/// }
/// ```
///
/// **Null Handling:**
/// - Required fields throw if null
/// - Optional fields default to null
factory {{ModelName}}.fromJson(Map<String, dynamic> json) {
return {{ModelName}}(
{{field1}}: json['{{jsonKey1}}'] as {{Type}},
{{field2}}: json['{{jsonKey2}}'] as {{Type}},
// ... other fields
);
}
/// Converts this model to JSON for API requests.
///
/// **Output:**
/// ```json
/// {
/// "{{jsonKey1}}": "{{value}}",
/// "{{jsonKey2}}": "{{value}}"
/// }
/// ```
Map<String, dynamic> toJson() {
return {
'{{jsonKey1}}': {{field1}},
'{{jsonKey2}}': {{field2}},
// ... other fields
};
}
/// Creates a {{ModelName}} from a {{EntityName}} entity.
factory {{ModelName}}.fromEntity({{EntityName}} entity) {
return {{ModelName}}(
{{field1}}: entity.{{field1}},
{{field2}}: entity.{{field2}},
// ... other fields
);
}
}
Example for Authentication Feature:
File: lib/features/auth/data/models/user_model.dart
import '../../domain/entities/user.dart';
/// Data model for User entity.
///
/// Handles JSON serialization/deserialization for user data from API.
///
/// **API Response Example:**
/// ```json
/// {
/// "id": "user_123456",
/// "email": "john.doe@example.com",
/// "display_name": "John Doe",
/// "photo_url": "https://example.com/avatars/john.jpg",
/// "email_verified": true,
/// "created_at": "2024-01-15T10:30:00Z"
/// }
/// ```
class UserModel extends User {
const UserModel({
required super.id,
required super.email,
required super.emailVerified,
required super.createdAt,
super.displayName,
super.photoUrl,
});
/// Creates UserModel from JSON response.
///
/// **Field Mappings:**
/// - id: Required string
/// - email: Required string
/// - display_name: Optional string (snake_case from API)
/// - photo_url: Optional string (snake_case from API)
/// - email_verified: Required boolean (snake_case from API)
/// - created_at: Required ISO 8601 timestamp (snake_case from API)
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'] as String,
email: json['email'] as String,
displayName: json['display_name'] as String?,
photoUrl: json['photo_url'] as String?,
emailVerified: json['email_verified'] as bool,
createdAt: DateTime.parse(json['created_at'] as String),
);
}
/// Converts UserModel to JSON for API requests.
///
/// Used when updating user profile or sending user data to backend.
Map<String, dynamic> toJson() {
return {
'id': id,
'email': email,
'display_name': displayName,
'photo_url': photoUrl,
'email_verified': emailVerified,
'created_at': createdAt.toIso8601String(),
};
}
/// Creates UserModel from User entity.
///
/// Useful when converting domain entities back to models for API calls.
factory UserModel.fromEntity(User user) {
return UserModel(
id: user.id,
email: user.email,
displayName: user.displayName,
photoUrl: user.photoUrl,
emailVerified: user.emailVerified,
createdAt: user.createdAt,
);
}
}
File: lib/features/auth/data/models/auth_tokens_model.dart
import '../../domain/entities/auth_tokens.dart';
/// Data model for AuthTokens entity.
///
/// Handles JSON serialization for JWT tokens from authentication API.
///
/// **API Response Example:**
/// ```json
/// {
/// "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
/// "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
/// "expires_at": "2024-01-16T10:30:00Z"
/// }
/// ```
///
/// **Security Note:**
/// Tokens are immediately stored in FlutterSecureStorage and cleared from memory.
class AuthTokensModel extends AuthTokens {
const AuthTokensModel({
required super.accessToken,
required super.refreshToken,
required super.expiresAt,
});
/// Creates AuthTokensModel from JSON response.
factory AuthTokensModel.fromJson(Map<String, dynamic> json) {
return AuthTokensModel(
accessToken: json['access_token'] as String,
refreshToken: json['refresh_token'] as String,
expiresAt: DateTime.parse(json['expires_at'] as String),
);
}
/// Converts to JSON for storage or API requests.
Map<String, dynamic> toJson() {
return {
'access_token': accessToken,
'refresh_token': refreshToken,
'expires_at': expiresAt.toIso8601String(),
};
}
/// Creates AuthTokensModel from AuthTokens entity.
factory AuthTokensModel.fromEntity(AuthTokens tokens) {
return AuthTokensModel(
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresAt: tokens.expiresAt,
);
}
}
3.2 Create Data Sources
Data sources handle direct communication with external systems (API, database).
3.2.1 Remote Data Source (API)
File: lib/features/{{feature_name}}/data/datasources/{{feature_name}}_remote_data_source.dart
Template:
import 'package:dio/dio.dart';
import '../../../../core/error/exceptions.dart';
import '../models/{{model_name}}.dart';
/// Remote data source for {{feature_name}} via REST API.
///
/// Handles HTTP requests to backend API endpoints.
/// Throws exceptions on errors (converted to Failures in repository).
///
/// **Base URL:** Configured in lib/core/network/api_client.dart
/// **Authentication:** Bearer token in Authorization header
abstract class {{FeatureName}}RemoteDataSource {
/// {{Method description}}
///
/// **Endpoint:** {{HTTP_METHOD}} {{/api/path}}
/// **Headers:** Authorization: Bearer {{token}}
///
/// **Throws:**
/// - ServerException: HTTP 500/502/503
/// - NetworkException: No internet connection
/// - AuthException: Invalid credentials or token expired
Future<{{ModelName}}> {{methodName}}({{params}});
}
class {{FeatureName}}RemoteDataSourceImpl implements {{FeatureName}}RemoteDataSource {
final Dio dio;
{{FeatureName}}RemoteDataSourceImpl({required this.dio});
@override
Future<{{ModelName}}> {{methodName}}({{params}}) async {
try {
final response = await dio.{{httpMethod}}(
'{{/api/endpoint}}',
data: {
'{{key1}}': {{value1}},
// ... request body
},
);
if (response.statusCode == 200 || response.statusCode == 201) {
return {{ModelName}}.fromJson(response.data);
} else {
throw ServerException(
message: 'Unexpected status code: ${response.statusCode}',
);
}
} on DioException catch (e) {
if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.receiveTimeout) {
throw NetworkException(message: 'Connection timeout');
} else if (e.response?.statusCode == 401 || e.response?.statusCode == 403) {
throw AuthException(
message: e.response?.data['message'] ?? 'Unauthorized',
);
} else if (e.response?.statusCode == 400) {
throw ValidationException(
message: e.response?.data['message'] ?? 'Invalid input',
);
} else if (e.response?.statusCode != null && e.response!.statusCode! >= 500) {
throw ServerException(
message: e.response?.data['message'] ?? 'Server error',
);
} else {
throw NetworkException(message: 'Network error: ${e.message}');
}
} catch (e) {
throw ServerException(message: 'Unexpected error: $e');
}
}
}
Example for Authentication Feature:
File: lib/features/auth/data/datasources/auth_remote_data_source.dart
import 'package:dio/dio.dart';
import '../../../../core/error/exceptions.dart';
import '../models/user_model.dart';
import '../models/auth_tokens_model.dart';
/// Remote data source for authentication via REST API.
///
/// **Base URL:** https://api.example.com/v1
/// **Endpoints:**
/// - POST /auth/register
/// - POST /auth/login
/// - POST /auth/logout
/// - POST /auth/refresh
/// - GET /auth/me
/// - POST /auth/verify-email
/// - POST /auth/reset-password
abstract class AuthRemoteDataSource {
/// Authenticates user with email and password.
///
/// **Endpoint:** POST /auth/login
/// **Request Body:**
/// ```json
/// {
/// "email": "user@example.com",
/// "password": "SecurePass123!"
/// }
/// ```
///
/// **Response:**
/// ```json
/// {
/// "user": { ...user data... },
/// "tokens": { ...tokens... }
/// }
/// ```
///
/// **Throws:**
/// - AuthException: Invalid credentials (401)
/// - ServerException: Server error (500+)
/// - NetworkException: Connection error
Future<Map<String, dynamic>> loginWithEmail({
required String email,
required String password,
});
/// Registers new user.
///
/// **Endpoint:** POST /auth/register
Future<Map<String, dynamic>> registerWithEmail({
required String email,
required String password,
String? displayName,
});
/// Logs out user (invalidates refresh token on backend).
///
/// **Endpoint:** POST /auth/logout
/// **Headers:** Authorization: Bearer {{access_token}}
Future<void> logout(String accessToken);
/// Fetches current user profile.
///
/// **Endpoint:** GET /auth/me
/// **Headers:** Authorization: Bearer {{access_token}}
Future<UserModel> getCurrentUser(String accessToken);
/// Refreshes access token.
///
/// **Endpoint:** POST /auth/refresh
/// **Request Body:**
/// ```json
/// {
/// "refresh_token": "..."
/// }
/// ```
Future<AuthTokensModel> refreshAccessToken(String refreshToken);
/// Verifies email with token.
///
/// **Endpoint:** POST /auth/verify-email
Future<UserModel> verifyEmail(String token);
/// Initiates password reset.
///
/// **Endpoint:** POST /auth/reset-password
Future<void> resetPassword(String email);
}
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
final Dio dio;
AuthRemoteDataSourceImpl({required this.dio});
@override
Future<Map<String, dynamic>> loginWithEmail({
required String email,
required String password,
}) async {
try {
final response = await dio.post(
'/auth/login',
data: {
'email': email,
'password': password,
},
);
if (response.statusCode == 200) {
return {
'user': UserModel.fromJson(response.data['user']),
'tokens': AuthTokensModel.fromJson(response.data['tokens']),
};
} else {
throw ServerException(
message: 'Unexpected status code: ${response.statusCode}',
);
}
} on DioException catch (e) {
_handleDioError(e);
rethrow; // Unreachable, but required for type safety
}
}
@override
Future<Map<String, dynamic>> registerWithEmail({
required String email,
required String password,
String? displayName,
}) async {
try {
final response = await dio.post(
'/auth/register',
data: {
'email': email,
'password': password,
if (displayName != null) 'display_name': displayName,
},
);
if (response.statusCode == 201) {
return {
'user': UserModel.fromJson(response.data['user']),
'tokens': AuthTokensModel.fromJson(response.data['tokens']),
};
} else {
throw ServerException(
message: 'Unexpected status code: ${response.statusCode}',
);
}
} on DioException catch (e) {
_handleDioError(e);
rethrow;
}
}
@override
Future<void> logout(String accessToken) async {
try {
await dio.post(
'/auth/logout',
options: Options(headers: {'Authorization': 'Bearer $accessToken'}),
);
} on DioException catch (e) {
// Logout can fail gracefully - local logout still proceeds
print('Logout request failed: ${e.message}');
}
}
@override
Future<UserModel> getCurrentUser(String accessToken) async {
try {
final response = await dio.get(
'/auth/me',
options: Options(headers: {'Authorization': 'Bearer $accessToken'}),
);
if (response.statusCode == 200) {
return UserModel.fromJson(response.data);
} else {
throw ServerException(
message: 'Unexpected status code: ${response.statusCode}',
);
}
} on DioException catch (e) {
_handleDioError(e);
rethrow;
}
}
@override
Future<AuthTokensModel> refreshAccessToken(String refreshToken) async {
try {
final response = await dio.post(
'/auth/refresh',
data: {'refresh_token': refreshToken},
);
if (response.statusCode == 200) {
return AuthTokensModel.fromJson(response.data);
} else {
throw ServerException(
message: 'Unexpected status code: ${response.statusCode}',
);
}
} on DioException catch (e) {
_handleDioError(e);
rethrow;
}
}
@override
Future<UserModel> verifyEmail(String token) async {
try {
final response = await dio.post(
'/auth/verify-email',
data: {'token': token},
);
if (response.statusCode == 200) {
return UserModel.fromJson(response.data);
} else {
throw ServerException(
message: 'Unexpected status code: ${response.statusCode}',
);
}
} on DioException catch (e) {
_handleDioError(e);
rethrow;
}
}
@override
Future<void> resetPassword(S
…(truncated)