FlintClient is used to call APIs from Dart/Flutter apps: GET data, send POST/PUT/PATCH/DELETE requests, upload files, handle timeouts, retry failures, cache responses, and cancel in-flight requests.
If you need only basic requests, many packages can work. Use FlintClient when you want advanced behavior already integrated and consistent.
timeout, network, http, parse, cancelled)Step 1: Open terminal in your Dart/Flutter project folder.
Step 2: Install from pub.dev:
dart pub add flint_client
Step 3: Confirm dependency in pubspec.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:
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:
final response = await client.get<Map<String, dynamic>>('/users/42');
print(response.data);
final user = await client.get<Map<String, dynamic>>('/users/42');
final created = await client.post<Map<String, dynamic>>(
'/users',
body: {'name': 'Ada', 'role': 'admin'},
);
final replaced = await client.put<Map<String, dynamic>>(
'/users/42',
body: {'id': 42, 'name': 'Ada Lovelace', 'role': 'admin'},
);
final patched = await client.patch<Map<String, dynamic>>(
'/users/42',
body: {'role': 'owner'},
);
final removed = await client.delete('/users/42');
print(removed.statusCode);
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<Map<String, dynamic>>(
'/files/upload',
file: downloadedFile,
fieldName: 'file',
body: {'folder': 'invoices'},
);
final multiUpload = await client.uploadFiles<Map<String, dynamic>>(
'/files/upload-many',
files: {
'invoice': File('invoice.pdf'),
'avatar': File('avatar.png'),
},
body: {'ownerId': 42},
);
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.
final client = FlintClient(baseUrl: 'https://api.example.com');
final response = await client.get<Map<String, dynamic>>('/users/42');
if (response.isError) {
print(response.error?.message);
print(response.error?.statusCode);
}
Use FlintError.data to read the exact backend payload.
It can be a Map, List, String, or null (empty body).
if (response.isError) {
final raw = response.error?.data;
// raw can be Map/List/String/null based on backend response body
print(raw);
}
If you prefer exception flow, enable throwIfError on the client.
final client = FlintClient(
baseUrl: 'https://api.example.com',
throwIfError: true,
);
try {
await client.get<Map<String, dynamic>>('/users/42');
} on FlintError catch (e) {
print(e.statusCode);
print(e.data); // exact backend payload
}
final login = await client.post<Map<String, dynamic>>(
'/auth/login',
body: {'email': 'user@mail.com', 'password': 'secret'},
);
if (login.isSuccess) {
final token = login.data?['token'];
print(token);
}
final products = await client.get<List>(
'/products',
cacheConfig: const CacheConfig(maxAge: Duration(minutes: 5)),
);
final report = await client.get<String>(
'/reports/daily',
retryConfig: RetryConfig(
maxAttempts: 3,
delay: const Duration(milliseconds: 300),
),
);
final token = CancelToken();
final pending = client.get<String>('/export/huge', cancelToken: token);
token.cancel('user left page');
final response = await pending;
print(response.error?.kind); // FlintErrorKind.cancelled
final upload = await client.post<Map<String, dynamic>>(
'/files/upload',
files: {'file': File('report.pdf')},
body: {'folder': 'invoices'},
);
Retry means: if a request fails for temporary reasons (network glitch, timeout, server busy), try again automatically.
500, 503, 429).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.
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)),
);
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<String>('/reports/slow', cancelToken: token);
token.cancel('user aborted');
final response = await pending;
if (response.isError) {
print(response.error?.kind); // FlintErrorKind.cancelled
}
Cancellation means stopping an API request before it finishes.
Why use it?
When to use it?
client.request() is the generic request method.
get/post/put/patch/delete are convenience shortcuts.
Use request() when:
RequestOptions.final response = await client.request<Map<String, dynamic>>(
'POST',
'/users',
options: RequestOptions<Map<String, dynamic>>(
body: {'name': 'Ada'},
headers: {'Content-Type': 'application/json'},
parseMode: ResponseParseMode.lenient,
cancelToken: CancelToken(),
),
);
Quick model: get() is quick shortcut, request() is full-control entrypoint.
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'});
You can authenticate WebSocket connections in 3 common ways: Bearer header, query token, or app-level auth event.
FlintClient.ws()final client = FlintClient(
baseUrl: 'http://localhost:8080',
headers: {'Authorization': 'Bearer your-token'},
);
final ws = client.ws('/ws');
await ws.connect();
final ws = FlintWebSocketClient(
'ws://localhost:8080/ws',
sendTokenAsQuery: true,
queryTokenKey: 'token',
tokenProvider: () async => await loadTokenFromStorage(),
);
await ws.connect();
final ws = FlintWebSocketClient(
'ws://localhost:8080/ws',
autoAuthEvent: true,
authEventName: 'auth',
authPayload: {'token': 'your-token'},
);
await ws.connect();
For a full runnable demo (header auth + query auth + auth event), use:
dart run example/lib/websocket_auth_example.dart
Keep tests in test/ and usage examples in example/lib/.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flint_client/flint_client.dart';
Future<void> 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<void> _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<void>.delayed(const Duration(milliseconds: 300));
ws.dispose();
client.dispose();
}
Future<void> _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<void>.delayed(const Duration(milliseconds: 300));
ws.dispose();
}
Future<void> _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<void>.delayed(const Duration(milliseconds: 300));
ws.dispose();
}
Future<HttpServer> _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<String, dynamic>;
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;
}
Parse mode controls how FlintClient handles response data type mismatches.
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 tries best-effort conversion/fallback instead of failing immediately.
Use lenient mode for flexible APIs or UI pages where partial data is acceptable.
If you expect int but server returns text:
// Global default
final strictClient = FlintClient(
baseUrl: 'https://api.example.com',
defaultParseMode: ResponseParseMode.strict,
);
// Per-request override
final response = await strictClient.get<int>(
'/stats/value',
parseMode: ResponseParseMode.lenient,
);
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.
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).
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);
},
);
Use the end-to-end mock server example in the client repo to test cache/retry/cancel/hook behavior quickly.
dart run example/lib/full_observability_mock_example.dart
dart run example/lib/http_methods_and_download_example.dart