A batteries-included, no-codegen Dio wrapper for Flutter and Dart. It adds typed response parsing, resilient retry, automatic token refresh, persistent / ETag-aware caching, pluggable pagination, and Web/WASM-ready connectivity checks on top of Dio — with first-class testability.
The runtime-wrapper segment wins on developer experience, not breadth. This
package is positioned against raw Dio (which lacks the opinionated layer) and
against retrofit/chopper (which demand code-generation and build_runner).
- Zero code-generation — works instantly, no
build_runner. - Automatic 401 token-refresh-and-retry out of the box.
- Persistent + ETag-aware caching that survives restarts (via the
CacheStoreinterface). - Typed exceptions that preserve server error bodies — 422 field-validation errors are reachable.
- Pluggable pagination for any API shape (offset or cursor based).
- Web/WASM-ready with optional, pluggable connectivity checks.
- ✅ Generic Response Parsing — automatic JSON parsing to your models,
including JSON-encoded
Stringbodies. - ✅ Comprehensive Error Handling — typed exceptions for all HTTP status
codes that preserve the server response body (
data,errors,statusCode). - ✅ Automatic Retry — configurable retry with exponential backoff for transient errors.
- ✅ Automatic Token Refresh — inject a
TokenRefresherto refresh expired tokens and retry the original request once on 401. - ✅ Connectivity Verification — pluggable
ConnectivityCheckerwith a no-op default for pure-Dart / Web / WASM. - ✅ Injectable Logging — instance-based
ApiLogger, silencable per-client. - ✅ Token Management — dynamic authorization header update and removal.
- ✅ Pluggable Pagination —
OffsetPaginationStrategyandCursorPaginationStrategywith configurable field keys. - ✅ Response Caching —
CacheStoreinterface with in-memoryApiCache(TTL + ETag), plus request deduplication for concurrent identical GETs. - ✅ Flexible Interceptors — dynamic hooks for request, response, and error modification.
- ✅ File Upload/Download — simple methods with progress callbacks.
Add this dependency to your project's pubspec.yaml:
dependencies:
api_network_kit: ^1.1.0Then run:
flutter pub getimport 'package:api_network_kit/api_network_kit.dart';
final api = ApiKit(baseUrl: 'https://api.example.com');class User {
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) => User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
final int id;
final String name;
final String email;
}
final user = await api.get<User>(
'/users/1',
parser: User.fromJson,
);try {
final user = await api.get<User>('/users/1', parser: User.fromJson);
} on UnauthorizedException {
print('Please log in again.');
} on UnprocessableEntityException catch (e) {
// Field-level validation errors are now reachable:
print(e.errors); // e.g. {'email': ['Already taken']}
} on NoInternetException {
print('Please check your internet connection.');
} on Exception catch (e) {
print('An error occurred: $e');
}final api = ApiKit(
baseUrl: 'https://api.example.com',
token: initialToken,
tokenRefresher: (error, options) async {
// Call your refresh endpoint and return the new token.
final newToken = await refreshToken();
return newToken;
},
);On a 401, the interceptor calls tokenRefresher, updates the stored token, and
retries the original request once. Infinite loops are prevented via a
per-request flag.
// Offset-based with custom field keys:
final result = await api.getPaginated<User>(
'/users',
parser: User.fromJson,
paginationConfig: const PaginationConfig(
itemsKey: 'data',
currentPageKey: 'page',
totalPagesKey: 'pages',
totalItemsKey: 'count',
),
);
// Cursor-based:
final result = await api.getPaginated<User>(
'/users',
parser: User.fromJson,
paginationStrategy: const CursorPaginationStrategy(),
);
print(result.nextCursor); // cursor for the next pagefinal api = ApiKit(
baseUrl: 'https://api.example.com',
useCache: true,
cacheMaxAge: 300, // 5 minutes
);
// Concurrent identical GETs share a single in-flight request automatically.// Pure-Dart / Web: no-op checker is the default — no native plugin required.
final api = ApiKit(baseUrl: 'https://api.example.com');
// Flutter: inject a connectivity_plus-backed checker if desired.
final api = ApiKit(
baseUrl: 'https://api.example.com',
connectivityChecker: MyConnectivityChecker(),
);final silentLogger = ApiLogger(level: LogLevel.none);
final api = ApiKit(
baseUrl: 'https://api.example.com',
logger: silentLogger,
);When making HTTP requests, the default logger automatically logs requests, responses, and errors in a beautifully structured, developer-friendly card layout.
Key features:
- Visual Grouping: Every request, response, and error is clearly bordered using box-drawing characters so they don't get lost or interleaved in console noise.
- Response Timing: Displays the request duration (e.g.
⏱️ Duration: 125 ms) calculated from when the request was dispatched. - Sensitive Data Masking: Automatically masks credentials (like
Authorization: Bearer abcd12...3456) and cookie data to protect secrets during screen sharing or live debugging. - Prettified Data: Headers (with unwrapped list values) and JSON request/response payloads are neatly formatted and indented.
- Payload Truncation: Automatically truncates payloads longer than 10KB to prevent the debugger console from lagging or freezing.
- Copy as cURL: Outputs a ready-to-run, multi-line format of the cURL request directly inside the request card.
Example Request Output:
[ℹ️ INFO 09:01:56] ┌──────────────────────────────────────────────────────────────────────────────
│ 🚀 REQUEST: GET | https://jsonplaceholder.typicode.com/posts?_page=1&_limit=10
├──────────────────────────────────────────────────────────────────────────────
│ Headers:
│ Accept: application/json
│ Authorization: Bearer secr-t...oken
├──────────────────────────────────────────────────────────────────────────────
│ Body:
│ None
├──────────────────────────────────────────────────────────────────────────────
│ Copy as cURL:
│ curl --location 'https://jsonplaceholder.typicode.com/posts?_page=1&_limit=10' \
│ --request GET \
│ --header 'Accept: application/json' \
│ --header 'Authorization: Bearer secret-auth-token-12345'
└──────────────────────────────────────────────────────────────────────────────
final api = ApiKit(
baseUrl: 'https://api.example.com',
token: 'your-initial-auth-token',
maxRetries: 3,
retryDelayBase: 1000, // base delay in ms for exponential backoff
checkConnectivity: true,
useCache: true,
cacheMaxAge: 300, // cache expiration in seconds (5 minutes)
connectTimeout: 30000,
receiveTimeout: 30000,
sendTimeout: 30000,
);api.updateToken('new-user-token');
api.clearToken();api.addInterceptor(
onRequest: (options) async {
options.headers['X-Custom-Header'] = 'custom-value';
return options;
},
onResponse: (response) async {
print('Response status: ${response.statusCode}');
return response;
},
onError: (error) async {
print('Request failed: ${error.message}');
return error;
},
);api_network_kit is verified compatible with all six Flutter-supported
targets. The library contains no platform-specific imports (dart:io,
dart:html, Platform.*, kIsWeb) and is not a federated plugin — it is
pure Dart built on Dio, which supports all platforms natively.
| Platform | Compilation | Core methods | File methods | Status |
|---|---|---|---|---|
| Android | ✅ | ✅ | ✅ | Fully supported |
| iOS | ✅ | ✅ | ✅ | Fully supported |
| Web | ✅ | ✅ | Supported with caveat | |
| WASM | ✅ | ✅ | Supported with caveat | |
| Linux | ✅ | ✅ | ✅ | Fully supported |
| macOS | ✅ | ✅ | ✅ | Fully supported |
| Windows | ✅ | ✅ | ✅ | Fully supported |
In v1.0.x, the package depended on connectivity_plus — a native plugin that
blocked Web and WASM compilation. In v1.1.0, connectivity checking was
decoupled behind the ConnectivityChecker
interface with a NoOpConnectivityChecker
default that always reports a connection. connectivity_plus was moved to
dev_dependencies, so pure-Dart, Web, and WASM consumers no longer pull a
native plugin. Flutter consumers who want real connectivity checks can inject a
connectivity_plus-backed ConnectivityChecker implementation.
The uploadFile() and
downloadFile() methods rely on
Dio's MultipartFile.fromFile() and Dio.download() respectively, which
access the filesystem via dart:io under the hood. These two methods will
throw at runtime on Flutter Web and WASM when called. They do not block
compilation and do not affect any other method (get, post, put, delete,
getPaginated, getMap, getList, getRaw, etc. are fully platform-
agnostic).
On Web, use MultipartFile.fromBytes() with post() for uploads instead of
uploadFile().
get<T>()/getPaginated<T>()getMap()/getList()/getRaw()post<T>()/postMap()/postList()put<T>()/patch<T>()/delete<T>()uploadFile<T>()/uploadFiles<T>()/downloadFile()
All exceptions extend ApiBaseException and carry statusCode, data, and
errors from the server response body.
NetworkException,ServerException,NoInternetException,TimeoutException,ApiExceptionBadRequestException(400),UnauthorizedException(401),ForbiddenException(403),NotFoundException(404),ConflictException(409),UnprocessableEntityException(422),TooManyRequestsException(429)
PaginationStrategy(interface)OffsetPaginationStrategy,CursorPaginationStrategyPaginationConfig(configurable field keys)
CacheStore(interface)ApiCache(in-memory, TTL + ETag)RequestDeduplicator
ConnectivityChecker(interface)NoOpConnectivityChecker(default)
For a complete set of features, check the example directory for detailed implementations.
CHANGELOG.md— version history and release notes.