karna_network
A lightweight, robust networking toolkit for Dio. It adds client-side resilience and performance patterns out of the box: Stale-While-Revalidate (SWR) caching, automatic token refresh, retry with circuit breaking, offline mutation queuing, request batching, and error normalization.
Features
- β‘ SWR Caching: Serves cached responses instantly, revalidates in the background, dedupes in-flight calls, and supports ETag /
304 Not Modified. - πΎ Pluggable Persistence: Built-in in-memory LRU (
MemoryCacheStore) and disk persistence with Hive CE (HiveCacheStore). - π Auth & Single-Flight Refresh: Attaches tokens and refreshes once on 401 across concurrent requests before retrying.
- π‘οΈ Retry & Circuit Breaker: Exponential backoff for network errors/5xx and fast-failing circuit breaker to protect struggling endpoints.
- π¦ Request Coalescing: Batches multiple simultaneous single-item requests (
BatchCollector) into a single API call. - π΄ Offline Mutation Queue: Queues non-GET requests when offline and replays them in order when connectivity returns (
HiveMutationQueueStore). - π― Normalized Errors: Consistent
AppErrorshape across all failure types.
Installation
Add karna_network to your pubspec.yaml:
dependencies:
karna_network: ^0.1.0
Quick Start
Wire the interceptors into your Dio instance (order matters: Auth β Retry β SWR β Error normalization):
import 'package:dio/dio.dart';
import 'package:karna_network/karna_network.dart';
late final Dio dio;
// 1. Configure components
final auth = AuthInterceptor(
dioProvider: () => dio,
tokenProvider: myTokenProvider, // implements TokenProvider
);
final retry = RetryInterceptor(
dioProvider: () => dio,
circuitBreaker: CircuitBreaker(failureThreshold: 3),
maxRetries: 2,
);
final swr = SwrInterceptor(
dioProvider: () => dio,
store: MemoryCacheStore(maxEntries: 200), // or HiveCacheStore(box)
defaultOptions: const SwrOptions(
freshDuration: Duration(seconds: 30),
staleDuration: Duration(minutes: 5),
),
);
final errors = ErrorNormalizingInterceptor();
// 2. Attach to Dio
dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'))
..interceptors.addAll([auth, retry, swr, errors]);
Core Guides
1. SWR Caching & Persistence
GET requests return cached data immediately if within freshDuration. If stale (within staleDuration), cached data is served instantly while fetching fresh data in the background.
// Fast in-memory cache
final store = MemoryCacheStore(maxEntries: 200);
// Or persistent disk cache with Hive CE (no TypeAdapter needed)
final box = await Hive.openBox<Map<dynamic, dynamic>>('swr_cache');
final store = HiveCacheStore(box);
// Per-request options:
final response = await dio.get<dynamic>(
'/profile',
options: Options(
extra: {
swrExtraKey: const SwrOptions(
freshDuration: Duration(minutes: 1),
staleDuration: Duration(hours: 1),
),
},
),
);
print(response.extra['fromCache']); // true / false
Listen to background revalidation events:
swr.events.listen((event) {
switch (event) {
case SwrRevalidating():
// Show subtle "refreshing" indicator
case SwrRevalidated(:final data):
// Update UI with fresh payload
case SwrRevalidationFailed(:final error):
// Stale data remains visible; log or ignore
}
});
2. Auth & Token Refresh
Implement TokenProvider. When any request returns 401, concurrent requests share a single refresh call before retrying:
class MyTokenProvider implements TokenProvider {
@override
Future<String?> getAccessToken() async => secureStorage.read(key: 'jwt');
@override
Future<String?> refreshAccessToken() async {
// Refresh token once; return new token or null
return await authApi.refreshToken();
}
}
3. Error Normalization
Catches upstream Dio errors and wraps them in a unified AppError:
try {
await dio.get<dynamic>('/dashboard');
} on DioException catch (e) {
final appError = e.error as AppError;
print(appError.kind); // timeout, network, unauthorized, badResponse, etc.
print(appError.message);
print(appError.retryable);
}
4. Offline Mutation Queue
Queue POST/PUT/DELETE requests when offline and replay them upon reconnection:
final box = await Hive.openBox<Map<dynamic, dynamic>>('offline_queue');
final queue = OfflineMutationQueue(
dio: dio,
store: HiveMutationQueueStore(box),
isOnline: () async => checkConnectivity(),
);
try {
await queue.send('POST', '/todos', data: {'title': 'Buy groceries'});
} on MutationQueuedException {
// Saved locally; notify user "Saved, will sync when online"
}
// When internet is restored:
await queue.flush();
5. Request Coalescing (Batching)
Prevents multiple UI widgets from firing individual requests for items in the same frame:
final userBatcher = BatchCollector<Map<String, dynamic>>(
batchFetcher: (ids) async {
final res = await dio.get<dynamic>('/users', queryParameters: {'ids': ids.join(',')});
final list = (res.data as List).cast<Map<String, dynamic>>();
return {for (final u in list) u['id'] as String: u};
},
);
// Multiple callers in the same frame trigger only ONE network call:
final user = await userBatcher.load('123');
License
MIT