This commit is contained in:
2025-10-28 00:09:46 +07:00
parent 9ebe7c2919
commit de49f564b1
110 changed files with 15392 additions and 3996 deletions

View File

@@ -0,0 +1,246 @@
import 'package:equatable/equatable.dart';
/// Generic API response wrapper that handles the standard API response format
///
/// All API responses follow this structure:
/// ```json
/// {
/// "Value": T,
/// "IsSuccess": bool,
/// "IsFailure": bool,
/// "Errors": List<String>,
/// "ErrorCodes": List<String>
/// }
/// ```
///
/// Usage:
/// ```dart
/// final response = ApiResponse.fromJson(
/// jsonData,
/// (json) => User.fromJson(json),
/// );
///
/// if (response.isSuccess && response.value != null) {
/// // Handle success
/// final user = response.value!;
/// } else {
/// // Handle error
/// final errorMessage = response.errors.first;
/// }
/// ```
class ApiResponse<T> extends Equatable {
/// The actual data/payload of the response
/// Can be null if the API call failed or returned no data
final T? value;
/// Indicates if the API call was successful
final bool isSuccess;
/// Indicates if the API call failed
final bool isFailure;
/// List of error messages if the call failed
final List<String> errors;
/// List of error codes for programmatic error handling
final List<String> errorCodes;
const ApiResponse({
this.value,
required this.isSuccess,
required this.isFailure,
this.errors = const [],
this.errorCodes = const [],
});
/// Create an ApiResponse from JSON
///
/// The [fromJsonT] function is used to deserialize the "Value" field.
/// If null, the value is used as-is.
///
/// Example:
/// ```dart
/// // For single object
/// ApiResponse.fromJson(json, (j) => User.fromJson(j))
///
/// // For list of objects
/// ApiResponse.fromJson(
/// json,
/// (j) => (j as List).map((e) => User.fromJson(e)).toList()
/// )
///
/// // For primitive types or no conversion needed
/// ApiResponse.fromJson(json, null)
/// ```
factory ApiResponse.fromJson(
Map<String, dynamic> json,
T Function(dynamic)? fromJsonT,
) {
return ApiResponse(
value: json['Value'] != null && fromJsonT != null
? fromJsonT(json['Value'])
: json['Value'] as T?,
isSuccess: json['IsSuccess'] ?? false,
isFailure: json['IsFailure'] ?? true,
errors: json['Errors'] != null
? List<String>.from(json['Errors'])
: const [],
errorCodes: json['ErrorCodes'] != null
? List<String>.from(json['ErrorCodes'])
: const [],
);
}
/// Create a successful response (useful for testing or manual creation)
factory ApiResponse.success(T value) {
return ApiResponse(
value: value,
isSuccess: true,
isFailure: false,
);
}
/// Create a failed response (useful for testing or manual creation)
factory ApiResponse.failure({
required List<String> errors,
List<String>? errorCodes,
}) {
return ApiResponse(
isSuccess: false,
isFailure: true,
errors: errors,
errorCodes: errorCodes ?? const [],
);
}
/// Check if response has data
bool get hasValue => value != null;
/// Get the first error message if available
String? get firstError => errors.isNotEmpty ? errors.first : null;
/// Get the first error code if available
String? get firstErrorCode => errorCodes.isNotEmpty ? errorCodes.first : null;
/// Get a combined error message from all errors
String get combinedErrorMessage {
if (errors.isEmpty) return 'An unknown error occurred';
return errors.join(', ');
}
/// Convert to a map (useful for serialization or debugging)
Map<String, dynamic> toJson(Object? Function(T)? toJsonT) {
return {
'Value': value != null && toJsonT != null ? toJsonT(value as T) : value,
'IsSuccess': isSuccess,
'IsFailure': isFailure,
'Errors': errors,
'ErrorCodes': errorCodes,
};
}
/// Create a copy with modified fields
ApiResponse<T> copyWith({
T? value,
bool? isSuccess,
bool? isFailure,
List<String>? errors,
List<String>? errorCodes,
}) {
return ApiResponse(
value: value ?? this.value,
isSuccess: isSuccess ?? this.isSuccess,
isFailure: isFailure ?? this.isFailure,
errors: errors ?? this.errors,
errorCodes: errorCodes ?? this.errorCodes,
);
}
@override
List<Object?> get props => [value, isSuccess, isFailure, errors, errorCodes];
@override
String toString() {
if (isSuccess) {
return 'ApiResponse.success(value: $value)';
} else {
return 'ApiResponse.failure(errors: $errors, errorCodes: $errorCodes)';
}
}
}
/// Extension to convert ApiResponse to nullable value easily
extension ApiResponseExtension<T> on ApiResponse<T> {
/// Get value if success, otherwise return null
T? get valueOrNull => isSuccess ? value : null;
/// Get value if success, otherwise throw exception with error message
T get valueOrThrow {
if (isSuccess && value != null) {
return value!;
}
throw Exception(combinedErrorMessage);
}
}
/// Specialized API response for list data with pagination
class PaginatedApiResponse<T> extends ApiResponse<List<T>> {
/// Current page number
final int currentPage;
/// Total number of pages
final int totalPages;
/// Total number of items
final int totalItems;
/// Number of items per page
final int pageSize;
/// Whether there is a next page
bool get hasNextPage => currentPage < totalPages;
/// Whether there is a previous page
bool get hasPreviousPage => currentPage > 1;
const PaginatedApiResponse({
super.value,
required super.isSuccess,
required super.isFailure,
super.errors,
super.errorCodes,
required this.currentPage,
required this.totalPages,
required this.totalItems,
required this.pageSize,
});
/// Create a PaginatedApiResponse from JSON
factory PaginatedApiResponse.fromJson(
Map<String, dynamic> json,
List<T> Function(dynamic) fromJsonList,
) {
final apiResponse = ApiResponse<List<T>>.fromJson(json, fromJsonList);
return PaginatedApiResponse(
value: apiResponse.value,
isSuccess: apiResponse.isSuccess,
isFailure: apiResponse.isFailure,
errors: apiResponse.errors,
errorCodes: apiResponse.errorCodes,
currentPage: json['CurrentPage'] ?? 1,
totalPages: json['TotalPages'] ?? 1,
totalItems: json['TotalItems'] ?? 0,
pageSize: json['PageSize'] ?? 20,
);
}
@override
List<Object?> get props => [
...super.props,
currentPage,
totalPages,
totalItems,
pageSize,
];
}