Appearance
🌐 API Management
Overview
Use Dio, Retrofit and Retrofit Generator for API handling.
The API management is located in lib/shared/data/network/ directory, which contains:
api_endpoints.dart: Contains all API endpointsapi_response.dart: Generic response wrapper with error handlingcancellable_repository.dart: Mixin for handling cancellable requestserror_response.dart: API error response model with utility methodsresponse_status.dart: Constants for status codes and error identifiersinterceptor.dart: Custom Dio interceptor for authentication and error handling
Request/Response Format
Standard Response Structure
The API uses a generic ApiResponse<T> class where:
Trepresents the main data type
dart
class ApiResponse<T> {
Status? _status;
T? data;
String? errorMessage;
ErrorResponse? errorData;
}Status Types
dart
enum Status {
completed, // Successful response
error, // Error occurred
sessionExpired // Authentication token expired
// ...etc.
}Code Standards
1. Adding New API Endpoints
Step 1: Add endpoint constant Add your endpoint to api_endpoints.dart:
dart
class ApiEndpoints {
static const String myNewFeature = '/api/my-new-feature';
}Step 2: Create service method In your feature's service file, add the API method:
dart
@GET(ApiEndpoints.myNewFeature)
Future<ApiResponse<MyModel, MyExtraData>> getMyData(
@Query('param') String param,
);Step 3: Generate code Run the build runner command:
bash
flutter pub run build_runner build --delete-conflicting-outputsStep 4: Implement repository method In your feature's repository, handle the response:
dart
Future<Either<Failure, MyEntity>> getMyData(String param) async {
final response = await _service.getMyData(param);
if (response.hasSucceeded) {
return Right(response.data!.toEntity());
} else {
return response.onFail();
}
}2. Error Handling Guidelines
- Always use the
onFail()extension method for error handling - Never manually handle errors - use the built-in error handling
- Return
Leftwith appropriateFailuretype - the extension handles this automatically - Never use
!operator on API responses without checkinghasSucceededfirst
Correct approach:
dart
Future<Either<Failure, MyEntity>> getData() async {
final response = await _service.getData();
if (response.hasSucceeded) {
return Right(response.data!.toEntity());
} else {
return response.onFail();
}
}Wrong approach:
dart
// ❌ Don't do this
final data = response.data!; // May crash if response failed3. Authentication
- Authentication tokens are automatically added by the interceptor
- Don't manually add authorization headers in service methods
- The interceptor handles token refresh and session management
- If you need custom headers, add them in
interceptor.dart
4. HTTP Methods
Use the appropriate HTTP method annotations:
dart
@GET(ApiEndpoints.endpoint) // For GET requests
@POST(ApiEndpoints.endpoint) // For POST requests
@PUT(ApiEndpoints.endpoint) // For PUT requests
@DELETE(ApiEndpoints.endpoint) // For DELETE requests
@PATCH(ApiEndpoints.endpoint) // For PATCH requests5. Request Parameters
Use appropriate parameter annotations:
dart
@Query('param') String param // Query parameters
@Path('id') String id // Path parameters
@Body() Map<String, dynamic> body // Request body
@Header('Custom-Header') String header // Custom headers6. Cancellable Requests
- Always extend
CancellableRepositorymixin when creating repositories - Use
CancelTokenfor requests that can be cancelled - Cancel requests when navigating away from a screen to avoid memory leaks
Example:
dart
class MyRepository with CancellableRepository {
late CancelToken _cancelToken;
Future<Either<Failure, MyEntity>> getData() async {
_cancelToken = CancelToken();
final response = await _service.getData(cancelToken: _cancelToken);
// ... handle response
}
}7. Response Data Transformation
- Always convert models to entities using
toEntity()method - Never expose models directly to the domain/presentation layer
- Create entity classes in the domain layer for each model
Example:
dart
// In repository
if (response.hasSucceeded) {
return Right(response.data!.toEntity());
}
// In domain layer
abstract class MyEntity {
final String id;
final String name;
MyEntity({required this.id, required this.name});
}Best Practices
Do's ✅
- Use generic
ApiResponse<T, U>for all API responses - Always check
hasSucceededbefore accessing data - Use
onFail()extension for error handling - Convert models to entities in the repository layer
- Add endpoints to
api_endpoints.dartfor consistency - Run build_runner after adding new service methods
- Use appropriate HTTP method annotations
- Document complex API calls with comments
Don'ts ❌
- Don't use
!operator without checking response status - Don't manually handle errors when
onFail()is available - Don't expose models directly to other layers
- Don't add authorization headers manually in services
- Don't create API methods without annotations
- Don't skip running build_runner
- Don't ignore the status enum values
- Don't forget to cancel requests when navigating away
Error Types
The system handles these error types automatically:
- ConnectionFailure: No internet connection
- ServerFailure: Server-side errors
- DevelopmentFailure: Development environment errors
- SessionExpired: Authentication token expired
- CancelledRequests: Request was cancelled
Use the onFail() extension method which handles all these cases automatically.
Testing API Calls
When writing tests for API calls:
dart
test('should return entity when API call is successful', () async {
// Arrange
when(() => mockService.getData()).thenAnswer(
(_) async => ApiResponse<MyModel, dynamic>(
data: MyModel(id: '1', name: 'Test'),
hasSucceeded: true,
),
);
// Act
final result = await repository.getData();
// Assert
expect(result, isA<Right>());
final entity = result.getOrElse(() => throw Exception('Expected Right'));
expect(entity.name, 'Test');
});