dartvel 0.1.1 copy "dartvel: ^0.1.1" to clipboard
dartvel: ^0.1.1 copied to clipboard

Laravel-native API client for Flutter: contract envelope parsing, typed validation errors, Paginator, Sanctum token lifecycle, device registration.

dartvel

license: MIT

A Laravel-native API client for Flutter. It speaks the gaitco/dartvel contract: envelope parsing ({"data": ..., "message": ..., "meta": ...}), typed exceptions built from the backend's error_code, Paginator<T> built from Laravel's own pagination meta, a Sanctum-shaped token lifecycle, and device registration for push notifications.

Is this the right package for you? #

This package parses one specific wire contract, not arbitrary JSON APIs. It is the right choice if your backend is a Laravel app using the gaitco/dartvel composer package — or one that hand-rolls responses matching that same envelope and error_code shape.

It is not the right choice if:

  • Your API has its own response envelope (a different pagination shape, different error format, no error_code) — this package cannot parse it, and bending your backend to match it just to use this client is backwards. Reach for dio directly and write your own decoding.
  • You need offline caching, broadcasting/realtime, file uploads, or ready-made UI widgets. None of that exists here in v0.1 — this is a thin HTTP + parsing layer, nothing more. (Realtime is a separate concern — see laravel_reverb if you need it.)
  • You want codegen from an OpenAPI/Scramble document. That's a planned v0.2 (php artisan dart:generate), not shipped yet.

If your backend already speaks (or is willing to speak) the Dartvel contract, this package removes the boilerplate of envelope unwrapping, error mapping, and pagination parsing that every Laravel + Flutter app otherwise reimplements by hand.

Install #

flutter pub add dartvel

Quickstart #

final client = DartvelClient(
  baseUrl: 'https://api.example.com/api',
  tokenStore: SecureTokenStore(),
  localeResolver: () => 'ar',
);

final auth = AuthApi(client: client, decodeUser: User.fromJson);
final user = await auth.login({'email': email, 'password': password});

final posts = await client.getPaginated(
  '/posts',
  page: const PageRequest(page: 1, perPage: 20),
  decodeItem: Post.fromJson,
);

try {
  await client.post('/posts', body: draft.toJson(), decode: Post.fromJson);
} on ValidationException catch (e) {
  formErrors.value = e.errors;
}

A full, dart analyze-clean version of this snippet (with stub User/Post models) lives in example/dartvel_example.dart.

How it fits together #

How dartvel fits together

DartvelClient wraps a Dio instance. Every request:

  • attaches Accept: application/json
  • attaches X-localization: <locale> if localeResolver returns non-null
  • attaches Authorization: Bearer <token> if tokenStore holds one (MemoryTokenStore by default — pass SecureTokenStore() for flutter_secure_storage-backed persistence)
  • checks the response's X-Contract-Version header: if the backend's major version doesn't match the client's, every call throws ContractViolationException instead of silently misparsing a future wire format
  • unwraps the envelope into Envelope<T> (data, message, meta) via decode, or throws a typed DartvelException

get/post/put/patch/delete all take a required decode callback and return Future<Envelope<T>>; post/put/patch also take body. getPaginated decodes each list item with decodeItem and returns a Paginator<T> built from Laravel's pagination meta (current_page/last_page/per_page/total/has_more).

Error handling #

Every non-2xx contract response ({"message": ..., "error_code": ...}) maps to one of these typed exceptions, all subclasses of the sealed DartvelException:

Exception error_code Typical HTTP status
ValidationException (carries errors: Map<String, List<String>> and firstError) validation_failed 422
UnauthenticatedException unauthenticated 401
ForbiddenException forbidden 403
NotFoundException not_found 404
RateLimitedException rate_limited 429
ServerException server_error, or any error_code this client doesn't recognize 500
ConnectionException — no response reached the client (timeout, no network, DNS, etc.)
ContractViolationException (carries expected) — response body isn't valid contract JSON, or the backend's X-Contract-Version major doesn't match this client's any

DartvelException is sealed, so an exhaustive switch on it is a compile-time guarantee — the analyzer flags a missing case the moment a new subtype ships.

Auth: AuthApi #

final auth = AuthApi<User>(client: client, decodeUser: User.fromJson);

auth.authState.listen((status) {
  // AuthStatus.unknown | .authenticated | .unauthenticated
});

await auth.restore(); // reads the token store, emits the current status

final user = await auth.login({'email': email, 'password': password});
await auth.requestOtp({'phone': phone});
final otpUser = await auth.verifyOtp({'phone': phone, 'otp': code});
final me = await auth.me();
await auth.logout(); // always clears the token store, even if the server call fails

login/verifyOtp expect the backend to return {"data": {"token": "...", "user": {...}}} — anything else throws ContractViolationException. Both write the token to client.tokenStore and emit AuthStatus.authenticated on authState before returning the decoded user. All auth routes default to the /auth prefix (login, otp/verify, otp/request, me, logout); pass prefix: to AuthApi to change it.

Device registration #

DeviceRegistrar posts to /devices (or a custom path) and retries connection/server failures with a linear backoff (retryDelay * attempt, up to maxAttempts). It does not depend on firebase_messaging — feed it whatever push token your app already has:

path is appended to client's baseUrl as-is — it is not authority-relative. DartvelClient's baseUrl already ends in /api (see Quickstart above), and the default path: '/devices' reaches the backend out of the box because gaitco/dartvel's own route_prefix defaults to 'api/devices' — see laravel/README.md's Devices section for the matching Laravel-side config.

final registrar = DeviceRegistrar(client: client);

final fcmToken = await FirebaseMessaging.instance.getToken();
await registrar.register(
  uniqueId: deviceId,
  fcmToken: fcmToken,
  type: 'android',
  locale: 'ar',
  timezone: 'Africa/Cairo',
);

Only ValidationException and non-retryable DartvelExceptions propagate immediately; ConnectionException/ServerException are retried up to maxAttempts times before propagating.

The contract #

The wire format this package parses is defined by golden JSON fixtures in contract/ at the repo root (envelope.json, pagination.json, auth.json) and implemented on the Laravel side by gaitco/dartvel. Both packages' test suites read the same fixture files, so the two sides can't silently drift apart.

contract/VERSION holds the contract's major version. A bump there is a breaking wire change and ships as a major version bump of both packages together — see the root README for the policy.

1
likes
130
points
120
downloads

Documentation

API reference

Publisher

verified publishergaitco.com

Weekly Downloads

Laravel-native API client for Flutter: contract envelope parsing, typed validation errors, Paginator, Sanctum token lifecycle, device registration.

Repository (GitHub)
View/report issues

Topics

#laravel #api #http #rest

License

MIT (license)

Dependencies

dio, flutter, flutter_secure_storage

More

Packages that depend on dartvel