http_network_client 1.0.0 copy "http_network_client: ^1.0.0" to clipboard
http_network_client: ^1.0.0 copied to clipboard

A Flutter network package built on Dio with intelligent AES-256 encrypted caching, automatic retry, SSL pinning, file uploads, and type-safe Either error handling.

http_network_client #

A Flutter HTTP client package built on Dio that bundles everything a production app needs in a single, zero-codegen package:

  • Encrypted cache — AES-256-CBC with per-write random IV, SHA-256 integrity check, versioned Hive storage
  • Offline support — automatic fallback to cached data on network/5xx errors
  • Automatic retry — configurable attempts with clean counter management
  • SSL pinning — PEM string or raw bytes, production-safe (no bypass in non-dev mode)
  • File uploads — single and multiple multipart helpers
  • Type-safe errorsEither<Response, FailureState> via dartz, no exception catching in business logic
  • Request cancellation — per-URL cancel token lifecycle
  • Global auth callback — register once at startup, override per-request if needed
  • Zero codegen — no build_runner, no annotations, works out of the box

Why http_network_client? #

Most Flutter teams assemble a network layer from multiple packages. This package ships the full stack as one cohesive unit so you don't have to wire them together.

Need Typical approach This package
HTTP requests dio Built-in
Response caching dio_cache_interceptor (no encryption) Built-in, AES-256 encrypted
Offline fallback Manual interceptor Built-in
Retry on error dio_smart_retry Built-in
SSL pinning Manual HttpClient config Built-in, production-safe
Type-safe error handling Try/catch everywhere Either<Response, FailureState>
File upload Manual FormData Helper methods
Auth token expiry Manual interceptor Global + per-request callback

Unlike chopper or retrofit, there is no code generation step. Unlike dio alone, you get caching, encryption, pinning, and error handling included.


Table of Contents #

  1. Architecture
  2. Installation
  3. Setup
  4. Making Requests
  5. Handling Responses
  6. Caching
  7. Cache Warming
  8. Cache Lifecycle
  9. Cache Encryption
  10. Retry Logic
  11. File Uploads
  12. SSL Pinning
  13. Request Cancellation
  14. Dynamic Headers After Login
  15. API Reference
  16. Error Status Codes

Architecture #

┌───────────────────────────────────────────────────────┐
│                  Your Application                     │
│            NetworkService.apiRequest.getResponse()    │
└───────────────────┬───────────────────────────────────┘
                    │
        ┌───────────▼───────────┐
        │    ApiRequestImpl     │  ← handles method routing,
        │  (ApiRequest impl)    │    retry logic, cache pre-check,
        └───────────┬───────────┘    optional auth callback
                    │ Dio call
        ┌───────────▼───────────┐
        │    ApiInterceptor     │  ← Dio interceptor layer
        │  (Dio Interceptor)    │    onRequest:  serve fresh cache
        └───────┬───────┬───────┘    onResponse: save to cache
                │       │            onError:    fallback to cache
     Network    │       │ Cache hit
     request    │       │
   ┌────────────▼─┐   ┌─▼──────────────────┐
   │  Dio (HTTP)  │   │  NetworkCacheService │  ← Hive-backed store
   │  + SSL/TLS   │   │  (Hive + SHA-256    │     SHA-256 keyed entries
   └─────────┬────┘   │   key hashing)      │     AES-256-CBC encrypted
             │        └─────────┬───────────┘
   ┌─────────▼────┐             │
   │  Remote API  │   ┌─────────▼──────────────┐
   └──────────────┘   │ CacheEncryptDecryptSvc  │  ← AES-256-CBC
                      │ (per-call random IV,    │     random IV per write
                      │  SHA-256 integrity)     │     SHA-256 tamper check
                      └────────────────────────┘

Package structure #

lib/
  http_network_client.dart          <- public barrel (import this)
  export.dart                   <- backward-compatible alias
  src/
    http_network_client.dart        <- NetworkService entry point
    api_request_impl.dart
    api_manager_impl.dart
    api_interceptors.dart
    cache_config.dart
    cache_data_model.dart
    failure_state.dart
    retry_config.dart
    interface/
      api_request.dart
      api_manager.dart
    service/
      network_cache_service.dart
      cache_encrypt_decrypt_service.dart
    constant/
      enums.dart
    utils/
      network_utils.dart

