UNIVERSAL DART & FLUTTER CLIENT

Flint Client SDK

A production-ready HTTP engine engineered for resilience, performance, and typed simplicity.

Eliminate boilerplate across mobile, desktop, CLI, and web apps. FlintClient gives you exponential backoff retries, multi-tier LRU caching, cooperative cancellation tokens, multipart file streaming, and full-duplex WebSocket channels in pure Dart.

$dart pub add flint_client
Idempotent requests automatically retry on 429, 502, 503, and network timeouts with jitter.
Exponential Backoff
import 'package:flint_client/flint_client.dart'; final client = FlintClient( baseUrl: 'https://api.flintdart.dev/v1', retryPolicy: RetryPolicy( maxAttempts: 4, initialDelay: Duration(milliseconds: 200), maxDelay: Duration(seconds: 5), retryOnStatuses: {429, 502, 503, 504}, backoffStrategy: BackoffStrategy.exponentialWithJitter, ), ); // Transparently retries with exponential backoff on transient failure final response = await client.get<Map<String, dynamic>>( '/analytics/realtime', headers: {'X-Correlation-Id': Uuid().v4()}, ); print('Payload: ${response.data}');
HTTP 200 OK • 3 attempts • 412ms
Pure Dart • Zero Platform Glue
BUILT FOR RELIABILITY

Everything you need in a modern HTTP client.

No need to assemble 5 separate packages. FlintClient provides integrated retry policies, caching strategies, cooperative cancellation, and full error diagnostics out of the box.

Idempotent Retry Policy

Automatic exponential backoff with jitter, retry budgets, and standard HTTP 429 Retry-After header awareness.

Multi-Tier Caching

In-memory LRU combined with persistent disk cache. Supports cacheFirst, networkFirst, and staleWhileRevalidate strategies.

Cooperative Cancellation

Pass CancellationToken to any request. Abort network sockets cleanly with dedicated FlintCancelledException handling.

Strict & Lenient Deserialization

Type-safe response decoding. Toggle between strict schema validation or lenient parsing with fallback data serializers.

Observability & Interceptors

Comprehensive request lifecycle hooks. Inject Correlation IDs, log traffic, track latency, and export OpenTelemetry metrics.

Full-Duplex WebSockets

Built-in WebSocket client with automatic heartbeat pings, exponential reconnect backoff, and typed JSON stream channels.

UNDER THE HOOD

The FlintClient Pipeline Flow

Every request travels through a deterministic, observable lifecycle before returning typed data to your application.

1. Dispatch

Intercept headers & inject Correlation ID

2. Cache Check

Evaluate LRU store & TTL policy

3. Transport

Stream HTTP/2 or HTTP/1.1 socket payload

4. Resilience

Handle 429/503 retry backoff & jitter

5. Decode

Deserialize typed model or throw structured error

DIAGNOSTICS & EXCEPTIONS

Predictable, Structured Error Handling

Never catch generic exceptions again. Every failure belongs to a typed category with HTTP status codes, original response bodies, and diagnostic stack traces.

FlintHttpException

HTTP 4xx/5xx status codes with decoded server error body.

FlintTimeoutException

Distinct connect, read, and write socket timeout boundaries.

FlintNetworkException

DNS failures, offline state, connection refused, or SSL errors.

FlintParseException

JSON syntax or schema deserialization validation errors.

FlintCancelledException

Request explicitly aborted via CancellationToken.

FlintRateLimitException

HTTP 429 with parsed duration from Retry-After header.

FlintClient

FlintClient Guide

A production-ready Dart HTTP client with retries, caching, cancellation, structured errors, lifecycle hooks, and strict or lenient parsing.

pub.dev GitHub [Examples](/examples)

On This Page

