Dart
The Dart SDK is an idiomatic package emitted from the same GlottoIR as every other target.
Operations under a declared resource hang off an accessor — client.pets.listPets() — while an
operation you leave ungrouped stays a method on Client. Request/response shapes are Dart classes
with fromJson/toJson; binary download responses expose byte streams and bounded byte reads.
Manual pages expose typed items, full response metadata and explicit continuation. RequestOptions controls headers, deadlines, retries, extra parameters and cancellation. Binary operations return an owned BinaryDownload with bounded readAll, pipe, byte chunks, response metadata and close. Close the client when finished to release SDK-owned HTTP connections; injected clients remain caller-owned. See pagination, request retries and controls, streaming, and file transfers.
Quickstart
dart pub add petstore
import 'package:petstore/petstore.dart';
final client = Client(token: "<token>");
try {
final result = await client.pets.createPet(
PetCreate(name: "Biscuit", species: PetCreateSpecies.cat),
);
} finally {
client.close();
}
Typed models
Each GlottoIR model becomes a Dart class with fromJson/toJson; operations decode and return
the typed response and accept a typed request body. Discriminated unions resolve to the right
variant.
Typed errors
Operations throw an ApiError carrying the parsed error body, with an ApiErrorKind enum so
callers switch (error.kind) / compare ApiErrorKind.notFound rather than matching status codes.
See Errors.
Pagination
Paginated list methods return a Stream that walks every page as you listen, advancing the cursor
for you. See Pagination.
Retries & backoff
Transient failures (5xx, 429, transport errors) retry with exponential backoff and jitter,
configurable per client. See Retries & timeouts.
SSE streaming
Server-sent-event endpoints return a Stream of typed events decoded from the text/event-stream
framing. See Streaming and Authentication.
Unknown response fields
Your API can add a response field without it being a breaking change — but a generated class has no field to put it in, and its fromJson factory reads only the keys it declares. The generated
models keep it instead: a field the SDK wasn’t generated from is retained on decode, readable
through an accessor, and written back out when the model is re-serialized.
final pet = await client.pets.createPet(body);
// A field your API started returning after this SDK was generated.
final species = pet.extraFields['species'];
// Re-encoding preserves it — a read-modify-write never silently drops it.
final json = jsonEncode(pet.toJson());
extraFields is a getter returning Map<String, dynamic>, so nested objects and lists survive
intact, and retention is recursive.
Your existing construction still compiles: the retention parameter is optional and named.
The retained fields are read-only by design. To send a field your spec doesn’t model yet, use the per-call extra-body escape hatch rather than writing to the retained bag.
File uploads
multipart/form-data operations build the multipart body from their fields for you; an
application/octet-stream operation takes a positional List<int> body sent raw with the right
Content-Type.
Secure token storage
The client takes an optional tokenStore parameter. Supply one and the bearer token is read from it
per request (falling back to the static token); on OAuth2 client-credentials APIs the token cache
is also persisted through it, so a restarted app reuses a still-valid token instead of minting a new
one. See Authentication.
Because Flutter is a mobile platform, the contract is asynchronous — the shape a platform-backed store can actually implement:
abstract class TokenStore {
Future<String?> getToken();
// OAuth2 client-credentials APIs also get:
// Future<void> setToken(String value);
}
That means you can back it with flutter_secure_storage,
which keeps the credential in the iOS Keychain or Android Keystore:
class SecureTokenStore implements TokenStore {
final FlutterSecureStorage storage;
SecureTokenStore(this.storage);
@override
Future<String?> getToken() => storage.read(key: 'api_token');
}
Generating the adapter instead
Rather than writing that yourself, opt in and the SDK ships it:
targets:
dart:
secureStorage: true
Your client then carries a FlutterSecureStorageTokenStore, so wiring the platform store is one
expression:
const storage = FlutterSecureStorage();
final client = Client(tokenStore: FlutterSecureStorageTokenStore(read: storage.read));
Pass key: to choose the storage entry (useful when an API has several bearer-style schemes, each
with its own store). On OAuth2 client-credentials APIs the adapter also takes write: — pass
storage.write — so the token cache is persisted.
No storage backend is bundled and no dependency is added. The adapter takes the store’s methods,
not the store itself — Dart function types are structural, so storage.read fits whether it comes
from flutter_secure_storage, an encrypted store of your own, or a test double. Your pubspec.yaml
is unchanged either way, and leaving the flag off emits exactly the SDK you have today.
Connectivity- and lifecycle-aware retries
Two opt-in seams stop a mobile app burning its retry budget against a radio that is down or while it is backgrounded — the same pair the React Native, Kotlin-Android and Swift SDKs ship. Enable either, both, or neither:
targets:
dart:
connectivity: true # pause retries while the device is offline
lifecycle: true # pause backoff while the app is backgrounded
A default Dart SDK is unchanged. With the flags on, the client takes two more optional parameters and consults them before every attempt — a known-offline state waits and re-checks rather than spending the request, and a backgrounded app pauses backoff without consuming a retry attempt:
abstract class ConnectivityMonitor {
Future<bool> isOnline();
}
abstract class AppLifecycle {
Future<bool> isForeground();
}
Both reads are asynchronous, because the Flutter APIs behind them are — connectivity_plus exposes
Future<List<ConnectivityResult>> checkConnectivity(). A synchronous contract would be
unimplementable by the standard package, the same way a synchronous TokenStore was for
flutter_secure_storage.
No package is bundled and nothing imports Flutter, so the SDK stays a plain Dart package. Adapters take a closure, so wiring a real app is one expression per seam:
import 'package:connectivity_plus/connectivity_plus.dart';
final lifecycle = MutableAppLifecycle();
final client = Client(
'https://api.example.com',
connectivity: CallbackConnectivityMonitor(() async =>
(await Connectivity().checkConnectivity())
.any((r) => r != ConnectivityResult.none)),
lifecycle: lifecycle,
);
Flutter reports lifecycle by callback rather than by query, so MutableAppLifecycle is the shape a
WidgetsBindingObserver pushes into:
class _AppState extends State<App> with WidgetsBindingObserver {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
lifecycle.setForeground(state == AppLifecycleState.resumed);
}
}
There is also a CallbackAppLifecycle if you have a lifecycle source you can poll instead.