smart_network 1.0.0
smart_network: ^1.0.0 copied to clipboard
A production-ready networking layer for Flutter with smart retries, advanced caching, request deduplication, offline support, automatic token refresh, batching, and seamless Dio integration.
smart_network π #
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+HiveCachebehindSmartCacheinterface - 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.