smart_network πŸš€

pub.flutter-io.cn License: MIT Flutter

A smart, production-ready networking layer for Flutter.

Built on top of Dio, smart_network gives you a robust HTTP client with zero boilerplate for the features every production app needs:

Feature Description
πŸ” Retry Exponential / Linear / Constant / Decorrelated-Jitter backoff
πŸ—„οΈ Cache 5 strategies Β· Two-layer Memory + Hive Β· Stale-While-Revalidate
πŸ”€ Deduplication Collapses identical concurrent GETs into one HTTP call
πŸ“΅ Offline Queue Disk-persistent queue Β· Auto-replays on reconnect
πŸ” Auto Token Refresh Thread-safe JWT refresh Β· Race-condition proof
πŸ“¦ Request Batching Groups multiple requests into a single HTTP call
πŸͺ΅ Logging Structured pretty-print via logger package
πŸ”¬ Diagnostics Live cache/queue snapshots + manual maintenance API

Installation

dependencies:
  smart_network: ^1.0.0

Quick Start

import 'package:smart_network/smart_network.dart';

// ── 1. Initialise once in main() ──────────────────────────────────────────────
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await SmartNetworkClient().initialize(
    SmartConfig(
      baseUrl: 'https://api.example.com',
      retryPolicy: RetryPolicy(maxAttempts: 3),
      cachePolicy: CachePolicy(
        maxAge: Duration(minutes: 10),
        strategy: CacheStrategy.staleWhileRevalidate,
      ),
      tokenRefresher: MyTokenRefresher(), // optional
    ),
  );

  runApp(const MyApp());
}

// ── 2. Use anywhere ───────────────────────────────────────────────────────────
final client = SmartNetworkClient();

final res = await client.get<User>(
  '/users/42',
  fromJson: User.fromJson,
);

print(res.data.name);       // User object
print(res.fromCache);       // true if served from cache
print(res.isStale);         // true if background revalidation is pending

Features In Depth

πŸ” Retry

RetryPolicy(
  maxAttempts: 3,
  retryOnStatusCodes: {408, 500, 502, 503, 504},
  backoffStrategy: ExponentialBackoff(
    base: Duration(seconds: 1),
    maxDelay: Duration(seconds: 30),
    jitterFactor: 1.0,   // full jitter β€” prevents thundering herd
  ),
)

Four built-in back-off strategies:

Strategy Formula Use case
ConstantBackoff delay Brief uniform blips
LinearBackoff step Γ— (n+1) Graceful degradation
ExponentialBackoff base Γ— 2ⁿ + jitter Default β€” recommended
DecorrelatedJitterBackoff AWS-style spread High-concurrency clients

Disable retries per-request:

await client.post('/pay', data: body, allowRetry: false);

πŸ—„οΈ Cache

CachePolicy(
  enabled: true,
  maxAge: Duration(minutes: 10),   // fresh window
  staleAge: Duration(minutes: 30), // stale-while-revalidate window
  strategy: CacheStrategy.staleWhileRevalidate,
)
Strategy Description
cacheFirst Cache β†’ Network on miss
networkFirst Network β†’ Cache fallback on error
networkOnly Always network; writes to cache
cacheOnly Cache only; throws on miss
staleWhileRevalidate Instant cache + background refresh (default)

Cache is two-layer:

  • L1 β€” MemoryCache (LRU, sub-millisecond)
  • L2 β€” HiveCache (disk, survives app restarts)

Disable cache per-request:

await client.get('/live-price', useCache: false);

πŸ”€ Request Deduplication

When the same GET fires twice before the first returns, SmartNetwork makes one HTTP call and delivers the result to both callers:

// Both receive the exact same response β€” only 1 HTTP call
final [res1, res2] = await Future.wait([
  client.get('/users/1'),
  client.get('/users/1'),
]);

Disable per-request:

await client.get('/users/1', headers: {'deduplicate': 'false'});
// or via extra:
// Options(extra: {'deduplicate': false})

πŸ“΅ Offline Queue

POST/PUT/DELETE requests are persisted to disk when the device is offline and replayed automatically on reconnect:

// Survives app restarts β€” stored in Hive
await client.post(
  '/messages',
  data: {'text': 'Hello!'},
  queueIfOffline: true,
);

// Response when offline:
// SmartResponse(statusCode: 202, isQueued: true)

Manual control:

final pending = await client.offlineQueueSize;   // count
await client.processOfflineQueue();              // force replay
await client.clearOfflineQueue();               // discard all

πŸ” Auto Token Refresh

Implement TokenRefresher with your auth logic:

class MyTokenRefresher extends TokenRefresher {
  @override
  Future<TokenPair> refresh(String refreshToken) async {
    final res = await dio.post('/auth/refresh',
        data: {'refresh_token': refreshToken});
    return TokenPair(
      accessToken: res.data['access_token'],
      refreshToken: res.data['refresh_token'],
    );
  }
}

Then pass it to SmartConfig:

SmartConfig(
  baseUrl: '...',
  tokenRefresher: MyTokenRefresher(),
)

