Vexio — The Ultimate Flutter HTTP Client

Zero dependencies. Simple as http. Powerful as Dio + Copper combined.

Pub Version Dart SDK Flutter Dependencies License Platform


The Problem With Every Other Package

http — Too Primitive

  • Raw Response object — 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
  • DioException is one type for everything — no granularity
  • RequestOptions / Response ceremony is verbose
  • Interceptors require understanding RequestInterceptorHandler
  • No built-in offline queue, no WebSocket, no GraphQL
  • FormData for 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 retryOn extension
  • 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

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

Libraries

vexio
Vexio — The Ultimate Flutter HTTP Client.