Skip to content

Instantly share code, notes, and snippets.

@ehbc221
Last active June 8, 2023 16:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ehbc221/b27c7ac8216d3be13587f708f83cdbf2 to your computer and use it in GitHub Desktop.
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.
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);
}
}
}
@ehbc221
Copy link
Author

ehbc221 commented Jun 8, 2023

Example

Assuming that you have a budgetService that has the call logic to your budget API to retrieve the list of budgets with the method getAll

/// Get all budgets.
  Future<Resource<List<Budget>>> getAll(
    JsonMap budgetSearchRequest,
  ) async {
    logD(_tag, 'getAll called.');
    return safeApiCall<List<Budget>>(
      budgetService.getAll,
      data: budgetSearchRequest,
      wrappedWithHttpResponse: true,
    );
  }

Where budgetSearchRequest contains the search details (as a Map<String, dynamic)), and getAll should return List<Budget>.

@ehbc221
Copy link
Author

ehbc221 commented Jun 8, 2023

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