Skip to content

Repository files navigation

valync

A Flutter package for making typed HTTP requests with automatic JSON deserialization, structured success/failure responses, and built-in error handling.

Features

  • Typed HTTP client — responses are deserialized directly into your Dart models
  • Result<T, ApiError> return type (via option_result) — no uncaught exceptions
  • Structured API responses with success/failed status discrimination
  • Multipart file upload support
  • Built-in generic response types: EmptyResponse, ListResponse, JsonObjectResponse
  • Auto-generated factory registry via the @AutoFactory annotation and build_runner
  • Optional createClient for shared headers and automatic retry on error

Getting started

Add to your pubspec.yaml:

dependencies:
valync: ^0.1.2dev_dependencies:
build_runner: any

Setup

1. Annotate your models

Implement JsonType<T> and annotate with @AutoFactory:

import'package:valync/valync.dart';
@AutoFactory()
classUserimplementsJsonType<User> {
finalString id;
finalString name;
User({requiredthis.id, requiredthis.name});
@overrideUserfromJson(dynamic json) =>User(
id: json['id'] asString,
name: json['name'] asString,
);
}

2. Run the generator

flutter pub run build_runner build

This generates lib/type_factories.dart containing a registerAllFactories() function.

3. Register factories at startup

Call registerAllFactories() before making any requests — typically in main():

import'package:your_app/type_factories.dart';
voidmain() {
registerAllFactories();
runApp(constMyApp());
}

Built-in types (EmptyResponse, ListResponse, JsonObjectResponse) are pre-registered automatically — no annotation or setup needed.

Usage

One-off request

final result =awaitvalync<User>('https://api.example.com/users/1');
switch (result) {
caseOk(:final value):print(value.name);
caseErr(:final error):print('${error.name}: ${error.message}');
}

Client with shared config

final client =createClient(
headers: {'X-App-Version':'1.0.0'},
config:ValyncClientConfig(
headers: () => {'Authorization':'Bearer $token'},
onError: (error) async {
if (error.code.unwrapOr('') =='401') {
awaitrefreshToken();
returntrue; // true = retry the request once
}
returnfalse;
},
),
);
// GETfinal result =awaitclient<User>('https://api.example.com/me');
// POST with JSON bodyfinal result =awaitclient<User>(
'https://api.example.com/users',
method:HttpMethod.post,
body: {'name':'Alice'},
);
// POST with multipart filesfinal result =awaitclient<User>(
'https://api.example.com/avatar',
method:HttpMethod.post,
files: [await http.MultipartFile.fromPath('avatar', '/path/to/file.jpg')],
);

List responses

Use ListResponse when the API returns an array as data:

final result =awaitclient<ListResponse>('https://api.example.com/users');
switch (result) {
caseOk(:final value):final users = value.apply((e) =>User().fromJson(e));
print(users.map((u) => u.name).join(', '));
caseErr(:final error):print(error);
}

No-body responses

Use EmptyResponse for endpoints that return 204 No Content or a bare success:

final result =awaitclient<EmptyResponse>(
'https://api.example.com/logout',
method:HttpMethod.post,
);
switch (result) {
caseOk():print('logged out');
caseErr(:final error):print(error);
}

Raw JSON object

Use JsonObjectResponse when you need to map data yourself:

final result =awaitclient<JsonObjectResponse>('https://api.example.com/config');
switch (result) {
caseOk(:final value):final config = value.apply((e) =>AppConfig.fromJson(e));
print(config.featureFlags);
caseErr(:final error):print(error);
}

Supported HTTP methods

HttpMethod.get, HttpMethod.post, HttpMethod.put, HttpMethod.patch, HttpMethod.delete

API

valync<T>(url, {...})

Stateless one-off request.

Future<Result<T, ApiError>> valync<T>(
String url, {
HttpMethod method =HttpMethod.get,
Map<String, dynamic>? body,
Map<String, String>? headers,
List<http.MultipartFile>? files,
})

createClient({headers, config})

Returns a ValyncClient with shared configuration.

ValyncClientcreateClient({
Map<String, String>? headers,
ValyncClientConfig config =constValyncClientConfig(),
})

ValyncClientConfig

FieldTypeDescription
headersMap<String, String> Function()?Dynamic headers merged into every request (e.g. auth tokens)
onErrorFuture<bool> Function(ApiError)?Called on error — return true to retry the request once

ApiError

FieldTypeDescription
nameStringMachine-readable error identifier
messageStringHuman-readable description
codeOption<String>Optional error code

Built-in error names: ServerUnreacheable, InternalServerError, UnknownError.

Built-in response types

TypeUse when data is
EmptyResponseabsent or irrelevant (e.g. 204)
ListResponsea JSON array — use .apply((e) => T.fromJson(e)) to map
JsonObjectResponsean arbitrary value — use .apply((e) => T.fromJson(e)) to map

registerFactory(Type, JsonType)

Register a factory manually without code generation:

registerFactory(User, User(id:'', name:''));

API response contract

The package expects responses in this shape:

{ "status": "success", "data": { ... } }
{ "status": "failed", "error": { "name": "...", "message": "...", "code": "..." } }

204 No Content responses are treated as success with no data.

Example

A runnable Flutter example app is available in examples/basic. It demonstrates one-off requests, list responses, empty responses, error handling, and createClient — all using a mock HTTP client, no server required.

Additional information

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages