doApiRequest<T extends Decodable> method

Future<APIResponse<T, E>> doApiRequest<T extends Decodable>({
  1. ApiRequestType requestType = ApiRequestType.get,
  2. required String endpoint,
  3. ApiBodyType bodyType = ApiBodyType.json,
  4. Map<String, String>? headers,
  5. Map<String, String>? query,
  6. Object? body,
  7. T decodeContentFromMap(
    1. Map<String, dynamic> data
    )?,
  8. dynamic filterMapResponseToDecodeContent(
    1. Map<String, dynamic> data
    )?,
  9. E decodeErrorFromMapOverride(
    1. int statusCode,
    2. Map<String, dynamic> data
    )?,
  10. bool unescapeHtmlCodes = false,
  11. bool tryRefreshToken = true,
  12. bool useUtf8Decoding = false,
  13. Duration? timeout,
  14. bool? persistCookies,
  15. void uploadPercentage(
    1. int
    )?,
  16. bool validateStatus(
    1. int
    )?,
  17. void downloadProgress(
    1. int,
    2. int,
    3. int
    )?,
  18. 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 call
  • bodyType: 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 constructor
  • query: optional, query parameters as Map to add to endpoint
  • body: optional, the body of the request (usually a json encoded value)
  • decodeContentFromMap: optional, a method to automatically decode the response model, of type T, passed as tear-off, from a Map response
  • filterMapResponseToDecodeContent: 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 tree
  • decodeErrorFromMapOverride: optional, a method to automatically decode the error response model, of type E, passed as tear-off that overrides the method specified in ConnectionManager constructor
  • unescapeHtmlCodes: a boolean value to eventually unescape html chars in response, defaults to false
  • tryRefreshToken: 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 of CookieManager 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);
  }
}