Component responsibilities #

Component Role
NetworkService Singleton entry point. Initialises Dio, versioned Hive cache (ns_cache_v{version}), and the global auth callback.
ApiRequestImpl Routes HTTP methods, two-phase cache pre-check, retry counter with guaranteed cleanup, optional-callback resolution.
ApiInterceptor Dio-level interceptor. Serves fresh cache on onRequest, persists responses on onResponse, falls back to cache on network/5xx errors.
ApiManagerImpl Wraps Dio with base options, SSL/TLS pinning, and IOHttpClientAdapter. TLS bypass only active when isDevEnv: true.
NetworkCacheService Versioned Hive box. SHA-256 hashed keys prevent cache poisoning. Endpoint-indexed entries enable pagination-aware invalidation.
CacheEncryptDecryptService AES-256-CBC with a fresh random 16-byte IV per write. SHA-256 integrity check on every read.

Request flow #

  1. getResponse() is called.
  2. If isToLoadDataFromCache: true and cache is fresh → return cached data immediately (no network call).
  3. If isToRefresh: true or cache is stale/absent → make a Dio HTTP request.
  4. ApiInterceptor.onRequest checks cache once more (for interceptor-managed requests).
  5. On success (200/201) ApiInterceptor.onResponse saves the encrypted response to Hive.
  6. On network error or 5xx, ApiInterceptor.onError falls back to any available cached data.
  7. decodeHttpRequestResponse maps the Dio response to Either<Response, FailureState>.
  8. If retry is enabled and the result is a failure, getResponse recurses up to retryTimes.

Installation #

Add to your pubspec.yaml:

dependencies:
  http_network_client: ^1.0.0

Then run:

flutter pub get

Setup #

Call NetworkService.configureNetworkService once at app startup, before any HTTP call (e.g., inside main() or your DI setup).

import 'package:http_network_client/http_network_client.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await NetworkService.configureNetworkService(
    baseURL: 'https://api.example.com',
    cacheEncryptionKey: 'your-32-character-secret-key-here', // required
    onUnauthorized: () => AppRouter.goToLogin(),             // global 401/403 handler
  );

  runApp(const MyApp());
}

cacheEncryptionKey is required — a 32-character string. All cached responses are stored encrypted with AES-256-CBC. An ArgumentError is thrown at startup if the key is missing or wrong-length, so misconfiguration is caught early in every build.

Configuration parameters #

Parameter Type Required Description
baseURL String Base URL prepended to every endpoint
cacheEncryptionKey String required Exactly 32 characters — AES-256 key for cache encryption
cacheVersion int Hive box version (default 1). Increment to abandon the old cache and start fresh — no migration code needed.
onUnauthorized void Function()? Global callback fired on every 401/403. Individual requests can override this.
connectTimeout Duration Max time to establish a connection (default 15 s)
receiveTimeout Duration Max time to receive a response (default 25 s)
contentType String Default Content-Type header (default application/json)
headers Map<String, dynamic> Additional default headers for every request
pemCertificate String? PEM certificate string for SSL pinning
localCertificateBytes Uint8List? Raw certificate bytes for SSL pinning
isDevEnv bool Disables TLS certificate verification in development only

Making Requests #

All requests go through NetworkService.apiRequest.getResponse(...).

The unAuthorizedCallBack is optional on every request. When omitted, the global onUnauthorized registered in configureNetworkService is used automatically. Only pass a per-request callback when you need different behaviour for a specific endpoint.

GET #

final result = await NetworkService.apiRequest.getResponse(
  endPoint: '/users',
  apiMethods: ApiMethods.get,
  queryParams: {'page': 1, 'limit': 20},
  // unAuthorizedCallBack omitted — global callback is used
);

POST #

final result = await NetworkService.apiRequest.getResponse(
  endPoint: '/users',
  apiMethods: ApiMethods.post,
  body: {'name': 'John Doe', 'email': 'john@example.com'},
);

PUT / PATCH #