[Basics](#basics) [HTTP Methods](#methods) [Files](#files) [WebSocket](#websocket) [Error Handling](#error-handling) [Parse Modes](#parse-modes) [Observability](#observability)

What Is FlintClient Used For?

FlintClient is used to call APIs from Dart/Flutter apps: GET data, send POST/PUT/PATCH/QUERY/DELETE requests, upload files, handle timeouts, retry failures, cache responses, and cancel in-flight requests.

  • Mobile app calling backend endpoints
  • Dashboard fetching reports from APIs
  • CLI tools that consume web services
  • Apps that need retry + cache + cancellation built-in

Why Use FlintClient Instead of Another Package?

If you need only basic requests, many packages can work. Use FlintClient when you want advanced behavior already integrated and consistent.

  • Built-in cache layer with TTL control
  • Idempotency-aware retries with backoff and Retry-After support
  • Cancellation support with dedicated cancellation error kind
  • Structured error model (timeout, network, http, parse, cancelled)
  • Request lifecycle hooks for observability and correlation IDs
  • Strict vs lenient parse modes with serializer chains

Step-by-Step (Beginner Friendly)

Step 1: Open terminal in your Dart/Flutter project folder.

Step 2: Install from pub.dev:

CodeBlock bash
dart pub add flint_client

Step 3: Confirm dependency in pubspec.yaml:

CodeBlock yaml
dependencies:
  flint_client: ^x.y.z

Tip: your exact version may be different. Use the version shown on pub.dev.

Step 4: Import and create a client:

CodeBlock dart
import 'package:flint_client/flint_client.dart';

final client = FlintClient(
  baseUrl: 'https://api.example.com',
  timeout: const Duration(seconds: 10),
  debug: true,
  defaultParseMode: ResponseParseMode.lenient,
);

Step 5: Make your first request:

CodeBlock dart
final response = await client.get>('/users/42');
print(response.data);

Basic Requests

CodeBlock dart
final user = await client.get>('/users/42');

final created = await client.post>(
  '/users',
  body: {'name': 'Ada', 'role': 'admin'},
);

final replaced = await client.put>(
  '/users/42',
  body: {'id': 42, 'name': 'Ada Lovelace', 'role': 'admin'},
);

final patched = await client.patch>(
  '/users/42',
  body: {'role': 'owner'},
);

final searched = await client.query>(
  '/products/search',
  body: {'category': 'electronics'},
);

final removed = await client.delete('/users/42');
print(removed.statusCode);

HTTP QUERY

HTTP QUERY is defined by RFC 10008. It is safe and idempotent like GET, but it can include request content like POST. Use it for complex searches and filters that should not mutate server state.

CodeBlock dart
final response = await client.query<Map<String, dynamic>>(
  '/products/search',
  queryParameters: {'page': 1},
  body: {
    'category': 'electronics',
    'minimumPrice': 50000,
    'inStock': true,
  },
);

queryParameters are URI query string values. body is QUERY request content and uses FlintClient's normal JSON, text, form, timeout, interceptor, retry, cache, and response parsing pipeline. QUERY is treated as idempotent for retry configuration.

Compatibility warning: some proxies, browsers, servers, and API tools may not support QUERY yet. RFC: https://www.rfc-editor.org/rfc/rfc10008.html.

File Download + Upload

CodeBlock dart
import 'dart:io';

final tempDir = Directory.systemTemp;
final savePath = '${tempDir.path}/report.pdf';

final downloadedFile = await client.downloadFile(
  'https://example.com/report.pdf',
  savePath: savePath,
  onProgress: (received, total) {
    if (total > 0) {
      final percent = (received / total * 100).toStringAsFixed(0);
      print('Download: $percent%');
    }
  },
);

final upload = await client.uploadFile>(
  '/files/upload',
  file: downloadedFile,
  fieldName: 'file',
  body: {'folder': 'invoices'},
);

final multiUpload = await client.uploadFiles>(
  '/files/upload-many',
  files: {
    'invoice': File('invoice.pdf'),
    'avatar': File('avatar.png'),
  },
  body: {'ownerId': 42},
);

Error Handling

By default, FlintClient returns an error response object for failed requests (4xx/5xx). This means your request resolves with response.isError == true and details in response.error.

CodeBlock dart
final client = FlintClient(baseUrl: 'https://api.example.com');

final response = await client.get>('/users/42');
if (response.isError) {
  print(response.error?.message);
  print(response.error?.statusCode);
}

Access Raw Backend Error Payload

Use FlintError.data to read the exact backend payload. It can be a Map, List, String, or null (empty body).

CodeBlock dart
if (response.isError) {
  final raw = response.error?.data;
  // raw can be Map/List/String/null based on backend response body
  print(raw);
}

Throw Instead of Returning Error Response

If you prefer exception flow, enable throwIfError on the client.

CodeBlock dart
final client = FlintClient(
  baseUrl: 'https://api.example.com',
  throwIfError: true,
);

try {
  await client.get>('/users/42');
} on FlintError catch (e) {
  print(e.statusCode);
  print(e.data); // exact backend payload
}

Practical Examples You Can Use Today

1) Login Request

