vexio 1.0.0
vexio: ^1.0.0 copied to clipboard
Vexio — The Ultimate Flutter HTTP Client. Zero dependencies, simple as http, powerful as Dio + Copper combined. Auto token refresh, caching, retry, WebSocket, GraphQL, offline queue, multipart, interc [...]
Vexio — The Ultimate Flutter HTTP Client #
Zero dependencies. Simple as http. Powerful as Dio + Copper combined.
The Problem With Every Other Package #
http — Too Primitive #
- Raw
Responseobject — you parse everything manually - No interceptors, no retry, no token refresh
- No caching, no deduplication, no offline support
- No WebSocket, no GraphQL, no multipart progress
- No typed exceptions — just
Exception - Requires 50+ lines of boilerplate per endpoint
Dio — Too Heavy, Too Complex #
- Large external dependency with its own ecosystem
DioExceptionis one type for everything — no granularityRequestOptions/Responseceremony is verbose- Interceptors require understanding
RequestInterceptorHandler - No built-in offline queue, no WebSocket, no GraphQL
FormDatafor uploads is cumbersome- Does not work on all platforms without config
Copper — Good Ideas, Incomplete #
- No WebSocket support
- No GraphQL support
- No offline queue
- No request deduplication
- Limited interceptor system
- Smaller community, less battle-tested
AutoPilot — Better, But Still Missing #
- No WebSocket support
- No GraphQL support
- No cancel tokens
- No batch requests
- No interceptor chain
- No
retryOnextension - Limited exception granularity
Why Vexio Wins #
| Feature | http | Dio | Copper | AutoPilot | Vexio |
|---|---|---|---|---|---|
| Zero external dependencies | YES | NO | NO | YES | YES |
| Simple one-line init | NO | NO | NO | YES | YES |
| Auto token injection | NO | Interceptor | YES | YES | YES |
| Auto token refresh on 401 | NO | Interceptor | YES | YES | YES |
| Request retry + backoff | NO | NO | YES | YES | YES |
| Response caching | NO | NO | YES | YES | YES |
| Request deduplication | NO | NO | NO | YES | YES |
| Offline request queue | NO | NO | NO | NO | YES |
| Cancel tokens | NO | YES | NO | NO | YES |
| Multipart upload | Manual | YES | YES | YES | YES |
| Upload progress | NO | YES | YES | YES | YES |
| File download + progress | NO | YES | YES | YES | YES |
| WebSocket built-in | NO | NO | NO | NO | YES |
| WS auto-reconnect | NO | NO | NO | NO | YES |
| GraphQL support | NO | NO | NO | NO | YES |
| Interceptor chain | NO | YES | NO | NO | YES |
| Typed exception hierarchy | NO | Partial | NO | NO | YES |
| Fluent extensions | NO | NO | NO | YES | YES |
| Batch parallel requests | NO | NO | NO | NO | YES |
| Runtime reconfiguration | NO | NO | NO | YES | YES |
| Rich VexioResponse model | NO | Partial | YES | YES | YES |
| Global middleware hooks | NO | Interceptor | NO | YES | YES |
| MIME auto-detection | NO | NO | NO | NO | YES |
| Named enveloped JSON | NO | NO | NO | YES | YES |
| Total Score | 2/23 | 10/23 | 10/23 | 13/23 | 23/23 |
Installation #
dependencies:
vexio: ^1.0.0
flutter pub get
No pod install. No gradle changes. No native setup. Just add and use.
60-Second Quickstart #
// Step 1 — Initialize once in main()
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Vexio.init(
baseUrl: 'https://api.example.com/v1',
enableLogs: true,
printPayload: true,
enableCache: true,
maxRetries: 3,
);
runApp(const MyApp());
}
// Step 2 — Use anywhere, no context, no ref
final api = Vexio.instance;
// GET single
final res = await api.get('/users/1', parser: User.fromJson);
if (res.isSuccess) print(res.data); // User
// POST
final login = await api.post('/auth/login',
body: {'email': email, 'password': password},
parser: LoginModel.fromJson,
);
// Step 3 — Set token after login (auto-injected forever)
await Vexio.setToken(login.data!.token);
All HTTP Methods #
final api = Vexio.instance;
// GET single model
final res = await api.get('/users/1', parser: User.fromJson);
// GET list
final res = await api.get('/users',
parser: (json) => (json as List).map(User.fromJson).toList(),
);
// GET with query params → /posts?userId=1&page=2
final res = await api.get('/posts',
queryParams: {'userId': 1, 'page': 2, 'limit': 10},
parser: (json) => (json as List).map(Post.fromJson).toList(),
);
// GET with cache
final res = await api.get('/config',
parser: Config.fromJson,
useCache: true,
cacheDuration: const Duration(hours: 24),
);
// POST
final res = await api.post('/auth/login',
body: {'email': email, 'password': password},
parser: LoginModel.fromJson,
);
// PUT / PATCH / DELETE
await api.put('/posts/1', body: {'title': 'Updated'});
await api.patch('/users/1', body: {'name': 'New Name'});
await api.delete('/posts/1');
// HEAD
await api.head('/health');
File Upload #
// Single file
await api.upload('/profile/photo',
fileKey: 'image',
filePath: pickedFile.path,
);
// Multiple files + fields + progress
await api.upload('/documents',
files: [
VexioFile(
key: 'doc',
filePath: file.path,
fileName: 'report.pdf',
mimeType: 'application/pdf',
),
],
fields: {'title': 'Q4 Report'},
onProgress: (sent, total) => setState(() => progress = sent / total),
parser: UploadModel.fromJson,
);
// From bytes (no file path needed)
await api.upload('/upload',
files: [
VexioFile.fromBytes(
key: 'avatar',
bytes: imageBytes,
fileName: 'avatar.png',
mimeType: 'image/png',
),
],
);
File Download #
final res = await api.download(
'/reports/jan.pdf',
'/storage/Download/jan.pdf',
onProgress: (recv, total) => print('${(recv / total * 100).toInt()}%'),
);
if (res.isSuccess) print('Saved to: ${res.data}');
WebSocket — Built-in with Auto-Reconnect #
// Create
final ws = api.websocket('/chat',
autoReconnect: true,
reconnectDelay: const Duration(seconds: 3),
maxReconnectAttempts: 10,
pingInterval: const Duration(seconds: 30),
);
// Connect
await ws.connect();
// Listen to messages
ws.messages.listen((msg) {
print('Received: $msg');
});
// Listen to status changes
ws.statusStream.listen((status) {
print('Status: $status'); // connected, disconnected, reconnecting
});
// Send
ws.send({'type': 'message', 'text': 'Hello!'});
ws.send('raw string');
// Disconnect
await ws.disconnect();
ws.dispose();
GraphQL — Built-in Support #
// Query
final res = await api.graphql(
'/graphql',
const VexioGraphQLRequest(
query: '''
query GetUser(\$id: ID!) {
user(id: \$id) { id name email }
}
''',
variables: {'id': '1'},
),
parser: (data) => User.fromJson(data['user']),
);
if (res.isSuccess) print(res.data); // User
if (res.hasErrors) print(res.errors);
// Mutation
final res = await api.graphql(
'/graphql',
const VexioGraphQLRequest(
query: 'mutation CreatePost(\$input: PostInput!) { createPost(input: \$input) { id } }',
variables: {'input': {'title': 'Hello', 'body': 'World'}},
operationName: 'CreatePost',
),
parser: (data) => Post.fromJson(data['createPost']),
);
Cancel Tokens #
final cancelToken = VexioCancelToken();
// Start a request
final future = api.get('/large-data',
parser: LargeModel.fromJson,
cancelToken: cancelToken,
);
// Cancel it (e.g. user navigated away)
cancelToken.cancel('User left the screen');
// Catch the cancellation
try {
final res = await future;
} on VexioCancelledException catch (e) {
print('Cancelled: ${e.message}');
}
Interceptors #
// Built-in interceptors
await Vexio.init(
baseUrl: 'https://api.example.com',
interceptors: [
VexioLoggingInterceptor(printPayload: true),
VexioAuthInterceptor(getToken: () async => myTokenService.token),
VexioHeaderInterceptor({'X-App-Version': '1.0.0'}),
],
);
// Custom interceptor
class AnalyticsInterceptor extends VexioInterceptor {
@override
Future<Map<String, String>> onRequest(
String method, String url, Map<String, String> headers,
) async {
Analytics.logRequest(method, url);
return headers;
}
@override
Future<VexioResponse<T>> onResponse<T>(VexioResponse<T> response) async {
Analytics.logResponse(response.statusCode, response.responseTime);
return response;
}
@override
Future<VexioResponse<T>?> onError<T>(
Object error, String method, String url,
) async {
Crashlytics.record(error);
return null; // return a VexioResponse to recover, null to propagate
}
}
// Add at runtime
Vexio.instance.addInterceptor(AnalyticsInterceptor());
Fluent Extensions #
// .handle() — no if/else needed
await api.get('/me', parser: User.fromJson)
.handle(
onSuccess: (data, msg) => setState(() => user = data),
onFailure: (msg, code) => showSnackbar(msg),
);
// .mapData() — transform type in chain
final res = await api.get<String>('/total')
.mapData((s) => int.parse(s)); // VexioResponse<int>
// .onSuccess() / .onFailure() — side effects
await api.post('/order', body: data)
.onSuccess((_) => Analytics.log('order_placed'))
.onFailure((msg, __) => Crashlytics.record(msg))
.handle(
onSuccess: (data, _) => navigateTo(SuccessPage(data)),
onFailure: (msg, _) => showError(msg),
);
// .retryOn() — retry specific request
final res = await api.get('/unstable-endpoint')
.retryOn(3, delay: const Duration(seconds: 2));
// .log() — debug in chain
final res = await api.get('/debug-me')
.log('UserFetch');
Typed Exception Hierarchy #
try {
final res = await api.get('/users/1', parser: User.fromJson);
} on VexioNoInternetException {
showSnackbar('Check your connection');
} on VexioTimeoutException {
showSnackbar('Request timed out');
} on VexioUnauthorizedException {
navigateTo(LoginPage());
} on VexioServerException catch (e) {
Crashlytics.record('Server error ${e.statusCode}');
} on VexioParseException catch (e) {
debugPrint('Parse failed: ${e.originalError}');
} on VexioCancelledException {
// User cancelled — no action needed
} on VexioException catch (e) {
// Catch-all for any other Vexio error
showSnackbar(e.message);
}
VexioResponse — Rich Model #
res.isSuccess // bool — 2xx + business logic success
res.data // T? — parsed model
res.message // String — server or error message
res.statusCode // int — 200, 401, 422, 500...
res.raw // dynamic — raw JSON body
res.errors // Map? — validation errors
res.requestId // String? — trace ID "a3f2b1"
res.responseTime // Duration? — round-trip time
res.timestamp // DateTime — when received
res.headers // Map<String, String>
res.fromCache // bool — came from cache
// Status helpers
res.isClientError // 400–499
res.isServerError // 500+
res.isUnauthorized // 401
res.isForbidden // 403
res.isNotFound // 404
res.isValidationError // 422
res.isTimeout // 408
res.isNoInternet // 0
res.isCancelled // -1
// Data helpers
res.dataOr(fallback) // safe access with fallback
res.dataOrThrow // throws if null
res.validationError('email') // first error for field
res.allErrors // all errors as string
res.cast<R>(newData) // cast to different type
Token Management #
await Vexio.setToken(token); // access token
await Vexio.setTokens(
accessToken: accessToken,
refreshToken: refreshToken,
);
await Vexio.clearTokens(); // logout
// Auto-refresh on 401
await Vexio.init(
enableTokenRefresh: true,
onRefreshToken: () async {
final refresh = VexioTokenManager.instance.refreshToken;
final res = await api.post('/auth/refresh',
body: {'refresh_token': refresh});
return res.data?['access_token'];
},
);
Caching #
// Global
await Vexio.init(enableCache: true, cacheDuration: Duration(minutes: 5));
// Per-request
await api.get('/config',
useCache: true,
cacheDuration: const Duration(hours: 1),
);
// Invalidate
api.invalidateCache('/config');
api.invalidateCachePattern('/users');
api.clearCache();
print(api.cacheSize); // entries in cache
Offline Queue #
await Vexio.init(
enableOfflineQueue: true,
maxOfflineQueueSize: 50,
);
// Mutations auto-queue when offline
await api.post('/orders',
body: {'product': 'ABC'},
queueIfOffline: true, // queued if no internet, replayed when back online
);
print(api.pendingOfflineRequests); // count of queued requests
Batch Requests #
final results = await api.batch([
() => api.get('/users', parser: (j) => (j as List).map(User.fromJson).toList()),
() => api.get('/posts', parser: (j) => (j as List).map(Post.fromJson).toList()),
() => api.get('/config', parser: Config.fromJson),
]);
final users = results[0].data as List<User>;
final posts = results[1].data as List<Post>;
final config = results[2].data as Config;
Runtime Reconfiguration #
// Switch environment
api.reconfigure((c) => c.copyWith(baseUrl: 'https://staging.api.com'));
api.reconfigure((c) => c.copyWith(baseUrl: 'https://api.example.com'));
// Increase timeout on slow network
api.reconfigure((c) => c.copyWith(timeoutSeconds: 60));
// Add global header at runtime
api.reconfigure((c) => c.copyWith(
globalHeaders: {...c.globalHeaders, 'X-Locale': 'en-US'},
));
Full Configuration #
await Vexio.init(
baseUrl: 'https://api.example.com/v1', // required
tokenType: 'Bearer',
timeoutSeconds: 30,
maxRetries: 3,
retryDelay: const Duration(seconds: 1),
exponentialBackoff: true,
enableCache: true,
cacheDuration: const Duration(minutes: 5),
maxCacheEntries: 200,
enableLogs: true,
printPayload: true,
prettyPrint: true,
enableTokenRefresh: true,
onRefreshToken: () async => newToken,
enableDeduplication: true,
enableOfflineQueue: true,
maxOfflineQueueSize: 50,
globalHeaders: {'X-App-Version': '1.0.0'},
successKey: 'status',
successValue: true,
messageKey: 'message',
dataKey: 'data',
errorsKey: 'errors',
onError: (msg, code) => showSnackbar(msg),
onRequestSent: (url, method) => Analytics.log(url),
onResponseReceived: (url, code, time) => Metrics.record(url, code),
enableGlobalLoader: true,
onLoadingChanged: (v) => AppController.setLoading(v),
maxConnections: 8,
enableKeepAlive: true,
followRedirects: true,
maxRedirects: 5,
retryStatusCodes: [408, 429, 500, 502, 503, 504],
validateCertificate: true,
interceptors: [
VexioLoggingInterceptor(),
VexioHeaderInterceptor({'X-Platform': 'flutter'}),
],
);
State Manager Integration #
With Vexo (Recommended) #
class UserController extends VexoController {
final user = VexoAsyncState<User>();
Future<void> fetch() => user.execute(
() => Vexio.instance.get('/me', parser: User.fromJson)
.then((r) => r.dataOrThrow),
);
}
GetX #
class UserController extends GetxController {
final user = Rx<User?>(null);
final isLoading = false.obs;
Future<void> fetch() async {
isLoading.value = true;
await Vexio.instance.get('/me', parser: User.fromJson)
.handle(
onSuccess: (data, _) => user.value = data,
onFailure: (msg, _) => Get.snackbar('Error', msg),
);
isLoading.value = false;
}
}
Riverpod #
class UserNotifier extends AsyncNotifier<User?> {
@override Future<User?> build() async => null;
Future<void> fetch() async {
state = const AsyncLoading();
final res = await Vexio.instance.get('/me', parser: User.fromJson);
state = res.isSuccess
? AsyncData(res.data)
: AsyncError(res.message, StackTrace.current);
}
}
BLoC #
class UserCubit extends Cubit<UserState> {
UserCubit() : super(const UserInitial());
Future<void> fetch() async {
emit(const UserLoading());
final res = await Vexio.instance.get('/me', parser: User.fromJson);
emit(res.isSuccess ? UserLoaded(res.data!) : UserError(res.message));
}
}
Provider #
class UserProvider extends ChangeNotifier {
User? user;
bool isLoading = false;
Future<void> fetch() async {
isLoading = true; notifyListeners();
final res = await Vexio.instance.get('/me', parser: User.fromJson);
user = res.data;
isLoading = false; notifyListeners();
}
}
Debug Logs #
┌────────────────────────────────────────────────────
│ 🚀 REQUEST [a3f2b1] 14:32:01.432
│ POST https://api.example.com/v1/auth/login
│ ⊳ Body
│ {
│ "email": "john@example.com",
│ "password": "••••••••"
│ }
┌────────────────────────────────────────────────────
│ ✅ RESPONSE [a3f2b1] 14:32:01.687
│ Status: 200 ⏱ 255ms
│ ⊳ Payload
│ { "status": true, "message": "Login successful", "data": {...} }
┌────────────────────────────────────────────────────
│ 💥 ERROR [b7d3e2] 14:35:22.901
│ Status: 422
│ Validation failed
Auto-disabled in release builds. Zero configuration needed.
Architecture #
vexio/
├── lib/
│ ├── vexio.dart ← single import
│ └── src/
│ ├── core/vexio_core.dart ← main HTTP engine (dart:io)
│ ├── models/
│ │ ├── vexio_response.dart ← rich response model
│ │ └── vexio_config.dart ← config + VexioFile + CancelToken
│ ├── exceptions/vexio_exceptions.dart ← typed exception hierarchy
│ ├── interceptors/vexio_interceptor.dart ← interceptor chain
│ ├── auth/vexio_token_manager.dart ← memory-first token storage
│ ├── cache/vexio_cache.dart ← two-layer cache
│ ├── connectivity/ ← socket lookup
│ ├── logger/vexio_logger.dart ← colored ANSI logger
│ ├── parser/vexio_parser.dart ← smart JSON parser
│ ├── storage/vexio_storage.dart ← pure Dart file storage
│ ├── offline/vexio_offline_queue.dart ← offline request queue
│ ├── websocket/vexio_websocket.dart ← WS + auto-reconnect
│ ├── graphql/vexio_graphql.dart ← GraphQL support
│ └── extensions/vexio_extensions.dart ← fluent chain methods
├── example/lib/main.dart ← full demo app
├── test/vexio_test.dart ← 40+ tests
└── pubspec.yaml ← ZERO external deps
Performance #
| Metric | http | Dio | Vexio |
|---|---|---|---|
| Boilerplate LOC per endpoint | 50+ | 15 | 3 |
| External dependencies | 1 | 3+ | 0 |
| Token read latency | N/A | N/A | ~0ms (memory) |
| Duplicate GETs | All hit network | All hit network | 1 hits network |
| APK size impact | +50KB | +300KB | +0KB |
| WebSocket built-in | NO | NO | YES |
| GraphQL built-in | NO | NO | YES |
Roadmap #
- v1.0.0 — Full release (current)
- v1.1.0 — SSE (Server-Sent Events) support
- v1.2.0 — Request signing (HMAC)
- v1.3.0 — Mock client for testing
- v2.0.0 — Code generator (
@GET('/users')annotations)
License #
MIT License — Mysterious Coder