timsoftdz_network 2.0.0
timsoftdz_network: ^2.0.0 copied to clipboard
Enterprise-grade HTTP networking framework for Dart & Flutter. Interceptor-based, retry-safe, circuit-breaker support, Result<T> API, and zero-crash serialization. Inspired by Dio and Retrofit.
timsoftdz_network #
Enterprise-grade HTTP networking library for Dart & Flutter by TIMSoftDZ.
Inspired by OkHttp, Dio and Retrofit — built with clean architecture,
fully typed responses, composable interceptors, automatic retry, and
structured error handling via the Result<T> pattern.
✨ Features #
| Feature | Description |
|---|---|
| Result<T> Pattern | Every call returns Success or Failure — no unhandled exceptions |
| Typed responses | TimResponse<T> carries parsed data, status, headers, and elapsed time |
| Generic Parser<T> | Register once, reuse everywhere. Built-in: ListParser, WrappedParser, FunctionParser |
| Interceptor pipeline | Composable onRequest / onResponse / onError hooks |
| Auth + Auto-refresh | Bearer token injection, 401 detection, automatic token refresh with mutex |
| Retry policies | ExponentialBackoffPolicy (with jitter) and FixedDelayPolicy |
| CancelToken | Cooperative cancellation via Future.any — zero overhead when unused |
| Exception hierarchy | 12+ typed exceptions (NotFoundException, TimeoutException, etc.) |
| Pluggable transport | Swap package:http for any backend via HttpAdapter interface |
| Pure Dart | No Flutter dependency — works in Flutter, CLI, and server apps |
📦 Installation #
# pubspec.yaml
dependencies:
timsoftdz_network: ^0.1.0
dart pub get
🚀 Quick Start #
import 'package:timsoftdz_network/timsoftdz_network.dart';
// 1. Define your model
class User {
final int id;
final String name;
User({required this.id, required this.name});
factory User.fromJson(JsonMap j) =>
User(id: j['id'] as int, name: j['name'] as String);
}
// 2. Define a Parser
class UserParser implements Parser<User> {
@override
User parse(Object? data) => User.fromJson(data as JsonMap);
}
// 3. Create the client
final api = TimNetwork(
baseUrl: 'https://jsonplaceholder.typicode.com',
interceptors: [
LoggingInterceptor(),
],
);
// 4. Make requests
Future<void> fetchUser() async {
final result = await api.get<User>('/users/1', parser: UserParser());
result.fold(
onSuccess: (res) => print('${res.data.name} (${res.elapsed.inMilliseconds}ms)'),
onFailure: (err) => print('Error: ${err.message}'),
);
}
🏗️ Architecture #
YOUR APPLICATION
│
▼
TimNetwork (Facade) ← clean API: get/post/put/patch/delete
│
▼
TimHttpClient (Engine) ← builds request, runs pipeline, parses body
│
▼
InterceptorChain ← onRequest → adapter → onResponse/onError
├── HeadersInterceptor
├── AuthInterceptor
├── LoggingInterceptor
└── RetryInterceptor
│
▼
HttpAdapter (Transport) ← DefaultHttpAdapter (package:http)
│
▼
Result<TimResponse<T>> ← Success | Failure — never throws
📖 Usage Guide #
Basic Requests #
// GET
final r1 = await api.get<dynamic>('/posts');
// GET with query params
final r2 = await api.get<List<User>>(
'/users',
query : {'page': 2, 'limit': 10},
parser: ListParser(UserParser()),
);
// POST
final r3 = await api.post<User>(
'/users',
body : {'name': 'Alice', 'email': 'alice@example.com'},
parser: UserParser(),
);
// PUT
final r4 = await api.put<User>('/users/1', body: {'name': 'Bob'}, parser: UserParser());
// PATCH
final r5 = await api.patch<User>('/users/1', body: {'active': true}, parser: UserParser());
// DELETE
final r6 = await api.delete<dynamic>('/users/1');
Result API #
// Exhaustive fold
final label = result.fold(
onSuccess: (res) => res.data.name,
onFailure: (err) => 'Error: ${err.message}',
);
// Side effects
result
.onSuccess((res) => print('Got: ${res.data}'))
.onFailure((err) => logger.error(err.message));
// Transform
final upper = result.map((res) => res.data.name.toUpperCase());
// Safe access
final name = result
.map((r) => r.data.name)
.getOrDefault('Anonymous');
Interceptors #
final adapter = DefaultHttpAdapter();
final api = TimNetwork(
baseUrl : 'https://api.example.com/v2',
adapter : adapter,
interceptors: [
// 1. Static headers for every request
HeadersInterceptor.static({
'X-App-Version': '3.1.0',
'X-Platform' : Platform.operatingSystem,
}),
// 2. Dynamic headers (computed per request)
HeadersInterceptor(() => {
'X-Request-Id': Uuid().v4(),
'X-Timestamp' : DateTime.now().toIso8601String(),
}),
// 3. Auth: inject Bearer token + refresh on 401
AuthInterceptor(
storage : myTokenStorage,
refresher: MyTokenRefresher(api, myTokenStorage),
),
// 4. Colourised console logging
LoggingInterceptor(logHeaders: false, maxBodyLength: 500),
// 5. Auto-retry with exponential back-off
RetryInterceptor(
adapter: adapter,
policy : ExponentialBackoffPolicy(
maxAttempts: 3,
baseDelay : Duration(milliseconds: 300),
),
),
],
);
Token Storage + Refresh #
// 1. Implement TokenStorage (example: flutter_secure_storage)
class SecureStorage implements TokenStorage {
final _store = const FlutterSecureStorage();
@override Future<String?> getAccessToken() => _store.read(key: 'at');
@override Future<String?> getRefreshToken() => _store.read(key: 'rt');
@override Future<void> saveTokens({required String access, String? refresh}) async {
await _store.write(key: 'at', value: access);
if (refresh != null) await _store.write(key: 'rt', value: refresh);
}
@override Future<void> clear() => _store.deleteAll();
}
// 2. Implement TokenRefresher
class MyRefresher implements TokenRefresher {
final TokenStorage _storage;
final TimNetwork _http; // a separate client WITHOUT AuthInterceptor
MyRefresher(this._storage, this._http);
@override
Future<String> refresh() async {
final rt = await _storage.getRefreshToken();
if (rt == null) throw const TokenRefreshException();
final result = await _http.post<JsonMap>('/auth/refresh', body: {'refresh_token': rt});
return result.fold(
onSuccess: (res) {
final at = res.data['access_token'] as String;
_storage.saveTokens(access: at, refresh: res.data['refresh_token'] as String?);
return at;
},
onFailure: (e) => throw TokenRefreshException(message: e.message, cause: e),
);
}
}
Request Cancellation #
// Create a token
final token = CancelToken();
// Pass it to the request
final resultFuture = api.get<User>('/users/1', cancelToken: token);
// Cancel any time (e.g. on widget dispose)
@override
void dispose() {
token.cancel('Widget disposed');
super.dispose();
}
// The result will be Failure(CancelledException)
final result = await resultFuture;
result.onFailure((e) {
if (e is CancelledException) navigateToHome();
});
Custom Parsers #
// Wrap a list inside a "data" key: { "data": [...], "total": 42 }
final parser = WrappedListParser<User>(key: 'data', itemParser: UserParser());
// Single item inside a key: { "user": { ... } }
final parser2 = WrappedParser<User>(key: 'user', itemParser: UserParser());
// Inline function (no class needed)
final parser3 = FunctionParser<String>((d) => (d as JsonMap)['name'] as String);
Error Handling #
result.onFailure((e) {
switch (e) {
case NotFoundException():
showSnackBar('Resource not found.');
case UnauthorizedException():
router.go('/login');
case TooManyRequestsException(:final retryAfterSeconds):
showSnackBar('Rate limited. Retry in ${retryAfterSeconds}s.');
case TimeoutException(:final duration):
showSnackBar('Timed out after ${duration.inSeconds}s.');
case CancelledException():
break; // User navigated away — ignore.
case ConnectionException():
showSnackBar('No internet connection.');
case ServerException(:final statusCode):
logger.error('Server error $statusCode');
default:
logger.error('Network error: ${e.message}');
}
});
🧪 Running Tests #
cd timsoftdz_network
dart pub get
dart test
📁 Project Structure #
timsoftdz_network/
├── lib/
│ ├── timsoftdz_network.dart ← Public API barrel
│ └── src/
│ ├── core/
│ │ ├── result.dart ← Result<T>: Success | Failure
│ │ ├── http_method.dart ← HttpMethod enum
│ │ ├── cancel_token.dart ← CancelToken
│ │ └── parser.dart ← Parser<T> system
│ ├── client/
│ │ ├── tim_network.dart ← High-level facade
│ │ ├── tim_http_client.dart ← Core engine
│ │ ├── http_adapter.dart ← Transport interface
│ │ └── http_adapter_impl.dart← package:http implementation
│ ├── models/
│ │ ├── tim_request.dart
│ │ ├── tim_response.dart
│ │ └── network_options.dart
│ ├── interceptors/
│ │ ├── interceptor.dart
│ │ ├── interceptor_chain.dart
│ │ ├── logging_interceptor.dart
│ │ ├── headers_interceptor.dart
│ │ ├── auth_interceptor.dart
│ │ └── retry_interceptor.dart
│ ├── retry/
│ │ ├── retry_policy.dart
│ │ ├── exponential_backoff.dart
│ │ └── fixed_delay_policy.dart
│ ├── auth/
│ │ ├── token_storage.dart
│ │ ├── memory_token_storage.dart
│ │ └── token_refresher.dart
│ ├── exceptions/
│ │ ├── tim_network_exception.dart
│ │ ├── connection_exception.dart
│ │ ├── timeout_exception.dart
│ │ ├── cancelled_exception.dart
│ │ ├── http_status_exception.dart
│ │ ├── token_refresh_exception.dart
│ │ └── serialization_exception.dart
│ └── utils/
│ ├── query_builder.dart
│ └── status_code_utils.dart
├── example/
│ └── main.dart
├── test/
│ ├── client_test.dart
│ ├── interceptor_test.dart
│ └── retry_test.dart
├── pubspec.yaml
├── README.md
└── CHANGELOG.md
🗺️ Roadmap #
| Version | Feature |
|---|---|
| v0.2 | Full retry-from-interceptor support (re-proceed in AuthInterceptor) |
| v0.3 | Multipart upload + download progress callbacks |
| v0.4 | CacheInterceptor with TTL + ETag support |
| v0.5 | WebSocket module (TimWebSocket with same Result philosophy) |
| v0.6 | GraphQL query/mutation builder |
| v0.7 | Code generation — @TimApi annotation (Retrofit-style) |
| v1.0 | timsoftdz_network_flutter — ConnectivityInterceptor + SecureTokenStorage |
🤝 Part of TIMSoftDZ Ecosystem #
| Package | Description |
|---|---|
timsoftdz_core |
Shared utilities, base types |
timsoftdz_network |
This package |
timsoftdz_storage |
Local database abstraction |
timsoftdz_auth |
Full authentication flows |
timsoftdz_ui |
Flutter widget library |
📄 License #
MIT © 2026 TIMSoftDZ