CodeBlock dart
final login = await client.post>(
  '/auth/login',
  body: {'email': 'user@mail.com', 'password': 'secret'},
);

if (login.isSuccess) {
  final token = login.data?['token'];
  print(token);
}

2) Product List With Cache

CodeBlock dart
final products = await client.get
- ( '/products', cacheConfig: const CacheConfig(maxAge: Duration(minutes: 5)), ); ``` ### 3) Retry a Flaky Endpoint ```dart final report = await client.get( '/reports/daily', retryConfig: RetryConfig( maxAttempts: 3, delay: const Duration(milliseconds: 300), ), ); ``` ### 4) Cancel Long Request ```dart final token = CancelToken(); final pending = client.get('/export/huge', cancelToken: token); token.cancel('user left page'); final response = await pending; print(response.error?.kind); // FlintErrorKind.cancelled ``` ### 5) Upload File ```dart final upload = await client.post>( '/files/upload', files: {'file': File('report.pdf')}, body: {'folder': 'invoices'}, ); ``` ## Retry and Cache ### Why Retry? Retry means: if a request fails for temporary reasons (network glitch, timeout, server busy), try again automatically. Internet can fail for a moment.

- Server can return temporary errors (`500`, `503`, `429`).

- User sees fewer random failures.

### What Is Cache?

Cache is saved response data kept for a short time, so you do not call the server again for the same request.

Example: open product list once, save for 5 minutes, open again and show instantly.

### Why Use Retry + Cache Together?

- **Retry** improves reliability.

- **Cache** improves speed and reduces API calls.

- Together, your app feels faster and fails less.

final client = FlintClient( baseUrl: 'https://api.example.com', defaultRetryConfig: RetryConfig( maxAttempts: 3, delay: const Duration(milliseconds: 250), maxRetryTime: const Duration(seconds: 2), honorRetryAfter: true, ), defaultCacheConfig: const CacheConfig(maxAge: Duration(minutes: 2)), );

CodeBlock code

## Cancellation

Cancellation means stopping a request before it finishes. Use it when user leaves a page, presses cancel, or the response is no longer needed.

final token = CancelToken(); final pending = client.get('/reports/slow', cancelToken: token);