final result = await NetworkService.apiRequest.getResponse(
  endPoint: '/users/1',
  apiMethods: ApiMethods.put,   // or ApiMethods.patch
  body: {'name': 'Updated Name'},
);

DELETE #

final result = await NetworkService.apiRequest.getResponse(
  endPoint: '/users/1',
  apiMethods: ApiMethods.delete,
);

Per-request callback override #

Only needed when a specific request must handle 401 differently from the global default:

final result = await NetworkService.apiRequest.getResponse(
  endPoint: '/refresh-token',
  apiMethods: ApiMethods.post,
  unAuthorizedCallBack: () => _showSessionExpiredDialog(),
);

getResponse parameters #

Parameter Type Required Description
endPoint String required Path appended to baseURL
apiMethods ApiMethods required HTTP method
unAuthorizedCallBack void Function()? Per-request 401/403 override. Falls back to the global onUnauthorized.
queryParams Map<String, dynamic>? URL query parameters
body dynamic Request body (Map, FormData, etc.)
options Options? Override Dio options for this request
cacheConfig CacheConfig Caching behaviour (default: no cache)
retryConfig RetryConfig Retry behaviour (default: no retry)
hasInternet bool Pass false to force cache-only lookup

Handling Responses #

getResponse returns Either<Response<dynamic>, FailureState>:

  • Left(response) — success (status 200/201 or served from cache)
  • Right(failureState) — error with message and status code
final result = await NetworkService.apiRequest.getResponse(
  endPoint: '/profile',
  apiMethods: ApiMethods.get,
  unAuthorizedCallBack: () => _logout(),
);

result.fold(
  (response) {
    // Success
    final data = response.data as Map<String, dynamic>;
    print(data['name']);
  },
  (failure) {
    // statusCode is always set — switch on it for localised messages.
    switch (failure.statusCode) {
      case 401:
      case 403:
        _showDialog('Session expired. Please log in again.');
      case 404:
        _showDialog('Resource not found.');
      case 422:
        _showDialog(failure.message ?? 'Validation failed.');
      case 500:
        _showDialog('Server error. Please try again later.');
      default:
        _showDialog(failure.message ?? 'An unexpected error occurred.');
    }
  },
);

FailureState fields #

Field Type Description
statusCode int? HTTP status code — always set when an HTTP response was received. null for network-level failures (timeout, no connection).
message String? Server-provided message from the response body. null when the server did not return one — your UI should provide the display text.
data dynamic Raw response body on error

message is never hardcoded by the package. It comes directly from the server response or is null. Always handle the null case in your UI so error display stays in the presentation layer where it belongs.


Caching #

Configure caching per request using CacheConfig.

Cache and load from cache #

final result = await NetworkService.apiRequest.getResponse(
  endPoint: '/products',
  apiMethods: ApiMethods.get,
  unAuthorizedCallBack: _handleUnauth,
  cacheConfig: CacheConfig(
    isToCache: true,               // Save response to cache
    isToLoadDataFromCache: true,   // Return cache immediately if fresh
    cacheDuration: Duration(hours: 6),
  ),
);

Stale-while-revalidate pattern #

Use additionCallback to know whether a network call was made and whether the cache was stale, so you can show a refresh indicator:

cacheConfig: CacheConfig(
  isToCache: true,
  isToLoadDataFromCache: true,
  cacheDuration: Duration(days: 1),
  additionCallback: (willFetchFromNetwork, isCacheStale) {
    if (isCacheStale) showRefreshBanner();
  },
),

Force refresh #

cacheConfig: CacheConfig(
  isToCache: true,
  isToRefresh: true,   // Bypass cache, overwrite with fresh data
),

Offline-only lookup #

final result = await NetworkService.apiRequest.getResponse(
  endPoint: '/profile',
  apiMethods: ApiMethods.get,
  hasInternet: false,              // Only read from cache
  unAuthorizedCallBack: _handleUnauth,
  cacheConfig: CacheConfig(isToLoadDataFromCache: true),
);

CacheConfig reference #

Property Type Default Description
isToCache bool false Save the response to cache
isToLoadDataFromCache bool false Serve from cache when fresh
cacheDuration Duration 3 days Cache TTL
isToRefresh bool false Force network call and overwrite cache
additionCallback Function(bool, bool)? null (willFetchNetwork, isCacheStale)

