import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; import 'package:http/io_client.dart'; import '../exceptions/api_exception.dart'; typedef FactoryConstructor = U Function(dynamic); class APIError { int? errorCode; String? errorMessage; APIError(this.errorCode, this.errorMessage); Map toJson() => {'errorCode': errorCode, 'errorMessage': errorMessage}; @override String toString() { return jsonEncode(this); } } APIException _throwAPIException(Response response) { print(response.statusCode); switch (response.statusCode) { case 200: APIError? apiError; if (response.body.isNotEmpty) { var jsonError = jsonDecode(response.body); debugPrint(jsonError); apiError = APIError(response.statusCode, jsonError[0]); } return APIException(APIException.BAD_REQUEST, error: apiError); case 400: APIError? apiError; if (response.body.isNotEmpty) { var jsonError = jsonDecode(response.body); debugPrint("json error : $jsonError"); apiError = APIError(response.statusCode, jsonError[0]); } return APIException(APIException.BAD_REQUEST, error: apiError); case 401: return const APIException(APIException.UNAUTHORIZED); case 403: return const APIException(APIException.FORBIDDEN); case 404: return const APIException(APIException.NOT_FOUND); case 500: return const APIException(APIException.INTERNAL_SERVER_ERROR); case 444: var downloadUrl = response.headers["location"]; return APIException(APIException.UPGRADE_REQUIRED, arguments: downloadUrl); default: return const APIException(APIException.OTHER); } } class ApiClient { static final ApiClient _instance = ApiClient._internal(); ApiClient._internal(); factory ApiClient() => _instance; Future postJsonForObject(FactoryConstructor factoryConstructor, String url, T jsonObject, {String? token, Map? queryParameters, Map? headers, int retryTimes = 0, bool isFormData = false}) async { var defaultHeaders = {'Accept': 'application/json'}; if (headers != null && headers.isNotEmpty) { defaultHeaders.addAll(headers); } if (!kReleaseMode) { debugPrint("Url:$url"); var bodyJson = json.encode(jsonObject); debugPrint("body:$bodyJson"); } var response = await postJsonForResponse(url, jsonObject, token: token, queryParameters: queryParameters, headers: headers, retryTimes: retryTimes, isFormData: isFormData); try { var jsonData = jsonDecode(response.body); if (jsonData != null) { debugPrint(jsonData.runtimeType.toString()); return factoryConstructor(jsonData); } else { APIError? apiError; apiError = APIError(response.statusCode, jsonData[0]); throw APIException(APIException.BAD_REQUEST, error: apiError); } } catch (ex) { if (ex is APIException) { rethrow; } else { throw APIException(APIException.BAD_RESPONSE_FORMAT, arguments: ex); } } } Future putJsonForObject(FactoryConstructor factoryConstructor, String url, T jsonObject, {String? token, Map? queryParameters, Map? headers, int retryTimes = 0, bool isFormData = false}) async { var defaultHeaders = {'Accept': 'application/json'}; if (headers != null && headers.isNotEmpty) { defaultHeaders.addAll(headers); } if (!kReleaseMode) { debugPrint("Url:$url"); var bodyJson = json.encode(jsonObject); debugPrint("body:$bodyJson"); } var response = await putJsonForResponse(url, jsonObject, token: token, queryParameters: queryParameters, headers: headers, retryTimes: retryTimes, isFormData: isFormData); try { var jsonData = jsonDecode(response.body); if (jsonData != null) { debugPrint(jsonData.runtimeType.toString()); return factoryConstructor(jsonData); } else { APIError? apiError; apiError = APIError(response.statusCode, jsonData[0]); throw APIException(APIException.BAD_REQUEST, error: apiError); } } catch (ex) { if (ex is APIException) { rethrow; } else { throw APIException(APIException.BAD_RESPONSE_FORMAT, arguments: ex); } } } Future postJsonForResponse(String url, T jsonObject, {String? token, Map? queryParameters, Map? headers, int retryTimes = 0, bool isFormData = false}) async { int currentRetryTime = retryTimes; String? requestBody; late Map stringObj; if (jsonObject != null) { requestBody = jsonEncode(jsonObject); if (headers == null) { headers = {'Content-Type': 'application/json'}; } else { headers['Content-Type'] = 'application/json'; } } if (!kReleaseMode) { print("url:$url"); print("requestBody:$requestBody"); } if (isFormData) { headers = {'Content-Type': 'application/x-www-form-urlencoded'}; stringObj = ((jsonObject ?? {}) as Map).map((key, value) => MapEntry(key, value?.toString() ?? "")); } if (!kReleaseMode) { print("url:$url"); print("requestBody:$requestBody"); } Future retry(APIException exception) async { if (currentRetryTime > 0) { currentRetryTime -= 1; debugPrint('will retry after 3 seconds...'); await Future.delayed(const Duration(seconds: 3)); return await _postForResponse(url, requestBody, token: token, queryParameters: queryParameters, headers: headers); } else { throw exception; } } try { return await _postForResponse(url, isFormData ? stringObj : requestBody, token: token, queryParameters: queryParameters, headers: headers); } on SocketException catch (e) { return await retry(APIException(APIException.OTHER, arguments: e)); } on HttpException catch (e) { return await retry(APIException(APIException.OTHER, arguments: e)); } on TimeoutException catch (e) { throw APIException(APIException.TIMEOUT, arguments: e); } on ClientException catch (e) { return await retry(APIException(APIException.OTHER, arguments: e)); } } Future putJsonForResponse(String url, T jsonObject, {String? token, Map? queryParameters, Map? headers, int retryTimes = 0, bool isFormData = true}) async { int currentRetryTime = retryTimes; String? requestBody; late Map stringObj; if (jsonObject != null) { requestBody = jsonEncode(jsonObject); if (headers == null) { headers = {'Content-Type': 'application/json'}; } else { headers['Content-Type'] = 'application/json'; } } if (!kReleaseMode) { print(jsonObject); } if (isFormData) { headers = {'Content-Type': 'application/x-www-form-urlencoded'}; stringObj = ((jsonObject ?? {}) as Map).map((key, value) => MapEntry(key, value?.toString() ?? "")); } if (!kReleaseMode) { print("url:$url"); print("requestBody:$requestBody"); } Future retry(APIException exception) async { if (currentRetryTime > 0) { currentRetryTime -= 1; debugPrint('will retry after 3 seconds...'); await Future.delayed(const Duration(seconds: 3)); return await _putForResponse(url, requestBody, token: token, queryParameters: queryParameters, headers: headers); } else { throw exception; } } try { return await _postForResponse(url, isFormData ? stringObj : requestBody, token: token, queryParameters: queryParameters, headers: headers); } on SocketException catch (e) { return await retry(APIException(APIException.OTHER, arguments: e)); } on HttpException catch (e) { return await retry(APIException(APIException.OTHER, arguments: e)); } on TimeoutException catch (e) { throw APIException(APIException.TIMEOUT, arguments: e); } on ClientException catch (e) { return await retry(APIException(APIException.OTHER, arguments: e)); } } Future _postForResponse(String url, requestBody, {String? token, Map? queryParameters, Map? headers}) async { var defaultHeaders = {}; if (token != null) { defaultHeaders['Authorization'] = 'Bearer $token'; } if (headers != null && headers.isNotEmpty) { defaultHeaders.addAll(headers); } if (queryParameters != null) { var queryString = Uri(queryParameters: queryParameters).query; url = '$url?$queryString'; } var response = await _post(Uri.parse(url), body: requestBody, headers: defaultHeaders).timeout(const Duration(seconds: 120)); if (response.statusCode >= 200 && response.statusCode < 300) { return response; } else { throw _throwAPIException(response); } } Future _putForResponse(String url, requestBody, {String? token, Map? queryParameters, Map? headers}) async { var defaultHeaders = {}; if (token != null) { defaultHeaders['Authorization'] = 'Bearer $token'; } if (headers != null && headers.isNotEmpty) { defaultHeaders.addAll(headers); } if (queryParameters != null) { var queryString = Uri(queryParameters: queryParameters).query; url = '$url?$queryString'; } var response = await _put(Uri.parse(url), body: requestBody, headers: defaultHeaders).timeout(const Duration(seconds: 120)); if (response.statusCode >= 200 && response.statusCode < 300) { return response; } else { throw _throwAPIException(response); } } Future _deleteForResponse(String url, {String? token, Map? queryParameters, Map? headers}) async { var defaultHeaders = {}; if (token != null) { defaultHeaders['Authorization'] = 'Bearer $token'; } if (headers != null && headers.isNotEmpty) { defaultHeaders.addAll(headers); } if (queryParameters != null) { var queryString = Uri(queryParameters: queryParameters).query; url = '$url?$queryString'; } var response = await _delete(Uri.parse(url), headers: defaultHeaders).timeout(const Duration(seconds: 60)); if (response.statusCode >= 200 && response.statusCode < 300) { return response; } else { throw _throwAPIException(response); } } Future getJsonForResponse(String url, {String? token, Map? queryParameters, Map? headers, int retryTimes = 0}) async { int currentRetryTime = retryTimes; if (headers == null) { headers = {'Content-Type': 'application/json'}; } else { headers['Content-Type'] = 'application/json'; } Future retry(APIException exception) async { if (currentRetryTime > 0) { currentRetryTime -= 1; debugPrint('will retry after 3 seconds...'); await Future.delayed(const Duration(seconds: 3)); return await _getForResponse(url, token: token, queryParameters: queryParameters, headers: headers); } else { throw exception; } } try { return await _getForResponse(url, token: token, queryParameters: queryParameters, headers: headers); } on SocketException catch (e) { return await retry(APIException(APIException.OTHER, arguments: e)); } on HttpException catch (e) { return await retry(APIException(APIException.OTHER, arguments: e)); } on TimeoutException catch (e) { throw APIException(APIException.TIMEOUT, arguments: e); } on ClientException catch (e) { return await retry(APIException(APIException.OTHER, arguments: e)); } } Future deleteJsonForResponse(String url, {String? token, Map? queryParameters, Map? headers, int retryTimes = 0}) async { int currentRetryTime = retryTimes; if (headers == null) { headers = {'Content-Type': 'application/json'}; } else { headers['Content-Type'] = 'application/json'; } Future retry(APIException exception) async { if (currentRetryTime > 0) { currentRetryTime -= 1; debugPrint('will retry after 3 seconds...'); await Future.delayed(const Duration(seconds: 3)); return await _deleteForResponse(url, token: token, queryParameters: queryParameters, headers: headers); } else { throw exception; } } try { return await _deleteForResponse(url, token: token, queryParameters: queryParameters, headers: headers); } on SocketException catch (e) { return await retry(APIException(APIException.OTHER, arguments: e)); } on HttpException catch (e) { return await retry(APIException(APIException.OTHER, arguments: e)); } on TimeoutException catch (e) { throw APIException(APIException.TIMEOUT, arguments: e); } on ClientException catch (e) { return await retry(APIException(APIException.OTHER, arguments: e)); } } Future _getForResponse(String url, {String? token, Map? queryParameters, Map? headers}) async { var defaultHeaders = {}; if (token != null) { defaultHeaders['Authorization'] = 'Bearer $token'; } if (headers != null && headers.isNotEmpty) { defaultHeaders.addAll(headers); } if (queryParameters != null) { var queryString = Uri(queryParameters: queryParameters).query; url = '$url?$queryString'; } var response = await _get(Uri.parse(url), headers: defaultHeaders).timeout(const Duration(seconds: 60)); if (response.statusCode >= 200 && response.statusCode < 300) { return response; } else { throw _throwAPIException(response); } } Future _get(url, {Map? headers}) => _withClient((client) => client.get(url, headers: headers)); bool _certificateCheck(X509Certificate cert, String host, int port) => true; Future _withClient(Future Function(Client) fn) async { var httpClient = HttpClient()..badCertificateCallback = _certificateCheck; var client = IOClient(httpClient); try { return await fn(client); } finally { client.close(); } } Future _post(url, {Map? headers, body, Encoding? encoding}) => _withClient((client) => client.post(url, headers: headers, body: body, encoding: encoding)); Future _put(url, {Map? headers, body, Encoding? encoding}) => _withClient((client) => client.put(url, headers: headers, body: body, encoding: encoding)); Future _delete(url, {Map? headers}) => _withClient((client) => client.delete(url, headers: headers)); }