token.cancel('user aborted'); final response = await pending; if (response.isError) { print(response.error?.kind); // FlintErrorKind.cancelled }

CodeBlock code

## What Is Cancellation? Why and When?

Cancellation means stopping an API request before it finishes.

**Why use it?**

- User leaves page/screen.

- User changes search/filter quickly.

- Request is slow and no longer needed.

- Prevents old responses from overwriting new UI data.

**When to use it?**

- Live search/autocomplete

- Navigation between pages

- Long exports/downloads

- Rapid repeated requests

## client.request() Explained

`client.request()` is the generic request method. `get/post/put/patch/delete` are convenience shortcuts.

**Use request() when:**

- HTTP method is dynamic at runtime.

- You want one reusable request function.

- You want full control via `RequestOptions`.

final response = await client.request>( 'POST', '/users', options: RequestOptions>( body: {'name': 'Ada'}, headers: {'Content-Type': 'application/json'}, parseMode: ResponseParseMode.lenient, cancelToken: CancelToken(), ), );

CodeBlock code

Quick model: `get()` is quick shortcut, `request()` is full-control entrypoint.

## WebSocket vs Socket.IO (Important)

FlintClient supports **WebSocket** only. It can look like Socket.IO style because it uses event-style `emit/on` writing, but it does **not** implement Socket.IO protocol.

With FlintDart, you can create a simple “glue” convention: event names + JSON payload format, so it feels Socket.IO-like while staying pure WebSocket.

final ws = client.ws('/chat'); await ws.connect();

ws.on('message', (data) => print(data)); ws.emit('message', {'text': 'Hello from client'});

CodeBlock code

## WebSocket Auth (Client Examples)

You can authenticate WebSocket connections in 3 common ways: Bearer header, query token, or app-level auth event.

### 1) Bearer Header via `FlintClient.ws()`

final client = FlintClient( baseUrl: 'http://localhost:8080', headers: {'Authorization': 'Bearer your-token'}, );

final ws = client.ws('/ws'); await ws.connect();

CodeBlock code

### 2) Token in Query

final ws = FlintWebSocketClient( 'ws://localhost:8080/ws', sendTokenAsQuery: true, queryTokenKey: 'token', tokenProvider: () async => await loadTokenFromStorage(), );

await ws.connect();

CodeBlock code

### 3) Auth Event After Connect

final ws = FlintWebSocketClient( 'ws://localhost:8080/ws', autoAuthEvent: true, authEventName: 'auth', authPayload: {'token': 'your-token'}, );

await ws.connect();

CodeBlock code

## Run the WebSocket Auth Example

For a full runnable demo (header auth + query auth + auth event), use:

dart run example/lib/websocketauthexample.dart

CodeBlock code

Keep tests in `test/` and usage examples in `example/lib/`.

## Full WebSocket Auth Example Code

import 'dart:async'; import 'dart:convert'; import 'dart:io';

import 'package:flintclient/flintclient.dart';

Future main() async { final server = await _startMockWsServer(); final httpBaseUrl = 'http://localhost:${server.port}'; final wsUrl = 'ws://localhost:${server.port}/ws';

await headerAuthExample(httpBaseUrl); await queryAuthExample(wsUrl); await _authEventExample(wsUrl);

await server.close(force: true); }

Future _headerAuthExample(String httpBaseUrl) async { final client = FlintClient( baseUrl: httpBaseUrl, headers: {'Authorization': 'Bearer header-token-123'}, debug: true, );

final ws = client.ws('/ws', params: {'example': 'header'}); ws.on('connect', (_) => print('Connected with header token')); ws.on('ack', (data) => print('Server ack: $data'));

await ws.connect(); ws.emit('message', {'text': 'hello from header auth'});

await Future.delayed(const Duration(milliseconds: 300)); ws.dispose(); client.dispose(); }

Future _queryAuthExample(String wsUrl) async { final ws = FlintWebSocketClient( wsUrl, params: {'example': 'query'}, sendTokenAsQuery: true, queryTokenKey: 'token', tokenProvider: () async => 'query-token-456', debug: true, );

ws.on('connect', (_) => print('Connected with query token')); ws.on('ack', (data) => print('Server ack: $data'));

await ws.connect(); ws.emit('message', {'text': 'hello from query auth'});

await Future.delayed(const Duration(milliseconds: 300)); ws.dispose(); }

Future _authEventExample(String wsUrl) async { final ws = FlintWebSocketClient( wsUrl, params: {'example': 'event'}, autoAuthEvent: true, authEventName: 'auth', authPayload: {'token': 'event-token-789'}, debug: true, );

ws.on('connect', (_) => print('Connected, auth event will auto-send')); ws.on('authed', (data) => print('Auth accepted: $data')); ws.on('ack', (data) => print('Server ack: $data'));

await ws.connect(); ws.emit('message', {'text': 'hello after auth event'});

await Future.delayed(const Duration(milliseconds: 300)); ws.dispose(); }

Future _startMockWsServer() async { final server = await HttpServer.bind('localhost', 0);

server.listen((request) async { if (request.uri.path != '/ws') { request.response ..statusCode = 404 ..write('Not found') ..close(); return; }

final authHeader = request.headers.value(HttpHeaders.authorizationHeader); final tokenFromQuery = request.uri.queryParameters['token']; final exampleType = request.uri.queryParameters['example'] ?? 'unknown';

final socket = await WebSocketTransformer.upgrade(request); socket.add( jsonEncode({ 'event': 'ack', 'data': { 'example': exampleType, 'authHeader': authHeader, 'tokenFromQuery': tokenFromQuery, }, }), );

socket.listen((raw) { try { final msg = jsonDecode(raw.toString()) as Map; final event = msg['event']?.toString() ?? ''; final data = msg['data'];

if (event == 'auth') { socket.add(jsonEncode({'event': 'authed', 'data': data})); return; } if (event == 'ping') { socket.add(jsonEncode({'event': 'pong'})); return; }

socket.add( jsonEncode({ 'event': 'message', 'data': {'echo': data}, }), ); } catch (_) {} }); });

return server; }

CodeBlock code

## Strict vs Lenient Parse Modes

Parse mode controls how FlintClient handles response data type mismatches.

### Strict Mode

Strict mode fails fast. If response data cannot be parsed to expected type, FlintClient returns a parse error.

Use strict mode when data correctness is critical (finance, billing, admin rules).

### Lenient Mode

Lenient mode tries best-effort conversion/fallback instead of failing immediately.

Use lenient mode for flexible APIs or UI pages where partial data is acceptable.

### Example Mismatch

If you expect `int` but server returns text:

- **Strict:** returns parse error.

- **Lenient:** may fallback or coerce value.

// Global default final strictClient = FlintClient( baseUrl: 'https://api.example.com', defaultParseMode: ResponseParseMode.strict, );

// Per-request override final response = await strictClient.get( '/stats/value', parseMode: ResponseParseMode.lenient, );

CodeBlock code

## Observability Hooks and Context

Observability means seeing what your HTTP client is doing in real time: request start, retry events, cache hits, errors, and request end.

`RequestContext` is the shared request state that flows through hooks/interceptors. It carries things like correlation ID, attempt number, timing, and cache metadata.

### Why Use Hooks?

- Debug production issues faster.

- Measure latency and retry patterns.

- Track cache efficiency (hit/miss behavior).

- Attach correlation IDs for tracing across services.

### Hook Quick Meaning

- `onRequestStart`: request just started.

- `onRetry`: request failed and will retry.

- `onCacheHit`: response served from cache.

- `onError`: error happened (with retry intent flag).

- `onRequestEnd`: request completed (success or error).

### Common Flow

Start → (maybe cache hit) or network call → (maybe retry/error) → end. The same `RequestContext` follows the whole flow.

final client = FlintClient( baseUrl: 'https://api.example.com', lifecycleHooks: RequestLifecycleHooks( onRequestStart: (ctx) => print('START ${ctx.correlationId}'), onRetry: (ctx, err, delay) => print('RETRY ${ctx.attempt} in $delay'), onCacheHit: (ctx, key, _) => print('CACHE HIT $key'), onError: (ctx, err, willRetry) => print('ERROR ${err.kind} willRetry=$willRetry'), onRequestEnd: (ctx, response, error) => print('END status=${response?.statusCode} duration=${ctx.totalDuration}'), ), contextualRequestInterceptor: (request, ctx) async { request.headers.set('X-Correlation-Id', ctx.correlationId); }, );

CodeBlock code

## Run Full Local Demo

Use the end-to-end mock server example in the client repo to test cache/retry/cancel/hook behavior quickly.

dart run example/lib/fullobservabilitymock_example.dart

CodeBlock code

dart run example/lib/httpmethodsanddownloadexample.dart

CodeBlock code

Start Building with FlintClient

Production-ready HTTP with typed responses, retries, caching, and WebSockets — all in pure Dart.

Flint Dart logo
Flint EcosystemThe Unified Dart Technology Stack

One language powering Full-Stack Web, Cross-Platform Clients, Native AI, and Connected Robotics.

Fullstack
Client SDK
AI Engine
Hardware
Copyright 2026 Flint Dart. Maintained by Eulogia Technologies.
v 1.3.2
MIT License
Built with Dart