Retry Logic #

final result = await NetworkService.apiRequest.getResponse(
  endPoint: '/orders',
  apiMethods: ApiMethods.get,
  unAuthorizedCallBack: _handleUnauth,
  retryConfig: RetryConfig(
    isToRetryOnError: true,
    retryTimes: 3,
  ),
);
Property Type Default Description
isToRetryOnError bool false Enable automatic retry
retryTimes int 3 Maximum retry attempts

File Uploads #

Single file #

import 'dart:io';
import 'package:dio/dio.dart';

final file = File('/path/to/image.jpg');

final result = await NetworkService.apiRequest.uploadAnySingleFile(
  endPoint: '/upload/avatar',
  file: file,
  formData: FormData.fromMap({'userId': '123'}),
  uploadType: UploadType.jpg,
  unAuthorizedCallBack: _handleUnauth,
);

Multiple files #

final files = [File('/path/a.jpg'), File('/path/b.jpg')];

final result = await NetworkService.apiRequest.uploadAnyMultipleFile(
  endPoint: '/upload/photos',
  files: files,
  formData: FormData.fromMap({'albumId': '42'}),
  uploadType: UploadType.jpg,
  options: Options(headers: {'X-Custom': 'value'}),
  unAuthorizedCallBack: _handleUnauth,
);

UploadType values #

Value MIME type
UploadType.jpg image/jpg
UploadType.png image/png
UploadType.pdf pdf
UploadType.audio audio

SSL Pinning #

PEM certificate (string) #

const pemCert = '''
-----BEGIN CERTIFICATE-----
MIIBIjANBgkqhkiG9w0BAQEFAAOC...
-----END CERTIFICATE-----
''';

await NetworkService.configureNetworkService(
  baseURL: 'https://api.example.com',
  pemCertificate: pemCert,
);

Certificate bytes #

final certBytes = (await rootBundle.load('assets/cert.pem')).buffer.asUint8List();

await NetworkService.configureNetworkService(
  baseURL: 'https://api.example.com',
  localCertificateBytes: certBytes,
);

Note: Set isDevEnv: true to bypass SSL verification during development.


Request Cancellation #

Cancel in-flight requests by their endpoint URL:

// Start a cancellable request
NetworkService.apiRequest.getResponse(
  endPoint: '/search',
  apiMethods: ApiMethods.get,
  queryParams: {'q': query},
  unAuthorizedCallBack: _handleUnauth,
);

// Cancel it (e.g., when the user types a new query)
NetworkService.apiRequest.cancelRequest('/search');

Dynamic Headers After Login #

At app start there is no token yet, so configureNetworkService is called without one. After a successful login, inject the token into every subsequent request using setAuthToken — no re-initialisation required.

// App startup — no token yet
await NetworkService.configureNetworkService(
  baseURL: 'https://api.example.com',
  cacheEncryptionKey: encryptionKey,
  onUnauthorized: () => AppRouter.goToLogin(),
);

// After login succeeds
final token = await authRepo.login(username, password);
NetworkService.setAuthToken(token);   // all requests now carry Authorization: Bearer <token>

// On logout
NetworkService.clearAuthToken();      // strips the header
await NetworkService.clearCache();    // wipe user-specific cached data
AppRouter.goToLogin();

Update arbitrary headers at runtime #

Use updateHeaders when you need to change any header (e.g. locale, tenant ID) without touching the token:

NetworkService.updateHeaders({
  'Accept-Language': selectedLocale,
  'X-Tenant-Id': tenantId,
});

Existing keys are overwritten; all other headers are preserved.

Per-request header override #

Only needed when a single request requires a different header from the global default:

final result = await NetworkService.apiRequest.getResponse(
  endPoint: '/download',
  apiMethods: ApiMethods.get,
  options: Options(headers: {'Accept': 'application/octet-stream'}),
);

API Reference #

NetworkService #

Member Type Description
configureNetworkService(...) static Future<void> Initialize before first request
apiRequest static ApiRequest All HTTP request operations
apiManager static ApiManager Direct Dio access

ApiRequest #

