API Service

A lightweight and efficient Dart/Flutter package for handling API requests with optional response caching. Ideal for apps that need consistent, streamlined API access with performance optimization via local cache storage.

✨ Features

  • Simple API request handling using Dio
  • Built-in request caching support (Hive-backed, per-request TTL or a package-wide default)
  • Automatic cancellation of a superseded duplicate in-flight request
  • Support for GET, POST, PUT, PATCH, and DELETE
  • Upload/download progress callbacks
  • Optional automatic retry with backoff for transient network errors
  • Optional request-level logging
  • Typed responses via requestTyped<T>()
  • A requestPaginated<T>() helper for list endpoints
  • A reusable AuthInterceptor base class for token attach + refresh-on-401
  • Support for multiple API clients (different base URLs per service)
  • Third-party API request support with isolated base logic

πŸš€ Getting started

Prerequisites

  • Flutter 3.10+
  • Add api_request_handler to your pubspec.yaml:
dependencies:
  api_request_handler:
    path: ../path_to/api_request_handler

Initialization

await ApiRequest().initialize(
  baseUrl: 'https://api.example.com',
  enableLogging: true,
  defaultCacheDuration: const Duration(minutes: 5), // omit to make caching opt-in per call
  connectTimeout: const Duration(seconds: 10),
  receiveTimeout: const Duration(seconds: 10),
);
// For multiple clients
await ApiRequest().initializeMultipleClients(
  baseUrls: {
    'consumer': 'https://consumer.example.com',
    'cas': 'https://cas.example.com',
  },
  globalHeaders: {
    'Authorization': 'Bearer token',
  },
  interceptors: [TokenInterceptor()],
);

πŸ“¦ Usage

// Using a named client
final response = await ApiRequest().request(
  endpoint: '/users',
  method: RequestMethod.get,
  clientName: 'consumer',
  cacheDuration: const Duration(minutes: 5), // omit to skip caching for this call
);

// Using a third-party base URL
final thirdPartyResponse = await ApiRequest().request(
  endpoint: '/third-party-endpoint',
  method: RequestMethod.post,
  data: {'key': 'value'},
  isThirdParty: true,
  thirdPartyBaseUrl: 'https://thirdparty.com',
);

A successful call returns the parsed response body (Map<String, dynamic> / List<dynamic> / primitive). A failed call returns a normalized error map: {'status_code', 'message', 'data', 'success': false}.

Typed responses

final user = await ApiRequest().requestTyped<User>(
  endpoint: '/users/me',
  method: RequestMethod.get,
  fromJson: (json) => User.fromJson(json),
);

Pagination

Each page/param combination is cached under its own key, so pages never collide or get merged.

final result = await ApiRequest().requestPaginated<Item>(
  endpoint: '/items',
  page: 1,
  pageSize: 20,
  itemsKey: 'data',   // key holding the list in the response body
  totalKey: 'total',  // optional; used to compute hasMore
  fromJson: (json) => Item.fromJson(json),
);
// result.items, result.hasMore, result.totalItems

To invalidate every cached page of one endpoint at once:

await ApiRequest().clearCacheForEndpoint('/items');

Cache control

ApiRequest().clearCache('/users');             // one specific cached response
await ApiRequest().clearCacheForEndpoint('/users'); // every cached page/param variant of an endpoint
await ApiRequest().clearAllCache();            // wipe the entire cache

Retries

ApiRequest().configureRetries(retries: 2, retryDelay: const Duration(milliseconds: 500));
// or per-call:
await ApiRequest().request(endpoint: '/flaky', method: RequestMethod.get, retries: 3);

Retries apply only to transient network errors (timeouts, connection errors) β€” not to 4xx/5xx HTTP responses.

Connectivity check

ApiRequest().isConnectedChecker = () async {
  // e.g. backed by the connectivity_plus package in your app
  return await hasNetworkConnection();
};

Auth / token refresh

class MyAuthInterceptor extends AuthInterceptor {
  MyAuthInterceptor() : super(dio: () => ApiRequest().getNamedDio('consumer'));

  @override
  Future<String?> getAccessToken() => TokenStore.readAccessToken();

  @override
  Future<bool> refreshToken() => TokenStore.refresh();

  @override
  Future<void> onRefreshFailed() => TokenStore.logout();
}

Pass an instance via interceptors: [MyAuthInterceptor()] on initialize/initializeMultipleClients.

πŸ“š Additional information

  • This package uses Hive for caching, stored in the platform's cache directory.
  • Cache keys account for endpoint, HTTP method, client, and (order-independent) query params, so identical params in a different order still hit the cache, and the same endpoint on different clients/methods never collides.
  • Issues and contributions are welcome.
  • For suggestions or help, reach out to the package maintainer.