apiCall function

Future<ApiResponse> apiCall(
  1. String url, {
  2. Options options = const Options(),
})

Makes an API call to the specified url as per the given options.

This function sends an HTTP request to the provided url and returns the response wrapped in an ApiResponse object. It allows customization of the request through the optional options parameter.

Parameters:

  • url: The endpoint URL to which the API request is sent.
  • options: Optional configuration for the request, such as headers, query parameters, timeout, etc. Defaults to an empty Options(method: HttpMethod.get) object.

Returns:

A Future that completes with an ApiResponse object containing:

  • The response data (if the request is successful).
  • Error message (if the request fails).

Example 1: Simple GET api call

var res = await apiCall('https://jsonplaceholder.typicode.com/todos/1');
print(res);

Output:

ApiResponse(
  Ok: true
  Headers: {x-ratelimit-reset: 1739370722, x-ratelimit-limit: 1000, date: Sun, 16 Feb 2025 07:15:03 GMT, transfer-encoding: chunked, vary: Origin, Accept-Encoding, content-encoding: gzip, x-ratelimit-remaining: 999, pragma: no-cache, server: cloudflare, reporting-endpoints: heroku-nel=https://nel.heroku.com/reports?ts=1739370677&sid=e11707d5-02a7-43ef-b45e-2cf4d2036f7d&s=c4034Ntzv9xG2rv4p9jTn01XXS2WI5YBk%2BZUpEh0KUI%3D, cf-ray: 912bbe898f4ce165-MRS, etag: W/"53-hfEnumeNh6YirfjyjaujcOPPT+s", connection: keep-alive, cache-control: max-age=43200, age: 2596, server-timing: cfL4;desc="?proto=TCP&rtt=275810&min_rtt=262832&rtt_var=88203&sent=5&recv=7&lost=0&retrans=0&sent_bytes=2810&recv_bytes=719&delivery_rate=12063&cwnd=223&unsent_bytes=0&cid=5ff42c7a50985390&ts=324&x=0", report-to: {"group":"heroku-nel","max_age":3600,"endpoints":[{"url":"https://nel.heroku.com/reports?ts=1739370677&sid=e11707d5-02a7-43ef-b45e-2cf4d2036f7d&s=c4034Ntzv9xG2rv4p9jTn01XXS2WI5YBk%2BZUpEh0KUI%3D"}]}, cf-cache-status: HIT, content-type: application/json; charset=utf-8, access-control-allow-credentials: true, x-powered-by: Express, alt-svc: h3=":443"; ma=86400, nel: {"report_to":"heroku-nel","max_age":3600,"success_fraction":0.005,"failure_fraction":0.05,"response_headers":["Via"]}, via: 1.1 vegur, x-content-type-options: nosniff, expires: -1}
  Status Code: 200
  Status Message: OK
  JSON: {userId: 1, id: 1, title: delectus aut autem, completed: false}
  Body: {userId: 1, id: 1, title: delectus aut autem, completed: false}
)

Example 2: POST api call

res = await apiCall(
  'https://jsonplaceholder.typicode.com/todos',
  options: Options(
    method: HttpMethod.post,
    json: {'title': 'foo', 'body': 'bar', 'userId': 1},
  ),
);
print('res.ok: ${res.ok}');
print('res.statusCode: ${res.statusCode}');
print('res.statusMessage: ${res.statusMessage}');
print('res.json: ${res.json}');
print('res.body: ${res.body}');
print('res.headers: ${res.headers}');
print('res.error: ${res.error}');

Output:

res.ok: true
res.statusCode: 201
res.statusMessage: Created
res.json: {title: foo, body: bar, userId: 1, id: 201}
res.body: {title: foo, body: bar, userId: 1, id: 201}
res.headers: {x-ratelimit-reset: 1739690470, x-ratelimit-limit: 1000, date: Sun, 16 Feb 2025 07:20:47 GMT, vary: Origin, X-HTTP-Method-Override, Accept-Encoding, x-ratelimit-remaining: 999, access-control-expose-headers: Location, pragma: no-cache, server: cloudflare, location: https://jsonplaceholder.typicode.com/todos/201, reporting-endpoints: heroku-nel=https://nel.heroku.com/reports?ts=1739690447&sid=e11707d5-02a7-43ef-b45e-2cf4d2036f7d&s=XjgxrdvNN77WuG%2FKJaG8Ak0UcXg20np1rZx0c%2F5lsdw%3D, content-length: 65, cf-ray: 912bc6f0f96ae1d0-MRS, etag: W/"41-S72XhYKRBNSGo0mxoArJPNcK+ug", connection: keep-alive, cache-control: no-cache, server-timing: cfL4;desc="?proto=TCP&rtt=273148&min_rtt=271947&rtt_var=104383&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2811&recv_bytes=809&delivery_rate=13318&cwnd=251&unsent_bytes=0&cid=125845bdc8fbdb1b&ts=656&x=0", report-to: {"group":"heroku-nel","max_age":3600,"endpoints":[{"url":"https://nel.heroku.com/reports?ts=1739690447&sid=e11707d5-02a7-43ef-b45e-2cf4d2036f7d&s=XjgxrdvNN77WuG%2FKJaG8Ak0UcXg20np1rZx0c%2F5lsdw%3D"}]}, cf-cache-status: DYNAMIC, content-type: application/json; charset=utf-8, access-control-allow-credentials: true, x-powered-by: Express, alt-svc: h3=":443"; ma=86400, nel: {"report_to":"heroku-nel","max_age":3600,"success_fraction":0.005,"failure_fraction":0.05,"response_headers":["Via"]}, via: 1.1 vegur, x-content-type-options: nosniff, expires: -1}
res.error: null

Example 3: Form Data Upload with progress updates

var res = await apiCall(
  'https://api.escuelajs.co/api/v1/files/upload',
  options: Options(
    method: HttpMethod.post,
    formData: FormData(
      fields: {'name': 'Rituraj', 'age': ['20']},
      files: {'file': 'pubspec.lock', 'songs': ['music.mp3', 'tune.mp3']},
    ), // files must exist
    onSendProgress:(sent, total) {
      print('Sent: $sent/$total (${(sent * 100 / total).toStringAsFixed(2)}%)');
    },
    onReceiveProgress:(sent, total) {
      print('Receive: $sent/$total (${(sent * 100 / total).toStringAsFixed(2)}%)');
    },
  ),
);
print(res);

Output:

Sent: 29/3627 (0.80%)
Sent: 76/3627 (2.10%)
Sent: 83/3627 (2.29%)
Sent: 85/3627 (2.34%)
Sent: 114/3627 (3.14%)
Sent: 160/3627 (4.41%)
Sent: 162/3627 (4.47%)
Sent: 164/3627 (4.52%)
Sent: 193/3627 (5.32%)
Sent: 305/3627 (8.41%)
Sent: 3594/3627 (99.09%)
Sent: 3596/3627 (99.15%)
Sent: 3627/3627 (100.00%)
Receive: 115/115 (100.00%)
ApiResponse(
  Ok: true
  Headers: {connection: keep-alive, x-powered-by: Express, date: Sun, 16 Feb 2025 07:32:34 GMT, access-control-allow-origin: *, reporting-endpoints: heroku-nel=https://nel.heroku.com/reports?ts=1739691154&sid=c46efe9b-d3d2-4a0c-8c76-bfafa16c5add&s=apdZjJfRbz1LuU6zwa%2Frp5SCRl6oKtamUnSDbpD7XQo%3D, content-length: 115, nel: {"report_to":"heroku-nel","max_age":3600,"success_fraction":0.005,"failure_fraction":0.05,"response_headers":["Via"]}, report-to: {"group":"heroku-nel","max_age":3600,"endpoints":[{"url":"https://nel.heroku.com/reports?ts=1739691154&sid=c46efe9b-d3d2-4a0c-8c76-bfafa16c5add&s=apdZjJfRbz1LuU6zwa%2Frp5SCRl6oKtamUnSDbpD7XQo%3D"}]}, etag: W/"73-1JcWeVg6NkoEkXmerMyEq+GNFAw", via: 1.1 vegur, content-type: application/json; charset=utf-8, server: Cowboy}
  Status Code: 201
  Status Message: Created
  JSON: {originalname: pubspec.lock, filename: 1591.lock, location: https://api.escuelajs.co/api/v1/files/1591.lock}
  Body: {originalname: pubspec.lock, filename: 1591.lock, location: https://api.escuelajs.co/api/v1/files/1591.lock}
)

Example 4: API call with custom timeout and URL search params (query)

var res = await apiCall(
  'https://jsonplaceholder.typicode.com/todos',
  options: Options(
    timeout: Duration(milliseconds: 100),
    query: {'limit': '20', 'page': '1'},
  ),
);
print(res);

Output:

ApiResponse(
  Ok: false
  Headers: {}
  Error: The request connection took longer than 0:00:00.100000 and it was aborted. To get rid of this exception, try raising the RequestOptions.connectTimeout above the duration of 0:00:00.100000 or improve the response time of the server.
)

Example 5: Form Url Encoded data api call

var res = await apiCall(
  'https://mocktarget.apigee.net/echo',
  options: Options(
    method: HttpMethod.post,
    formUrlEncoded: {'name': 'Rituraj', 'age': 30},
    timeout: Duration(milliseconds: 10),
  ),
);
print(res);

Output:

ApiResponse(
  Ok: true
  Headers: {x-powered-by: Apigee, alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000, date: Sun, 16 Feb 2025 07:41:39 GMT, access-control-allow-origin: *, content-length: 441, etag: W/"1b9-WdGJtTc3EMBjFmBo/nWQS/mm5pY", via: 1.1 google, x-frame-options: ALLOW-FROM RESOURCE-URL, content-type: application/json; charset=utf-8, x-xss-protection: 1, x-content-type-options: nosniff}
  Status Code: 200
  Status Message: OK
  JSON: {headers: {host: mocktarget.apigee.net, user-agent: Dart/3.7 (dart:io), content-type: application/x-www-form-urlencoded, accept-encoding: gzip, content-length: 19, x-cloud-trace-context: 6271bbc1a5430dfa12ebbb3b55070bc1/71101670834545451, via: 1.1 google, x-forwarded-for: 223.185.63.47, 35.227.194.212, x-forwarded-proto: https, connection: Keep-Alive}, method: POST, url: /, args: {}, body: name=Rituraj&age=30}
  Body: {headers: {host: mocktarget.apigee.net, user-agent: Dart/3.7 (dart:io), content-type: application/x-www-form-urlencoded, accept-encoding: gzip, content-length: 19, x-cloud-trace-context: 6271bbc1a5430dfa12ebbb3b55070bc1/71101670834545451, via: 1.1 google, x-forwarded-for: 223.185.63.47, 35.227.194.212, x-forwarded-proto: https, connection: Keep-Alive}, method: POST, url: /, args: {}, body: name=Rituraj&age=30}
)

Example 6: Sending JSON data in POST using body field

res = await apiCall(
  'https://jsonplaceholder.typicode.com/todos',
  options: Options(
    method: HttpMethod.post,
    body: jsonEncode({'title': 'foo', 'body': 'bar', 'userId': 1}),
    headers: {'Content-Type': 'application/json'}
  ),
);
print('res.ok: ${res.ok}');
print('res.json: ${res.json}');

Output:

res.ok: true
res.json: {title: foo, body: bar, userId: 1, id: 201}

Example 7: Server Sent Events (SSE)

var res = await apiCall(
  'https://sse-fake.andros.dev/events/',
  options: Options(stream: true),
);
res.stream!.listen((e) {
  var msg = String.fromCharCodes(e as List<int>);
  print(msg);
});

Output:

:
event: stream-open
data:
2

event: message
data: {"action": "User connected", "name": "Jeremy"}
2

event: message
data: {"action": "New message", "name": "Nancy", "text": "How behind pattern world use. Reality save he where lead language.\nWhile ability likely difficult body someone. Two hope else listen never.\nHand speak town indicate else. Positive need price throw."}
2

event: message
data: {"action": "User connected", "name": "Melissa"}
...

Implementation

Future<ApiResponse> apiCall(String url, {Options options = const Options()}) async {
  try {
    var dioInstance = dio.Dio()..interceptors.clear();
    if (options.stream) {
      dioInstance.options.responseType = dio.ResponseType.stream;
    }
    dynamic body = options.body;
    if (options.timeout != null) {
      dioInstance.options.connectTimeout = options.timeout;
    }
    var headers = options.headers ?? {};
    var query = options.query ?? {};
    if (options.formData != null) {
      dio.FormData? formData;
      var map = <String, Object>{...options.formData!.fields};
      for (var e in options.formData!.files.entries) {
        if (e.value is String) {
          map[e.key] = await _toMultipartFile(e.value as String);
        } else if (e.value is List<String>) {
          var list = <dio.MultipartFile>[];
          for (var path in e.value as List<String>) {
            list.add(await _toMultipartFile(path));
          }
          map[e.key] = list;
        }
      }
      formData = dio.FormData.fromMap(map);
      body = formData;
    }
    if (options.json != null) {
      headers['content-type'] = 'application/json';
      body = options.json;
    }
    if (options.formUrlEncoded != null) {
      dioInstance.options.contentType = dio.Headers.formUrlEncodedContentType;
      body = options.formUrlEncoded;
    }
    dio.Response response;
    if (options.method == HttpMethod.get) {
      response = await dioInstance.get(url, queryParameters: query, options: dio.Options(headers: headers));
    } else if (options.method == HttpMethod.post) {
      response = await dioInstance.post(
        url,
        data: body,
        queryParameters: query,
        options: dio.Options(headers: headers),
        onSendProgress: options.onSendProgress,
        onReceiveProgress: options.onReceiveProgress,
      );
    } else if (options.method == HttpMethod.put) {
      response = await dioInstance.put(
        url,
        data: body,
        queryParameters: query,
        options: dio.Options(headers: headers),
        onSendProgress: options.onSendProgress,
        onReceiveProgress: options.onReceiveProgress,
      );
    } else if (options.method == HttpMethod.patch) {
      response = await dioInstance.patch(
        url,
        data: body,
        queryParameters: query,
        options: dio.Options(headers: headers),
        onSendProgress: options.onSendProgress,
        onReceiveProgress: options.onReceiveProgress,
      );
    } else if (options.method == HttpMethod.delete) {
      response = await dioInstance.delete(
        url,
        data: body,
        queryParameters: options.query,
        options: dio.Options(headers: headers),
      );
    } else {
      throw 'Unknown HTTP Method!';
    }

    var responseHeaders = <String, Object>{};
    for (final e in response.headers.map.entries) {
      if (e.value.length == 1) {
        responseHeaders[e.key] = e.value.first;
      } else if (e.value.length >= 2) {
        responseHeaders[e.key] = e.value;
      }
    }

    return ApiResponse(
      headers: responseHeaders,
      statusCode: response.statusCode,
      statusMessage: response.statusMessage,
      body: response.data,
      json:
          response.headers.map['content-type']?.any((e) => e.contains('application/json')) == true
              ? response.data
              : null,
      stream: options.stream ? response.data.stream : null,
    );
  } on dio.DioException catch (error) {
    final headers = error.response?.headers.map ?? {};
    var responseHeaders = <String, Object>{};
    for (final e in headers.entries) {
      if (e.value.length == 1) {
        responseHeaders[e.key] = e.value.first;
      } else if (e.value.length >= 2) {
        responseHeaders[e.key] = e.value;
      }
    }
    dynamic json;
    if (headers['content-type'] != null && headers['content-type']!.any((e) => e.contains('application/json'))) {
      json = error.response?.data;
    }

    return ApiResponse(
      headers: responseHeaders,
      body: error.response?.data,
      json: json,
      statusCode: error.response?.statusCode,
      statusMessage: error.response?.statusMessage,
      error: error.message,
    );
  }
}