After login, store tokens:

await client.setTokens(
  accessToken: loginResponse.accessToken,
  refreshToken: loginResponse.refreshToken,
);

On logout:

await client.clearTokens();

Token storage is thread-safe β€” even if 10 requests expire simultaneously, only one refresh call is made; all 10 requests receive the new token.

Opt out of auth per-request (for login/register endpoints):

await client.post(
  '/auth/login',
  data: credentials,
  headers: {'skipAuth': 'true'},
);
// or: Options(extra: {'skipAuth': true})

πŸ“¦ Request Batching

SmartConfig(
  batchConfig: BatchConfig(
    batchEndpoint: '/batch',
    maxBatchSize: 10,
    windowDuration: Duration(milliseconds: 50),
  ),
)

Use the batch method for individual requests:

final [users, posts] = await Future.wait([
  client.batch<User>(path: '/users/1', method: 'GET', fromJson: User.fromJson),
  client.batch<List>(path: '/users/1/posts', method: 'GET'),
]);

Or tag any request with extra['batch'] = true to route through BatchInterceptor automatically.


πŸ”¬ Diagnostics

final info = await client.diagnostics();
// {
//   'baseUrl': 'https://api.example.com',
//   'isOnline': true,
//   'cacheEntries': { 'memory': 12, 'hive': 47 },
//   'offlineQueueSize': 0,
//   'batchPending': 0,
// }

Cache maintenance:

await client.clearCache();              // wipe everything
await client.evictExpiredCache();       // remove only expired entries

Error Handling

All errors are wrapped in SmartException:

try {
  final res = await client.get('/users/1');
} on SmartException catch (e) {
  switch (e.type) {
    case SmartExceptionType.noInternet:
      showOfflineBanner();
    case SmartExceptionType.unauthorized:
      navigateToLogin();
    case SmartExceptionType.timeout:
      showRetrySnackbar();
    case SmartExceptionType.serverError:
      showGenericError(e.message);
    default:
      debugPrint(e.toString());
  }
}

SmartExceptionType values:

Type Trigger
noInternet No connectivity / SocketException
timeout Connect / send / receive timeout
serverError 5xx responses
unauthorized 401
forbidden 403
notFound 404
tooManyRequests 429
cancelled Request cancelled
parseError JSON deserialisation failure
authRefreshFailed Token refresh failed
cacheNotFound cacheOnly strategy + no cache
unknown Everything else

Architecture

SmartNetworkClient (singleton)
        β”‚
        β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚           Interceptor Chain (onRequest)          β”‚
   β”‚                                                  β”‚
   β”‚  1. LogInterceptor    ← records everything       β”‚
   β”‚  2. AuthInterceptor   ← Bearer token + 401 retry β”‚
   β”‚  3. CacheInterceptor  ← 5-strategy L1/L2 cache   β”‚
   β”‚  4. DedupInterceptor  ← collapses duplicate GETs β”‚
   β”‚  5. BatchInterceptor  ← routes to BatchProcessor β”‚
   β”‚  6. OfflineInterceptor← queues/rejects offline   β”‚
   β”‚  7. RetryInterceptor  ← backoff retry on errors  β”‚
   β”‚  8. Custom extras     ← user-supplied             β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚
        β–Ό
   Dio HTTP client β†’ API Server
        β”‚
        β–Ό
   SmartResponse<T>
   β”œβ”€β”€ data: T
   β”œβ”€β”€ statusCode: int
   β”œβ”€β”€ fromCache: bool
   β”œβ”€β”€ isStale: bool
   └── isQueued: bool

Design patterns used:

  • Singleton β€” one client per app
  • Interceptor Chain β€” like OkHttp / Express middleware
  • Strategy Pattern β€” pluggable BackoffStrategy, CacheStrategy, TokenStorage
  • Repository Pattern β€” MemoryCache + HiveCache behind SmartCache interface
  • Completer Pattern β€” race-condition-safe token refresh
  • Decorator Pattern β€” SmartResponse<T> wraps Dio's raw response

Dependencies

Package Version Purpose
dio ^5.4.0 HTTP client
hive ^2.2.3 Persistent cache & offline queue
hive_flutter ^1.1.0 Hive Flutter integration
connectivity_plus ^6.0.0 Network state monitoring
crypto ^3.0.3 SHA-256 cache key hashing
equatable ^2.0.5 Value equality
synchronized ^3.1.0 Mutex for token refresh
logger ^2.0.2+1 Structured logging

Comparison

Feature smart_network dio http retrofit
Auto Retry βœ… 4 strategies ❌ ❌ ❌
Cache βœ… 5 strategies ❌ ❌ ❌
Deduplication βœ… ❌ ❌ ❌
Offline Queue βœ… persistent ❌ ❌ ❌
Token Refresh βœ… race-safe ❌ ❌ ❌
Batching βœ… ❌ ❌ ❌
Type-safe Response βœ… partial ❌ βœ…
Built-in Logging βœ… βœ… ❌ ❌

License

MIT β€” see LICENSE.

Libraries

smart_network
smart_network