Method Description
getResponse(...) Execute GET/POST/PUT/PATCH/DELETE
uploadAnySingleFile(...) Upload a single file as multipart
uploadAnyMultipleFile(...) Upload multiple files as multipart
cancelRequest(url) Cancel in-flight request by URL key
decodeHttpRequestResponse(...) Low-level response decoder

Error Status Codes #

HTTP Code Behaviour
200 / 201 Left(response) — success
101 (internal) Cache hit — normalised to 200 before returning
400 / 422 Right(FailureState) with validation details
401 / 403 Right(FailureState) + unAuthorizedCallBack fired
404 / 406 Right(FailureState) with message and raw data
429 / 500 Right(FailureState) with server error message
Connection error Falls back to cache if available, otherwise Right(FailureState)

Cache Warming #

Pre-populate the cache at app startup so data is instantly available on first render — no loading spinner needed.

await NetworkService.configureNetworkService(
  baseURL: 'https://api.example.com',
  cacheEncryptionKey: key,
  onUnauthorized: () => AppRouter.goToLogin(),
);

// Fire all requests concurrently; each failure is independent.
await NetworkService.warmCache([
  WarmCacheRequest(endPoint: '/profile', method: ApiMethods.get),
  WarmCacheRequest(endPoint: '/dashboard', method: ApiMethods.get),
  WarmCacheRequest(
    endPoint: '/products',
    method: ApiMethods.get,
    queryParams: {'limit': 20},
    cacheDuration: Duration(hours: 6),
  ),
]);

Then in your screen, read from cache first with no UI delay:

final result = await NetworkService.apiRequest.getResponse(
  endPoint: '/profile',
  apiMethods: ApiMethods.get,
  cacheConfig: CacheConfig(isToLoadDataFromCache: true, isToCache: true),
);

WarmCacheRequest fields #

Field Type Default Description
endPoint String required Endpoint path
method ApiMethods required HTTP method
queryParams Map? null URL query parameters
body dynamic null Request body for POST/PUT
cacheDuration Duration 3 days TTL for warmed data

Cache Lifecycle #

Clear on logout #

Call NetworkService.clearCache() when a user logs out so no sensitive data remains on disk:

Future<void> logout() async {
  await NetworkService.clearCache();
  AppRouter.goToLogin();
}

Schema migration (cacheVersion) #

When a new app release changes the response structure of cached endpoints, increment cacheVersion. The old Hive box is abandoned and a fresh one starts — no migration code required:

// v1 → v2: old ns_cache_v1 box is left behind, ns_cache_v2 starts clean.
await NetworkService.configureNetworkService(
  baseURL: 'https://api.example.com',
  cacheEncryptionKey: key,
  cacheVersion: 2,
);

Inspect the active box name (debugging) #

print(NetworkCacheService.currentBoxName); // e.g. ns_cache_v2

Cache Encryption #

All cached responses are stored using AES-256-CBC with SHA-256 integrity verification:

  • Key: 32 bytes (required — ArgumentError thrown at startup if absent or wrong length)
  • IV: 16 random bytes generated fresh per write (never reused across entries)
  • Storage format per entry: ivBase64::encryptedBase64::sha256Hash
  • Hive keys are SHA-256 hashed — user-controlled request data cannot be used as raw storage keys

Encryption key rotation #

When rotating the cacheEncryptionKey (e.g. for a security policy change), also increment cacheVersion. The old encrypted data is unreachable under the new key and the new box starts fresh:

await NetworkService.configureNetworkService(
  baseURL: 'https://api.example.com',
  cacheEncryptionKey: newKey,  // new 32-char key
  cacheVersion: 3,             // bump version to abandon old encrypted entries
);
1
likes
150
points
10
downloads

Documentation

API reference

Publisher

verified publisherjagaranmaharjan.com

Weekly Downloads

A Flutter network package built on Dio with intelligent AES-256 encrypted caching, automatic retry, SSL pinning, file uploads, and type-safe Either error handling.

Homepage

Topics

#networking #http #caching #dio #flutter

License

MIT (license)

Dependencies

crypto, dartz, dio, encrypt, flutter, hive, http_parser, path, path_provider

More

Packages that depend on http_network_client