doApiRequest<T extends Decodable> method
- ApiRequestType requestType = ApiRequestType.get,
- required String endpoint,
- ApiBodyType bodyType = ApiBodyType.json,
- Map<
String, String> ? headers, - Map<
String, String> ? query, - Object? body,
- T decodeContentFromMap()?,
- dynamic filterMapResponseToDecodeContent()?,
- E decodeErrorFromMapOverride()?,
- bool unescapeHtmlCodes = false,
- bool tryRefreshToken = true,
- bool useUtf8Decoding = false,
- Duration? timeout,
- bool? persistCookies,
- void uploadPercentage()?,
- bool validateStatus()?,
- void downloadProgress()?,
- CancelToken? cancelToken,
This method is used to generate an API request. It returns a Future to await with a APIResponse with some useful attributes from http response.
Other then E
(the custom class to decode error specified in ConnectionManager initialisation),
also a T
custom class can be specified referred to the specific expected response model
for the current API request. Note that T
must implement Decodable and its
fromMap
method to be used here.
Other than specifying the request type (get, post...), it is possible to specify
the body type: json, formdata, graphql...
To do so, use the bodyType
parameter (defaults to json type).
When passing a json body, it's mandatory to json encode the Map, as follows.
var response = context.read<NetworkProvider>().connectionManager.doApiRequest(
requestType: ApiRequestType.post,
endpoint: "/test-endpoint",
body: jsonEncode({
"test": "test"
}),
);
When using a formData body, it's mandatory to pass it as a Map<String,dynamic>. To pass a file, use the FileData class provided by this library to create a file and add it as a vaue of the Map. It's left to the package to manage it correctly.
When using a graphQL body, it's mandatory to pass it as a String. Parameters must be passed as values in the string itself. The ApiRequestType should be get for queries or anything else for mutations.
Parameters
requestType
: mandatory, the ApiRequestType (get, post...)endpoint
: mandatory, the endpoint for this API callbodyType
: the ApiBodyType to specify the body type (json, formdata, graphQL...). Defaults to json.headers
: optional, headers as Map to add to the headers provided in ConnectionManager constructorquery
: optional, query parameters as Map to add to endpointbody
: optional, the body of the request (usually a json encoded value)decodeContentFromMap
: optional, a method to automatically decode the response model, of typeT
, passed as tear-off, from a Map responsefilterMapResponseToDecodeContent
: optional, a key from the original json response map (retrieved as argument of this method) can be specificied to try to the decode the content. This is useful, for example, when the response body has many nested keys but we need to decode a specific one, also deep in the json treedecodeErrorFromMapOverride
: optional, a method to automatically decode the error response model, of typeE
, passed as tear-off that overrides the method specified in ConnectionManager constructorunescapeHtmlCodes
: a boolean value to eventually unescape html chars in response, defaults to falsetryRefreshToken
: a boolean value to refresh the auth token and retry the API call when the http status code is 401. Defaluts to true.useUtf8Decoding
: a boolean value to eventyally decode the response with utf8 directly to the bytes, ignoring the body. Defaluts to false.timeout
: the timeout for the API call, overrides that of the ConnectionManager.persistCookies
: overrides the persistCookies property of ConnectionManager. If true creates an instance ofCookieManager
to persist cookies for all the API calls.uploadPercentage
: optional, it's used to retrieve the upload percentage status for formData bodies. It's ignored for other bodyTypes.validateStatus
: optional, it's used to evaluate response status code and manage it as success/error accordingly. Simply return true or false depending on the status. Note that status codes between 200 and 299 are always accepted as successfull.downloadProgress
: optional, it's used to retrieve the download percentage status for responses from BE. It has three arguments: download bytes, total bytes count and percentage downloaded.cancelToken
: optional, it's eventually used to cancel the http request before awaiting termination. It does not work for graphql requests.
Usage
// Class to decode in response
class User implements Decodable {
String? name;
User({
this.name,
});
factory User.fromMap(Map<String, dynamic> map) => User(name: map['user']);
}
// Api network caller
var response = context.read<NetworkProvider>().connectionManager.doApiRequest(
requestType: ApiRequestType.get,
endpoint: "/test-endpoint",
decodeContentFromMap: User.fromMap,
);
Implementation
Future<APIResponse<T, E>> doApiRequest<T extends Decodable>({
ApiRequestType requestType = ApiRequestType.get,
required String endpoint,
ApiBodyType bodyType = ApiBodyType.json,
Map<String, String>? headers,
Map<String, String>? query,
Object? body,
T Function(Map<String, dynamic> data)? decodeContentFromMap,
dynamic Function(Map<String, dynamic> data)?
filterMapResponseToDecodeContent,
E Function(int statusCode, Map<String, dynamic> data)?
decodeErrorFromMapOverride,
bool unescapeHtmlCodes = false,
bool tryRefreshToken = true,
bool useUtf8Decoding = false,
Duration? timeout,
bool? persistCookies,
void Function(int)? uploadPercentage,
bool Function(int)? validateStatus,
void Function(int, int, int)? downloadProgress,
CancelToken? cancelToken,
}) async {
// Evaluate correct endpoint for API call
String url;
if (endpoint.contains("http")) {
url = endpoint;
} else {
url = baseUrl + endpoint;
}
// Evaluate correct headers
Map<String, String> headersForApiRequest = Map.of(this.headers);
if (headers != null) {
headersForApiRequest.addAll(headers);
}
var httpClient = client ?? http.Client();
try {
http.Response response = await getResponse(
requestType: requestType,
url: url,
headersForApiRequest: headersForApiRequest,
bodyType: bodyType,
body: body,
query: query,
timeout: timeout ?? this.timeout,
persistCookies: persistCookies ?? this.persistCookies,
uploadPercentage: uploadPercentage,
validateStatus: validateStatus,
downloadProgress: downloadProgress,
httpClient: httpClient,
cancelToken: cancelToken,
);
if (onResponseReceived != null) {
onResponseReceived!(response);
}
// Decode body
dynamic rawValue;
try {
if (response.contentLength != 0) {
if (useUtf8Decoding) {
rawValue = jsonDecode(utf8.decode(response.bodyBytes));
} else {
rawValue = jsonDecode(response.body);
}
if (unescapeHtmlCodes) {
verifyHtmlToUnescape(rawValue);
}
}
} catch (e) {
rawValue = response.body;
}
var statusCode = response.statusCode;
if (mapStatusCodeFromResponse != null) {
try {
statusCode =
mapStatusCodeFromResponse!(rawValue) ?? response.statusCode;
} catch (e) {
if (kDebugMode) {
print(e);
}
}
}
// Evaluate response
if ((statusCode >= 200 && statusCode < 300) ||
(validateStatus != null && validateStatus(statusCode))) {
T? decoded;
List<T>? decodedList;
if (response.contentLength != 0) {
if (decodeContentFromMap != null) {
var mapToDecode = rawValue;
if (filterMapResponseToDecodeContent != null) {
mapToDecode = filterMapResponseToDecodeContent(rawValue);
}
if (mapToDecode is Map<String, dynamic>) {
decoded = decodeContentFromMap(mapToDecode);
decodedList = [decoded];
} else if (mapToDecode is List) {
decodedList =
List.from(mapToDecode.map((e) => decodeContentFromMap(e)));
}
}
}
return APIResponse<T, E>(
decodedBody: decoded,
decodedBodyAsList: decodedList,
rawValue: rawValue,
originalResponse: response,
statusCode: statusCode,
hasError: false);
} else if ((statusCode == 401 ||
(onTokenExpiredRuleOverride != null &&
onTokenExpiredRuleOverride!(response))) &&
onTokenExpired != null &&
tryRefreshToken) {
var newToken = await onTokenExpired!();
if (newToken != null) {
setAuthHeader(newToken);
}
return await doApiRequest(
requestType: requestType,
endpoint: endpoint,
headers: headers,
bodyType: bodyType,
query: query,
body: body,
decodeContentFromMap: decodeContentFromMap,
filterMapResponseToDecodeContent: filterMapResponseToDecodeContent,
decodeErrorFromMapOverride: decodeErrorFromMapOverride,
unescapeHtmlCodes: unescapeHtmlCodes,
tryRefreshToken: false,
useUtf8Decoding: useUtf8Decoding,
timeout: timeout,
persistCookies: persistCookies,
uploadPercentage: uploadPercentage,
validateStatus: validateStatus,
downloadProgress: downloadProgress,
);
}
// http status error
E? decoded;
if (response.contentLength != 0) {
try {
if (decodeErrorFromMapOverride != null) {
decoded = decodeErrorFromMapOverride(statusCode, rawValue);
} else if (decodeErrorFromMap != null) {
decoded = decodeErrorFromMap!(statusCode, rawValue);
}
} catch (e) {
if (kDebugMode) {
print(e);
}
}
}
return APIResponse<T, E>(
decodedErrorBody: decoded,
rawValue: rawValue,
originalResponse: response,
statusCode: statusCode,
hasError: true);
} catch (e) {
if (e.toString().toLowerCase() == "failed to parse header value" &&
onTokenExpired != null &&
tryRefreshToken) {
var newToken = await onTokenExpired!();
if (newToken != null) {
setAuthHeader(newToken);
}
return await doApiRequest(
requestType: requestType,
endpoint: endpoint,
headers: headers,
bodyType: bodyType,
query: query,
body: body,
decodeContentFromMap: decodeContentFromMap,
decodeErrorFromMapOverride: decodeErrorFromMapOverride,
unescapeHtmlCodes: unescapeHtmlCodes,
tryRefreshToken: false,
);
}
return APIResponse(
rawValue: null,
originalResponse: null,
statusCode: 500,
hasError: true,
message: returnCatchedErrorMessage ? e.toString() : null);
}
}