Last active
June 8, 2023 16:54
-
-
Save ehbc221/b27c7ac8216d3be13587f708f83cdbf2 to your computer and use it in GitHub Desktop.
A wrapper around all your datasources (ie: API) in Flutter with the dio package. Responsible for calling the APIs and handling the response / errors parsing, and avoid redundancy.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:io'; | |
import 'package:dio/dio.dart'; | |
import 'package:expenza/constants/global.constants.dart'; | |
import 'package:expenza/domain/resource.dart'; | |
import 'package:expenza/network/dto/base.dto.dart'; | |
import 'package:expenza/utils/log.utils.dart'; | |
import 'package:expenza/utils/pagination.utils.dart'; | |
import 'package:retrofit/dio.dart'; | |
const String _tag = 'BaseDatasource'; | |
/// Base Datasource. | |
/// | |
/// Wraps all the app data sources for API calls handling. | |
/// | |
/// - author - @ehbc221 | |
/// - version - 1.0.0 | |
/// - last updated - June 8th, 2023 | |
abstract class BaseDatasource { | |
/// Wrapper for all api calls with this safeApiCall method. | |
/// | |
/// Provides errors handling and returns the right [Resource]. | |
/// | |
/// - [T] - the type used to parse the response. | |
/// - [apiCall] - the function used to call the API. | |
/// - [data] - the data passed to the API call. | |
/// - [pathId] - in case of an update / patch API, this should be given to map the resource path. | |
/// - [wrappedWithBaseDTO] - whether the response should be wrapped with [BaseDTO] or not. | |
/// - [wrappedWithHttpResponse] - whether the reponse should be wrapped with [HttpResponse] or not. | |
Future<Resource<T>> safeApiCall<T>( | |
Function apiCall, { | |
dynamic data, | |
dynamic pathId, | |
bool wrappedWithBaseDTO = false, | |
bool wrappedWithHttpResponse = false, | |
}) async { | |
logD(_tag, 'safeApiCall: called.'); | |
// Default error message | |
String message = defaultErrorMessage; | |
int status = statusCodeDefault; | |
try { | |
// Handle the BaseDTO wrapper | |
if (wrappedWithBaseDTO) { | |
// Handle the HttpResponse wrapper | |
if (wrappedWithHttpResponse) { | |
// Retrieve response | |
final HttpResponse<BaseDTO<T>> response; | |
if (data != null) { | |
if (pathId != null) { | |
response = | |
await apiCall(data, pathId) as HttpResponse<BaseDTO<T>>; | |
} else { | |
response = await apiCall(data) as HttpResponse<BaseDTO<T>>; | |
} | |
} else { | |
response = await apiCall() as HttpResponse<BaseDTO<T>>; | |
} | |
if (response.data.success == true) { | |
// Result successful | |
return Resource<T>.success( | |
message: response.data.message, | |
data: response.data.data, | |
paginator: getPaginatorDtoFromHeaders(response.response.headers), | |
); | |
} else { | |
// Result unsuccessful | |
return Resource<T>.error( | |
message: response.data.message, | |
status: response.data.status, | |
); | |
} | |
} else { | |
// Retrieve response | |
final BaseDTO<T> response; | |
if (data != null) { | |
if (pathId != null) { | |
response = await apiCall(data, pathId) as BaseDTO<T>; | |
} else { | |
response = await apiCall(data) as BaseDTO<T>; | |
} | |
} else { | |
response = await apiCall() as BaseDTO<T>; | |
} | |
if (response.success == true) { | |
// Result successful | |
return Resource<T>.success( | |
message: response.message, | |
data: response.data, | |
paginator: response.paginator, | |
); | |
} else { | |
// Result unsuccessful | |
return Resource<T>.error( | |
message: response.message, | |
status: response.status, | |
); | |
} | |
} | |
} else { | |
// Handle the HttpResponse wrapper | |
final HttpResponse<T> response; | |
if (wrappedWithHttpResponse) { | |
// Retrieve response | |
if (data != null) { | |
if (pathId != null) { | |
response = await apiCall(data, pathId) as HttpResponse<T>; | |
} else { | |
response = await apiCall(data) as HttpResponse<T>; | |
} | |
} else { | |
response = await apiCall() as HttpResponse<T>; | |
} | |
// Result successful | |
return Resource<T>.success( | |
data: response.data, | |
paginator: getPaginatorDtoFromHeaders(response.response.headers), | |
); | |
} else { | |
// Retrieve response | |
final T response; | |
if (data != null) { | |
if (pathId != null) { | |
response = await apiCall(data, pathId) as T; | |
} else { | |
response = await apiCall(data) as T; | |
} | |
} else { | |
response = await apiCall() as T; | |
} | |
// Result successful | |
return Resource<T>.success(data: response); | |
} | |
} | |
} catch (exception) { | |
logW(_tag, 'Catch exception: $exception'); | |
if (exception is DioError) { | |
if ((<DioErrorType>[ | |
DioErrorType.connectionTimeout, | |
DioErrorType.connectionError, | |
DioErrorType.sendTimeout, | |
DioErrorType.receiveTimeout, | |
DioErrorType.cancel | |
].contains(exception.type)) || | |
(exception.type == DioErrorType.unknown && | |
exception.error is SocketException)) { | |
// Network Error | |
message = 'Network Error. Please try again later'; | |
return Resource<T>.error( | |
detail: message, | |
status: status, | |
isNetworkError: true, | |
); | |
} else if (<DioErrorType>[ | |
DioErrorType.badResponse, | |
DioErrorType.badCertificate, | |
].contains(exception.type)) { | |
// Check if the error is parsable from the API response (Spring - problem) | |
/*try { | |
if (exception.response != null) { | |
return ResourceError<T>.fromJson( | |
exception.response?.data as JsonMap, | |
); | |
} | |
} catch (exception) { | |
logW(_tag, 'Catch exception: $exception'); | |
}*/ | |
// When the server response, but with a incorrect status, such as 404, 503... | |
final int statusCode = | |
exception.response?.statusCode ?? HttpStatus.badRequest; | |
status = statusCode; | |
// TODO logout directly the user when the backend responds with a 401 or 403 | |
if (statusCode == HttpStatus.unauthorized) { | |
message = | |
'Your are unauthenticated. Please login to access this resource'; | |
} else if (statusCode == HttpStatus.forbidden) { | |
message = 'You are not allowed to access this resource'; | |
} else if (statusCode == HttpStatus.notFound) { | |
message = 'This resource is not found. Please try again'; | |
} else if (statusCode == HttpStatus.internalServerError) { | |
message = defaultErrorMessage; | |
} else if (statusCode == HttpStatus.gatewayTimeout) { | |
message = 'Idle time reached. Please reconnect'; | |
} else { | |
message = 'Bas request. Please try again'; | |
} | |
} else { | |
// Other types of error | |
if (exception.error is SocketException) { | |
final int errorCode = | |
(exception.error as SocketException).osError?.errorCode ?? | |
statusCodeDefault; | |
if (errorCode == statusCode101OsError) { | |
message = | |
'You are not connected to internet. Please make sure that Wifi/Data is activated'; | |
return Resource<T>.error( | |
detail: message, | |
status: errorCode, | |
isNetworkError: true, | |
); | |
} | |
} | |
message = defaultErrorMessage; | |
} | |
} | |
return Resource<T>.error(detail: message, status: status); | |
} | |
} | |
} |
It can also wrap API calls without data passed (ie: getAccount
) with:
/// Get a user's account details
Future<Resource<Account>> getAccount() async {
logD(_tag, 'getAccount: called.');
return safeApiCall<Account>(authenticationService.getAccount);
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example
Assuming that you have a
budgetService
that has the call logic to your budget API to retrieve the list of budgets with the methodgetAll
Where budgetSearchRequest contains the search details (as a
Map<String, dynamic)
), andgetAll
should returnList<Budget>
.