prprompts-flutter-generator
Version:
AI-powered Flutter development with full automation + official extension support - Generate 32 security-audited guides & auto-implement in 2-3 hours. NEW v5.1: Official Claude Code plugin with hooks, Gemini TOML commands, Qwen MCP settings. Features: Comp
3,417 lines • 97.1 kB
Markdown
# 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:
1. **Flutter Project Initialized:**
```bash
# Check Flutter project exists
test -f pubspec.yaml || echo "❌ Not a Flutter project"
```
2. **IMPLEMENTATION_PLAN.md Exists:**
```bash
# Check implementation plan
test -f docs/IMPLEMENTATION_PLAN.md || echo "❌ IMPLEMENTATION_PLAN.md not found"
```
3. **PRPROMPTS Files Present:**
```bash
# Check PRPROMPTS directory
test -d PRPROMPTS || echo "❌ PRPROMPTS directory not found"
ls PRPROMPTS/*.md | wc -l # Should show 32 files
```
4. **Flutter Bootstrapper Completed:**
- Clean Architecture folder structure exists: `lib/features/`, `lib/core/`
- Core utilities configured (dependency injection, error handling, network)
**If prerequisites not met:**
- Run `@claude use skill automation/flutter-bootstrapper` first
---
## Step 1: Parse Feature Specification
### 1.1 Read IMPLEMENTATION_PLAN.md
**Read the entire implementation plan:**
```bash
# Display implementation plan
cat docs/IMPLEMENTATION_PLAN.md
```
**Expected Structure:**
```markdown
# 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:**
```markdown
❌ 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:**
```dart
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`
```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`
```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:**
```dart
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`
```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`
```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 tokens
- `get_current_user.dart` - Retrieves currently authenticated user
- `refresh_token.dart` - Refreshes expired access token
- `verify_email.dart` - Verifies email with token
- `reset_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:**
```dart
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`
```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:**
```dart
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`
```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`
```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:**
```dart
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`
```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(String email) async {
try {
await dio.post(
'/auth/reset-password',
data: {'email': email},
);
} on DioException catch (e) {
_handleDioError(e);
}
}
/// Handles Dio errors and converts to custom exceptions.
void _handleDioError(DioException e) {
if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.receiveTimeout ||
e.type == DioExceptionType.sendTimeout) {
throw NetworkException(message: 'Connection timeout');
} else if (e.response?.statusCode == 401) {
throw AuthException(
message: e.response?.data['message'] ?? 'Invalid credentials',
);
} else if (e.response?.statusCode == 403) {
throw AuthException(
message: e.response?.data['message'] ?? 'Access denied',
);
} else if (e.response?.statusCode == 400) {
throw ValidationException(
message: e.response?.data['message'] ?? 'Invalid input',
);
} else if (e.response?.statusCode == 409) {
throw ValidationException(
message: e.response?.data['message'] ?? 'Email already exists',
);
} 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}');
}
}
}
```
#### 3.2.2 Local Data Source (Storage)
**File:** `lib/features/{{feature_name}}/data/datasources/{{feature_name}}_local_data_source.dart`
**Example for Authentication Feature:**
**File:** `lib/features/auth/data/datasources/auth_local_data_source.dart`
```dart
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import '../../../../core/error/exceptions.dart';
import '../models/user_model.dart';
import '../models/auth_tokens_model.dart';
/// Local data source for authentication using secure storage.
///
/// **Storage:**
/// - FlutterSecureStorage: JWT tokens (encrypted)
/// - SharedPreferences: User profile (non-sensitive)
///
/// **Keys:**
/// - 'auth_access_token': Access token
/// - 'auth_refresh_token': Refresh token
/// - 'auth_tokens_expiry': Token expiration timestamp
/// - 'cached_user': User profile JSON
abstract class AuthLocalDataSource {
/// Stores authentication tokens securely.
Future<void> cacheTokens(AuthTokensModel tokens);
/// Retrieves cached tokens.
///
/// **Throws:**
/// - CacheException: Tokens not found or corrupted
Future<AuthTokensModel> getCachedTokens();
/// Deletes stored tokens (logout).
Future<void> clearTokens();
/// Stores user profile in cache.
Future<void> cacheUser(UserModel user);
/// Retrieves cached user profile.
///
/// **Throws:**
/// - CacheException: User not found in cache
Future<UserModel> getCachedUser();
/// Deletes cached user profile.
Future<void> clearUser();
/// Checks if user is logged in (tokens exist and not expired).
Future<bool> isLoggedIn();
}
class AuthLocalDataSourceImpl implements AuthLocalDataSource {
final FlutterSecureStorage secureStorage;
final SharedPreferences sharedPreferences;
// Storage keys
static const String _accessTokenKey = 'auth_access_token';
static const String _refreshTokenKey = 'auth_refresh_token';
static const String _expiryKey = 'auth_tokens_expiry';
static const String _cachedUserKey = 'cached_user';
AuthLocalDataSourceImpl({
required this.secureStorage,
required this.sharedPreferences,
});
@override
Future<void> cacheTokens(AuthTokensModel tokens) async {
try {
await Future.wait([
secureStorage.write(key: _accessTokenKey, value: tokens.accessToken),
secureStorage.write(key: _refreshTokenKey, value: tokens.refreshToken),
secureStorage.write(
key: _expiryKey,
value: tokens.expiresAt.toIso8601String(),
),
]);
} catch (e) {
throw CacheException(message: 'Failed to cache tokens: $e');
}
}
@override
Future<AuthTokensModel> getCachedTokens() async {
try {
final accessToken = await secureStorage.read(key: _accessTokenKey);
final refreshToken = await secureStorage.read(key: _refreshTokenKey);
final expiryString = await secureStorage.read(key: _expiryKey);
if (accessToken == null || refreshToken == null || expiryString == null) {
throw CacheException(message: 'Tokens not found in cache');
}
return AuthTokensModel(
accessToken: accessToken,
refreshToken: refreshToken,
expiresAt: DateTime.parse(expiryString),
);
} catch (e) {
throw CacheException(message: 'Failed to retrieve tokens: $e');
}
}
@override
Future<void> clearTokens() async {
try {
await Future.wait([
secureStorage.delete(key: _accessTokenKey),
secureStorage.delete(key: _refreshTokenKey),
secureStorage.delete(key: _expiryKey),
]);
} catch (e) {
throw CacheException(message: 'Failed to clear tokens: $e');
}
}
@override
Future<void> cacheUser(UserModel user) async {
try {
final userJson = jsonEncode(user.toJson());
await sharedPreferences.setString(_cachedUserKey, userJson);
} catch (e) {
throw CacheException(message: 'Failed to cache user: $e');
}
}
@override
Future<UserModel> getCachedUser() async {
try {
final userJson = sharedPreferences.getString(_cachedUserKey);
if (userJson == null) {
throw CacheException(message: 'User not found in cache');
}
final userMap = jsonDecode(userJson) as Map<String, dynamic>;
return UserModel.fromJson(userMap);
} catch (e) {
throw CacheException(message: 'Failed to retrieve user: $e');
}
}
@override
Future<void> clearUser() async {
try {
await sharedPreferences.remove(_cachedUserKey);
} catch (e) {
throw CacheException(message: 'Failed to clear user: $e');
}
}
@override
Future<bool> isLoggedIn() async {
try {
final tokens = await getCachedTokens();
return !tokens.isExpired;
} catch (e) {
return false;
}
}
}
```
### 3.3 Implement Repository
**Repository implementation coordinates data sources and converts exceptions to failures.**
**File:** `lib/features/{{feature_name}}/data/repositories/{{repository_name}}_impl.dart`
**Example for Authentication Feature:**
**File:** `lib/features/auth/data/repositories/auth_repository_impl.dart`
```dart
import 'package:dartz/dartz.dart';
import '../../../../core/error/exceptions.dart';
import '../../../../core/error/failures.dart';
import '../../../../core/network/network_info.dart';
import '../../domain/entities/user.dart';
import '../../domain/entities/auth_tokens.dart';
import '../../domain/repositories/auth_repository.dart';
import '../datasources/auth_remote_data_source.dart';
import '../datasources/auth_local_data_source.dart';
import '../models/auth_tokens_model.dart';
/// Implementation of AuthRepository.
///
/// Coordinates remote and local data sources, handles errors, and ensures
/// consistent behavior across authentication operations.
///
/// **Architecture:**
/// - Repository Pattern: Abstracts data source details
/// - Offline-First: Uses cached data when network unavailable
/// - Error Handling: Converts exceptions to Either<Failure, T>
///
/// **Data Flow:**
/// 1. Check network connectivity (for remote operations)
/// 2. Attempt remote operation
/// 3. Cache result locally on success
/// 4. Fall back to cache on network error (when applicable)
/// 5. Return Either<Failure, T>
class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteDataSource remoteDataSource;
final AuthLocalDataSource localDataSource;
final NetworkInfo networkInfo;
AuthRepositoryImpl({
required this.remoteDataSource,
required this.localDataSource,
required this.networkInfo,
});
@override
Future<Either<Failure, User>> loginWithEmail({
required String email,
required String password,
}) async {
// Check network connectivity
if (!await networkInfo.isConnected) {
return const Left(NetworkFailure(message: 'No internet connection'));
}
try {
// Attempt remote login
final result = await remoteDataSource.loginWithEmail(
email: email,
password: password,
);
// Extract user and tokens
final user = result['user'] as User;
final tokens = result['tokens'] as AuthTokensModel;
// Cache tokens and user
await Future.wait([
localDataSource.cacheTokens(tokens),
localDataSource.cacheUser(result['user']),
]);
return Right(user);
} on AuthException catch (e) {
return Left(AuthFailure(message: e.message));
} on NetworkException catch (e) {
return Left(NetworkFailure(message: e.message));
} on ServerException catch (e) {
return Left(ServerFailure(message: e.message));
} catch (e) {
return Left(ServerFailure(message: 'Unexpected error: $e'));
}
}
@override
Future<Either<Failure, User>> registerWithEmail({
required String email,
required String password,
String? displayName,
}) async {
if (!await networkInfo.isConnected) {
return const Left(NetworkFailure(message: 'No internet connection'));
}
try {
final result = await remoteDataSource.registerWithEmail(
email: email,
password: password,
displayName: displayName,
);
final user = result['user'] as User;
final tokens = result['tokens'] as AuthTokensModel;
// Cache tokens and user
await Future.wait([
localDataSource.cacheTokens(tokens),
localDataSource.cacheUser(result['user']),
]);
return Right(user);
} on AuthException catch (e) {
return Left(AuthFailure(message: e.message));
} on ValidationException catch (e) {
return Left(ValidationFailure(message: e.message));
} on NetworkException catch (e) {
return Left(NetworkFailure(message: e.message));
} on ServerException catch (e) {
return Left(ServerFailure(message: e.message));
} catch (e) {
return Left(ServerFailure(message: 'Unexpected error: $e'));
}
}
@override
Future<Either<Failure, void>> logout() async {
try {
// Get access token for backend logout
final tokens = await localDataSource.getCachedTokens();
// Attempt remote logout (best effort - doesn't fail if network unavailable)
if (await networkInfo.isConnected) {
try {
await remoteDataSource.logout(tokens.accessToken);
} catch (e) {
// Remote logout failed - continue with local logout
print('Remote logout failed: $e');
}
}
// Clear local storage (always succeeds)
await Future.wait([
localDataSource.clearTokens(),
localDataSource.clearUser(),
]);
return const Right(null);
} catch (e) {
// Logout should always succeed locally
return const Right(null);
}
}
@override
Future<Either<Failure, User>> getCurrentUser() async {
try {
// Check if tokens exist and are valid
final isLoggedIn = await localDataSource.isLoggedIn();
if (!isLoggedIn) {
return const Left(AuthFailure(message: 'No user logged in'));
}
// Try to get cached user first
try {
final cachedUser = await localDataSource.getCachedUser();
return Right(cachedUser);
} on CacheException {
// Cache miss - fetch from remote
}
// Fetch from remote if network available
if (await networkInfo.isConnected) {
final tokens = await localDataSource.getCachedTokens();
final user = await remoteDataSource.getCurrentUser(tokens.accessToken);
// Update cache
await localDataSource.cacheUser(user);
return Right(user);
} else {
return const Left(CacheFailure(message: 'User data not available offline'));
}
} on AuthException catch (e) {
return Left(AuthFailure(message: e.message));
} on NetworkException catch (e) {
return Left(NetworkFailure(message: e.message));
} on CacheException catch (e) {
return Left(CacheFailure(message: e.message));
} catch (e) {
return Left(ServerFailure(message: 'Unexpected error: $e'));
}
}
@override
Future<Either<Failure, AuthTokens>> refreshAccessToken() async {
if (!await networkInfo.isConnected) {
return const Left(NetworkFailure(message: 'No internet connection'));
}
try {
// Get current refresh token
final oldTokens = await localDataSource.getCachedTokens();
// Request new tokens
final newTokens = await remoteDataSource.refreshAccessToken(
oldTokens.refreshToken,
);
// Cache new tokens
await localDataSource.cacheTokens(newTokens);
return Right(newTokens);
} on AuthException catch (e) {
// Refresh token expired - user needs to log in again
await Future.wait([
localDataSource.clearTokens(),
localDataSource.clearUser(),
]);
return Left(AuthFailure(message: 'Session expired. Please log in again.'));
} on NetworkException catch (e) {
return Left(NetworkFailure(message: e.message));
} on ServerException catch (e) {
return Left(ServerFailure(message: e.message));
} catch (e) {
return Left(ServerFailure(message: 'Unexpected error: $e'));
}
}
@override
Future<Either<Failure, User>> verifyEmail(String token) async {
if (!await networkInfo.isConnected) {
return const Left(NetworkFailure(message: 'No internet connection'));
}
try {
final user = await remoteDataSource.verifyEmail(token);
// Update cached user
await localDataSource.cacheUser(user);
return Right(user);
} on AuthException catch (e) {
return Left(AuthFailure(message: e.message));
} on NetworkException catch (e) {
return Left(NetworkFailure(message: e.message));
} on ServerException catch (e) {
return Left(ServerFailure(message: e.message));
} catch (e) {
return Left(ServerFailure(message: 'Unexpected error: $e'));
}
}
@override
Future<Either<Failure, void>> resetPassword(String email) async {
if (!await networkInfo.isConnected) {
return const Left(NetworkFailure(message: 'No internet connection'));
}
try {
await remoteDataSource.resetPassword(email);
return const Right(null);
} on AuthException catch (e) {
return Left(AuthFailure(message: e.message));
} on NetworkException catch (e) {
return Left(NetworkFailure(message: e.message));
} on ServerException catch (e) {
return Left(ServerFailure(message: e.message));
} catch (e) {
return Left(ServerFailure(message: 'Unexpected error: $e'));
}
}
}
```
**Data Layer Summary:**
After Step 3, you should have:
- ✅ Models (JSON serialization)
- ✅ Remote Data Source (API communication)
- ✅ Local Data Source (Storage)
- ✅ Repository Implementation (orchestration)
- ✅ All files in `lib/features/{{feature_name}}/data/`
---
## Step 4: Implement Presentation Layer
The presentation layer handles UI and state management using BLoC pattern.
### 4.1 Create BLoC/Cubit
**BLoC manages UI state and handles user events.**
For each screen/feature, create BLoC files:
**File Structure:**
```
lib/features/{{feature_name}}/presentation/bloc/
├── {{feature_name}}_bloc.dart
├── {{feature_name}}_event.dart
└── {{feature_name}}_state.dart
```
**Example for Authentication Feature:**
**File:** `lib/features/auth/presentation/bloc/auth_event.dart`
```dart
import 'package:equatable/equatable.dart';
/// Base class for all authentication events.
abstract class AuthEvent extends Equatable {
const AuthEvent();
@override
List<Object?> get props => [];
}
/// Event triggered when user attempts to log in with email/password.
class LoginWithEmailEvent extends AuthEvent {
final String email;
final String password;
const LoginWithEmailEvent({
required this.email,
required this.password,
});
@override
List<Object?> get props => [email, password];
}
/// Event triggered when user attempts to register.
class RegisterWithEmailEvent extends AuthEvent {
final String email;
final String password;
final String? displayName;
const RegisterWithEmailEvent({
required this.email,
required this.password,
this.displayName,
});
@override
List<Object?> get props => [email, password, displayName];
}
/// Event triggered when user logs out.
class LogoutEvent extends AuthEvent {
const LogoutEvent();
}
/// Event triggered to check if user is already logged in (app startup).
class CheckAuthStatusEvent extends AuthEvent {
const CheckAuthStatusEvent();
}
/// Event triggered when user requests password reset.
class ResetPasswordEvent extends AuthEvent {
final String email;
const ResetPasswordEvent({required this.email});
@override
List<Object?> get props => [email];
}
/// Event triggered when user verifies email with token.
class VerifyEmailEvent extends AuthEvent {
final String token;
const VerifyEmailEvent({required this.token});
@override
List<Object?> get props => [token];
}
```
**File:** `lib/features/auth/presentation/bloc/auth_state.dart`
```dart
import 'package:equatable/equatable.dart';
import '../../domain/entities/user.dart';
/// Base class for all authentication states.
abstract class AuthState extends Equatable {
const AuthState();
@override
List<Object?> get props => [];
}
/// Initial state when app starts.
class AuthInitial extends AuthState {
const AuthInitial();
}
/// State when authentication operation is in progress.
class AuthLoading extends AuthState {
const AuthLoading();
}
/// State when user is authenticated.
class Authenticated extends AuthState {
final User user;
const Authenticated({required this.user});
@override
List<Object?> get props => [user];
}
/// State when user is not authenticated.
class Unauthenticated extends AuthState {
const Unauthenticated();
}
/// State when authentication operation fails.
class AuthError extends AuthState {
final String message;
const AuthError({required this.message});
@override
List<Object?> get props => [message];
}
/// State when password reset email sent successfully.
class PasswordResetSent extends AuthState {
const PasswordResetSent();
}
/// State when email verification succeeds.
class EmailVerified extends AuthState {
final User user;
const EmailVerified({required this.user});
@override
List<Object?> get props => [user];
}
```
**File:** `lib/features/auth/presentation/bloc/auth_bloc.dart`
```dart
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/usecases/login_with_email.dart';
import '../../domain/usecases/register_with_email.dart';
import '../../domain/usecases/logout.dart';
import '../../domain/usecases/get_current_user.dart';
import '../../domain/usecases/reset_password.dart';
import '../../domain/usecases/verify_email.dart';
import 'auth_event.dart';
import 'auth_state.dart';
/// BLoC for authentication feature.
///
/// Manages authentication state and handles user events.
///
/// **States:**
/// - AuthInitial: App just started
/// - AuthLoading: Operation in progress
/// - Authenticated: User logged in
/// - Unauthenticated: User logged out
/// - AuthError: Error occurred
///
/// **Events:**
/// - LoginWithEmailEvent: User attempts login
/// - RegisterWithEmailEvent: User attempts registration
/// - LogoutEvent: User logs out
/// - CheckAuthStatusEvent: Check if user already logged in
/// - ResetPasswordEvent: User requests password reset
/// - VerifyEmailEvent: User verifies email
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final LoginWithEmail loginWithEmail;
final RegisterWithEmail registerWithEmail;
final Logout logout;
final GetCurrentUser getCurrentUser;
final ResetPassword resetPassword;
final VerifyEmail verifyEmail;
AuthBloc({
required this.loginWithEmail,
required this.registerWithEmail,
required this.logout,
required this.getCurrentUser,
required this.resetPassword,
required this.verifyEmail,
}) : super(const AuthInitial()) {
// Register event handlers
on<LoginWithEmailEvent>(_onLoginWithEmail);
on<RegisterWithEmailEvent>(_onRegisterWithEmail);
on<LogoutEvent>(_onLogout);
on<CheckAuthStatusEvent>(_onCheckAuthStatus);
on<ResetPasswordEvent>(_onResetPassword);
on<VerifyEmailEvent>(_onVerifyEmail);
}
/// Handles login event.
Future<void> _onLoginWithEmail(
LoginWithEmailEvent event,
Emitter<AuthState> emit,
) async {
emit(const AuthLoading());
final params = LoginParams(
email: event.email,
password: event.password,
);
final result = await loginWithEmail(params);
result.fold(
(failure) => emit(AuthError(message: failure.message)),
(user) => emit(Authenticated(user: user)),
);
}
/// Handles registration event.
Future<void> _onRegisterWithEmail(
RegisterWithEmailEvent event,
Emitter<AuthState> emit,
) async {
emit(const AuthLoading());
final params = RegisterParams(
email: event.email,
password: event.password,
displayName: event.displayName,
);
final result = await registerWithEmail(params);
result.fold(
(failure) => emit(AuthError(message: failure.message)),
(user) => emit(Authenticated(user: user)),
);
}
/// Handles logout event.
Future<void> _onLogout(
LogoutEvent event,
Emitter<AuthState> emit,
) async {
emit(const AuthLoading());
final result = await logout(NoParams());
result.fold(
(failure) => emit(AuthError(message: failure.message)),
(_) => emit(const Unauthenticated()),
);
}
/// Handles check auth status event (app startup).
Future<void> _onCheckAuthStatus(
CheckAuthStatusEvent event,
Emitter<AuthState> emit,
) async {
emit(const AuthLoading());
final result = await getCurrentUser(NoParams());
result.fold(
(failure) => emit(const Unauthenticated()),
(user) => emit(Authenticated(user: user)),
);
}
/// Handles password reset event.
Future<void> _onResetPassword(
ResetPasswordEvent event,
Emitter<AuthState> emit,
) async {
emit(const AuthLoading());
final params = ResetPasswordParams(email: event.email);
final result = await resetPassword(params);
result.fold(
(failure) => emit(AuthError(message: failure.message)),
(_) => emit(const PasswordResetSent()),
);
}
/// Handles email verification event.
Future<void> _onVerifyEmail(
VerifyEmailEvent event,
Emitter<AuthState> emit,
) async {
emit(const AuthLoading());
final params = VerifyEmailParams(token: event.token);
final result = await verifyEmail(params);
result.fold(
(failure) => emit(AuthError(message: failure.message)),
(user) => emit(EmailVerified(user: user)),
);
}
}
// Note: NoParams, ResetPasswordParams, VerifyEmailParams need to be created
// in their respective use case files
```
### 4.2 Create UI Screens
**Screens are the top-level pages users navigate to.**
**File:** `lib/features/{{feature_name}}/presentation/pages/{{screen_name}}.dart`
**Example for Authentication Feature:**
**File:** `lib/features/auth/presentation/pages/login_screen.dart`
```dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/di/injection_container.dart';
import '../bloc/auth_bloc.dart';
import '../bloc/auth_event.dart';
import '../bloc/auth_state.dart';
import '../widgets/custom_text_field.dart';
import '../widgets/primary_button.dart';
/// Login screen for email/password authentication.
///
/// **Features:**
/// - Email/password input fields
/// - Form validation
/// - Loading indicator during authentication
/// - Error messages
/// - Navigation to registration and password reset
///
/// **Navigation:**
/// - On successful login: Navigate to HomeScreen
/// - "Sign Up" button: Navigate to RegisterScreen
/// - "Forgot Password?" link: Navigate to ForgotPasswordScreen
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
void _onLoginPressed() {
if (_formKey.currentState!.validate()) {
context.read<AuthBloc>().add(
LoginWithEmailEvent(
email: _emailController.text.trim(),
password: _passwordController.text,
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Log In'),
),
body: BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: Colors.red,
),
);
} else if (state is Authenticated) {
// Navigate to home screen
Navigator.of(context).pushReplacementNamed('/home');
}
},
builder: (context, state) {
final isLoading = state is AuthLoading;
return Padding(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Email field
CustomTextField(
controller: _emailController,
labelText: 'Email',
keyboardType: TextInputType.emailAddress,
enabled: !isLoading,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
final emailRegex = RegExp(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
);
if (!emailRegex.hasMatch(value)) {
return 'Please enter a valid email';
}
return null;
},
),
const SizedBox(height: 16),
// Password field
CustomTextField(
controller: _passwordController,
labelText: 'Password',
obscureText: _obscurePassword,
enabled: !isLoading,
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility_off
: Icons.visibility,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your password';
}
if (value.length < 8) {
return 'Password must be at least 8 characters';
}
return null;
},
),
const SizedBox(height: 8),
// Forgot password link
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: isLoading
? null
: () {
Navigator.of(context).pushNamed('/forgot-password');
},
child: const Text('Forgot Password?'),
),
),
const SizedBox(height: 24),
// Login button
PrimaryButton(
onPressed: isLoading ? null : _onLoginPressed,
isLoading: isLoading,
child: const Text('Log In'),
),
const SizedBox(height: 16),
// Sign up link
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("Don't have an account?"),
TextButton(
onPressed: isLoading
? null
: () {
Navigator.of(context).pushNamed('/register');
},
child: const Text('Sign Up'),
),
],
),
],
),
),
);
},
),
);
}
}
```
**Create additional screens:**
- `register_screen.dart` - User registration
- `forgot_password_screen.dart` - Password reset initiation
### 4.3 Create Reusable Widgets
**Widgets are reusable UI components.**
**File:** `lib/features/auth/presentation/widgets/custom_text_field.dart`
```dart
import 'package:flutter/material.dart';
/// Custom text field with consistent styling.
///
/// **Features:**
/// - Consistent design across app
/// - Built-in validation
/// - Password visibility toggle support
/// - Disabled state styling
class CustomTextField extends StatelessWidget {
final TextEditingController controller;
final String labelText;
final String? hintText;
final TextInputType? keyboardType;
final bool obscureText;
final bool enabled;
final Widget? suffixIcon;
final String? Function(String?)? validator;
const CustomTextField({
super.key,
required this.controller,
required this.labelText,
this.hintText,
this.keyboardType,
this.obscureText = false,
this.enabled = true,
this.suffixIcon,
this.validator,
});
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
keyboardType: keyboardType,
obscureText: obscureText,
enabled: enabled,
validator: validator,
decoration: InputDecoration(
labelText: labelText,
hintText: hintText,
suffixIcon: suffixIcon,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.red),
),
disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
),
);
}
}
```
**File:** `lib/features/auth/presentation/widgets/primary_button.dart`
```dart
import 'package:flutter/material.dart';
/// Primary button with loading state support.
///
/// **Features:**
/// - Loading indicator replaces text
/// - Disabled state when loading or onPressed is null
/// - Consistent styling
class PrimaryButton extends StatelessWidget {
final VoidCallback? onPressed;
final Widget child;
final bool isLoading;
const PrimaryButton({
super.key,
required this.onPressed,
required this.child,
this.isLoading = false,
});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: child,
);
}
}
```
**Presentation Layer Summary:**
After Step 4, you should have:
- ✅ BLoC (state management)
- ✅ Events (user actions)
- ✅ States (UI states)
- ✅ Screens (pages)
- ✅ Widgets (reusable components)
- ✅ All files in `lib/features/{{feature_name}}/presentation/`
---
## Step 5: Write Tests
Generate comprehensive tests for all layers.
### 5.1 Unit Tests for Domain Layer
**Test use cases, entities, and business logic.**
**File:** `test/features/{{feature_name}}/domain/usecases/{{use_case_name}}_test.dart`
**Example:**
**File:** `test/features/auth/domain/usecases/login_with_email_test.dart`
```dart
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:myapp/core/error/failures.dart';
import 'package:myapp/features/auth/domain/entities/user.dart';
import 'package:myapp/features/auth/domain/repositories/auth_repository.dart';
import 'package:myapp/features/auth/domain/usecases/login_with_email.dart';
import 'login_with_email_test.mocks.dart';
@GenerateMocks([AuthRepository])
void main() {
late LoginWithEmail usecase;
late MockAuthRepository mockAuthRepository;
setUp(() {
mockAuthRepository = MockAuthRepository();
usecase = LoginWithEmail(mockAuthRepository);
});
const tEmail = 'test@example.com';
const tPassword = 'SecurePass123!';
const tUser = User(
id: '123',
email: tEmail,
emailVerified: true,
createdAt: '2024-01-15T10:30:00Z',
);
test(
'should return User when repository login succeeds',
() async {
// arrange
when(mockAuthRepository.loginWithEmail(
email: anyNamed('email'),
password: anyNamed('password'),
)).thenAnswer((_) async => const Right(tUser));
// act
final result = await usecase(const LoginParams(
email: tEmail,
password: tPassword,
));
// assert
expect(result, const Right(tUser));
verify(mockAuthRepository.loginWithEmail(
email: tEmail,
password: tPassword,
));
verifyNoMoreInteractions(mockAuthRepository);
},
);
test(
'should return AuthFailure when credentials are invalid',
() async {
// arrange
const tFailure = AuthFailure(message: 'Invalid credentials');
when(mockAuthRepository.loginWithEmail(
email: anyNamed('email'),
password: anyNamed('password'),
)).thenAnswer((_) async => const Left(tFailure));
// act
final result = await usecase(const LoginParams(
email: tEmail,
password: tPassword,
));
// assert
expect(result, const Left(tFailure));
},
);
test(
'should return ValidationFailure when email is invalid',
() async {
// arrange
const tInvalidEmail = 'invalid-email';
// act
final result = await usecase(const LoginParams(
email: tInvalidEmail,
password: tPassword,
));
// assert
expect(result.isLeft(), true);
result.fold(
(failure) => expect(failure, isA<ValidationFailure>()),
(_) => fail('Should return failure'),
);
verifyZeroInteractions(mockAuthRepository);
},
);
test(
'should return ValidationFailure when password is too short',
() async {
// arrange
const tShortPassword = '1234567'; // Only 7 characters
// act
final result = await usecase(const LoginParams(
email: tEmail,
password: tShortPassword,
));
// assert
expect(result.isLeft(), true);
result.fold(
(failure) => expect(failure, isA<ValidationFailure>()),
(_) => fail('Should return failure'),
);
verifyZeroInteractions(mockAuthRepository);
},
);
}
```
### 5.2 Unit Tests for Data Layer
**Test models, data sources, and repositories.**
**File:** `test/features/{{feature_name}}/data/models/{{model_name}}_test.dart`
**Example:**
**File:** `test/features/auth/data/models/user_model_test.dart`
```dart
import 'package:flutter_test/flutter_test.dart';
import 'dart:convert';
import 'package:myapp/features/auth/data/models/user_model.dart';
import 'package:myapp/features/auth/domain/entities/user.dart';
import '../../../../fixtures/fixture_reader.dart';
void main() {
const tUserModel = UserModel(
id: '123',
email: 'test@example.com',
displayName: 'Test User',
photoUrl: 'https://example.com/photo.jpg',
emailVerified: true,
createdAt: '2024-01-15T10:30:00Z',
);
test('should be a subclass of User entity', () {
// assert
expect(tUserModel, isA<User>());
});
group('fromJson', () {
test('should return a valid model from JSON', () {
// arrange
final Map<String, dynamic> jsonMap = json.decode(
fixture('user.json'),
);
// act
final result = UserModel.fromJson(jsonMap);
// assert
expect(result, tUserModel);
});
test('should handle null optional fields', () {
// arrange
final Map<String, dynamic> jsonMap = {
'id': '123',
'email': 'test@example.com',
'display_name': null,
'photo_url': null,
'email_verified': true,
'created_at': '2024-01-15T10:30:00Z',
};
// act
final result = UserModel.fromJson(jsonMap);
// assert
expect(result.displayName, null);
expect(result.photoUrl, null);
});
});
group('toJson', () {
test('should return a JSON map containing proper data', () {
// act
final result = tUserModel.toJson();
// assert
final expectedMap = {
'id': '123',
'email': 'test@example.com',
'display_name': 'Test User',
'photo_url': 'https://example.com/photo.jpg',
'email_verified': true,
'created_at': '2024-01-15T10:30:00Z',
};
expect(result, expectedMap);
});
});
group('fromEntity', () {
test('should convert User entity to UserModel', () {
// arrange
const tUser = User(
id: '123',
email: 'test@example.com',
displayName: 'Test User',
photoUrl: 'https://example.com/photo.jpg',
emailVerified: true,
createdAt: '2024-01-15T10:30:00Z',
);
// act
final result = UserModel.fromEntity(tUser);
// assert
expect(result, tUserModel);
});
});
}
```
### 5.3 Widget Tests for Presentation Layer
**Test UI screens and widgets.**
**File:** `test/features/{{feature_name}}/presentation/pages/{{screen_name}}_test.dart`
**Example:**
**File:** `test/features/auth/presentation/pages/login_screen_test.dart`
```dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:myapp/features/auth/presentation/bloc/auth_bloc.dart';
import 'package:myapp/features/auth/presentation/bloc/auth_state.dart';
import 'package:myapp/features/auth/presentation/pages/login_screen.dart';
import 'login_screen_test.mocks.dart';
@GenerateMocks([AuthBloc])
void main() {
late MockAuthBloc mockAuthBloc;
setUp(() {
mockAuthBloc = MockAuthBloc();
});
Widget createWidgetUnderTest() {
return MaterialApp(
home: BlocProvider<AuthBloc>.value(
value: mockAuthBloc,
child: const LoginScreen(),
),
);
}
testWidgets('should display email and password fields', (tester) async {
// arrange
when(mockAuthBloc.state).thenReturn(const AuthInitial());
when(mockAuthBloc.stream).thenAnswer((_) => const Stream.empty());
// act
await tester.pumpWidget(createWidgetUnderTest());
// assert
expect(find.byType(TextField), findsNWidgets(2));
expect(find.text('Email'), findsOneWidget);
expect(find.text('Password'), findsOneWidget);
});
testWidgets('should display Log In button', (tester) async {
// arrange
when(mockAuthBloc.state).thenReturn(const AuthInitial());
when(mockAuthBloc.stream).thenAnswer((_) => const Stream.empty());
// act
await tester.pumpWidget(createWidgetUnderTest());
// assert
expect(find.widgetWithText(ElevatedButton, 'Log In'), findsOneWidget);
});
testWidgets('should show loading indicator when state is AuthLoading',
(tester) async {
// arrange
when(mockAuthBloc.state).thenReturn(const AuthLoading());
when(mockAuthBloc.stream).thenAnswer((_) => const Stream.empty());
// act
await tester.pumpWidget(createWidgetUnderTest());
// assert
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
testWidgets('should show error snackbar when state is AuthError',
(tester) async {
// arrange
when(mockAuthBloc.state).thenReturn(const AuthInitial());
when(mockAuthBloc.stream).thenAnswer(
(_) => Stream.value(const AuthError(message: 'Invalid credentials')),
);
// act
await tester.pumpWidget(createWidgetUnderTest());
await tester.pump(); // Trigger snackbar
// assert
expect(find.text('Invalid credentials'), findsOneWidget);
expect(find.byType(SnackBar), findsOneWidget);
});
testWidgets('should validate email format', (tester) async {
// arrange
when(mockAuthBloc.state).thenReturn(const AuthInitial());
when(mockAuthBloc.stream).thenAnswer((_) => const Stream.empty());
// act
await tester.pumpWidget(createWidgetUnderTest());
// Enter invalid email
await tester.enterText(find.byType(TextField).first, 'invalid-email');
await tester.enterText(find.byType(TextField).last, 'SecurePass123!');
// Tap login button
await tester.tap(find.widgetWithText(ElevatedButton, 'Log In'));
await tester.pump();
// assert
expect(find.text('Please enter a valid email'), findsOneWidget);
});
}
```
### 5.4 Integration Tests
**Test complete feature flows.**
**File:** `integration_test/{{feature_name}}_test.dart`
**Example:**
**File:** `integration_test/auth_test.dart`
```dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:myapp/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Authentication Flow', () {
testWidgets('complete login flow', (tester) async {
// Start app
app.main();
await tester.pumpAndSettle();
// Find email and password fields
final emailField = find.byKey(const Key('email_field'));
final passwordField = find.byKey(const Key('password_field'));
final loginButton = find.widgetWithText(ElevatedButton, 'Log In');
// Enter credentials
await tester.enterText(emailField, 'test@example.com');
await tester.enterText(passwordField, 'SecurePass123!');
// Tap login button
await tester.tap(loginButton);
await tester.pumpAndSettle();
// Verify navigation to home screen
expect(find.text('Home'), findsOneWidget);
});
testWidgets('complete registration flow', (tester) async {
// Start app
app.main();
await tester.pumpAndSettle();
// Navigate to registration
final signUpButton = find.text('Sign Up');
await tester.tap(signUpButton);
await tester.pumpAndSettle();
// Find registration fields
final emailField = find.byKey(const Key('email_field'));
final passwordField = find.byKey(const Key('password_field'));
final displayNameField = find.byKey(const Key('display_name_field'));
final registerButton = find.widgetWithText(ElevatedButton, 'Sign Up');
// Enter registration data
await tester.enterText(emailField, 'newuser@example.com');
await tester.enterText(passwordField, 'SecurePass123!');
await tester.enterText(displayNameField, 'New User');
// Tap register button
await tester.tap(registerButton);
await tester.pumpAndSettle();
// Verify success (email verification screen or home)
expect(find.text('Verify Your Email'), findsOneWidget);
});
});
}
```
**Test Summary:**
After Step 5, you should have:
- ✅ Unit tests for use cases (70%+ coverage)
- ✅ Unit tests for repositories
- ✅ Unit tests for models
- ✅ Widget tests for screens
- ✅ Widget tests for widgets
- ✅ Integration test for complete flow
- ✅ All tests in `test/` and `integration_test/` directories
---
## Step 6: Validation and Security Checks
### 6.1 Run Flutter Analyze
```bash
flutter analyze
```
**Fix any issues found:**
- Linting warnings
- Unused imports
- Type errors
- Dead code
### 6.2 Run Tests
```bash
flutter test --coverage
```
**Verify coverage meets target:**
```bash
# Generate coverage report
genhtml coverage/lcov.info -o coverage/html
# Check coverage percentage
lcov --summary coverage/lcov.info
```
**Expected output:**
```
Overall coverage rate:
lines......: 75.3% (1234 of 1638 lines)
functions..: 78.1% (345 of 442 functions)
```
**If coverage < {{test_coverage_target}}%:**
- Write additional tests for uncovered code
- Focus on critical paths (authentication, security)
### 6.3 Security Validation
**Check security patterns from PRPROMPTS files:**
Read relevant PRPROMPTS files:
```bash
cat PRPROMPTS/16-security_and_compliance.md
cat PRPROMPTS/08-authentication_and_authorization.md
```
**Validate:**
1. **JWT Token Handling:**
- ✅ Tokens stored in FlutterSecureStorage (encrypted)
- ✅ NEVER verify JWT in Flutter (backend only)
- ✅ Refresh token rotation implemented
- ✅ Tokens cleared on logout
2. **Password Security:**
- ✅ Passwords NEVER stored locally
- ✅ HTTPS used for all auth endpoints
- ✅ Strong password validation (8+ chars, uppercase, lowercase, digit, special)
3. **Error Handling:**
- ✅ Generic error messages (don't reveal if email exists)
- ✅ Rate limiting on backend
- ✅ Account lockout after failed attempts
4. **Compliance (if applicable):**
- ✅ HIPAA: PHI encrypted at rest, audit logging
- ✅ PCI-DSS: No card storage, tokenization used
- ✅ GDPR: User consent, data minimization
**Security Checklist:**
```markdown
## Security Validation for {{feature_name}}
- [ ] Sensitive data encrypted in storage
- [ ] HTTPS enforced for all API calls
- [ ] Passwords NEVER stored locally
- [ ] JWT tokens stored in FlutterSecureStorage
- [ ] Token expiration handled correctly
- [ ] Refresh token rotation implemented
- [ ] Error messages don't leak information
- [ ] Input validation on all user inputs
- [ ] No hardcoded secrets in code
- [ ] Compliance requirements met (if applicable)
```
### 6.4 Architecture Validation
**Verify Clean Architecture compliance:**
```bash
# Check folder structure
tree lib/features/{{feature_name}}
```
**Expected structure:**
```
lib/features/{{feature_name}}/
├── domain/
│ ├── entities/
│ ├── repositories/
│ └── usecases/
├── data/
│ ├── models/
│ ├── datasources/
│ └── repositories/
└── presentation/
├── bloc/
├── pages/
└── widgets/
```
**Validate dependencies:**
- ✅ Domain layer has NO dependencies on other layers
- ✅ Data layer depends only on domain
- ✅ Presentation layer depends only on domain
- ✅ No circular dependencies
---
## Step 7: Documentation and Summary
### 7.1 Generate Feature Documentation
**Create:** `docs/features/{{feature_name}}.md`
**Template:**
```markdown
# {{FeatureName}} Feature
## Overview
{{Brief description of what this feature does}}
## Implementation Details
### Domain Layer
- **Entities:** {{List entities}}
- **Use Cases:** {{List use cases}}
- **Repository Interface:** {{Repository name}}
### Data Layer
- **Models:** {{List models}}
- **Remote Data Source:** {{API endpoints}}
- **Local Data Source:** {{Storage keys}}
- **Repository Implementation:** {{Implementation details}}
### Presentation Layer
- **BLoC:** {{BLoC name}}
- **Events:** {{List events}}
- **States:** {{List states}}
- **Screens:** {{List screens}}
- **Widgets:** {{List widgets}}
## API Endpoints
{{List all API endpoints with methods and descriptions}}
## Security Considerations
{{List security measures implemented}}
## Tests
- **Unit Tests:** {{Number}} files, {{Coverage}}% coverage
- **Widget Tests:** {{Number}} files
- **Integration Tests:** {{Number}} flows
## Files Created
{{List all files created with paths}}
## Usage
{{How to use this feature in the app}}
## Known Issues / TODOs
{{List any pending items or known issues}}
```
### 7.2 Update IMPLEMENTATION_PLAN.md
**Mark feature as completed:**
```markdown
### Feature X: {{feature_name}}
**Priority:** HIGH
**Status:** ✅ COMPLETED
**Completed:** {{Date}}
**Implementation Time:** {{Actual time taken}}
**Files Created:** {{Total count}}
- Domain: {{Count}} files
- Data: {{Count}} files
- Presentation: {{Count}} files
- Tests: {{Count}} files
**Test Coverage:** {{Actual coverage}}%
**Notes:**
- {{Any important notes or deviations from plan}}
```
### 7.3 Create Summary Report
**Output the following summary:**
```markdown
# Feature Implementation Summary: {{feature_name}}
## ✅ Completion Status
**Feature:** {{feature_name}}
**Status:** Implemented and Tested
**Implementation Time:** {{X}} minutes
**Date:** {{Current date}}
## 📊 Statistics
### Files Created
- **Domain Layer:** {{count}} files
- **Data Layer:** {{count}} files
- **Presentation Layer:** {{count}} files
- **Tests:** {{count}} files
- **Total:** {{total}} files
### Lines of Code
- **Domain:** ~{{count}} lines
- **Data:** ~{{count}} lines
- **Presentation:** ~{{count}} lines
- **Tests:** ~{{count}} lines
- **Total:** ~{{total}} lines
### Test Coverage
- **Target:** {{test_coverage_target}}%
- **Achieved:** {{actual_coverage}}%
- **Status:** {{✅ Met / ❌ Below Target}}
## 🎯 Validation Results
### Flutter Analyze
{{Output of flutter analyze}}
### Tests
- **Unit Tests:** {{X}} passed, {{Y}} failed
- **Widget Tests:** {{X}} passed, {{Y}} failed
- **Integration Tests:** {{X}} passed, {{Y}} failed
### Security Checks
- [ ] JWT tokens stored securely
- [ ] Passwords never stored locally
- [ ] HTTPS enforced
- [ ] Input validation implemented
- [ ] Compliance requirements met
### Architecture Compliance
- [ ] Clean Architecture structure followed
- [ ] Domain layer independent
- [ ] Proper dependency direction
- [ ] Repository Pattern implemented
- [ ] BLoC Pattern implemented
## 📁 Files Created
### Domain Layer
{{List domain files with paths}}
### Data Layer
{{List data files with paths}}
### Presentation Layer
{{List presentation files with paths}}
### Tests
{{List test files with paths}}
## 🔒 Security Implementation
{{List security measures implemented}}
## 📝 Next Steps
{{If feature is part of larger workflow, suggest next feature to implement}}
## ⚠️ Issues / Warnings
{{List any issues found or warnings to address}}
---
**Feature implementation completed successfully!** ✅
```
---
## Error Handling
**If any step fails:**
1. **Identify the error:**
- Syntax error in generated code
- Missing dependency
- Invalid API endpoint
- Test failure
2. **Fix the issue:**
- Correct the code
- Install missing package
- Update API configuration
- Fix test logic
3. **Re-run validation:**
```bash
flutter analyze
flutter test
```
4. **Document the issue:**
- Add to feature documentation under "Known Issues"
- Update IMPLEMENTATION_PLAN.md with notes
5. **Continue with next step**
---
## Skill Completion
When all steps are complete:
**Output:**
```
✅ Feature "{{feature_name}}" implemented successfully!
📊 Summary:
- Files Created: {{total}}
- Test Coverage: {{coverage}}%
- Security Validated: Yes
- Architecture Compliant: Yes
🎉 Ready for code review and integration!
```
**Next Actions:**
1. Commit changes to version control
2. Create pull request
3. Request code review
4. Run CI/CD pipeline
5. Deploy to staging environment
---
**End of Skill Execution**