timsoftdz_network

pub.dev License: MIT Dart SDK

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

Libraries