flutter_network_plus 0.1.0
flutter_network_plus: ^0.1.0 copied to clipboard
An enterprise-grade, Dio-free networking foundation for Flutter: pluggable transport adapters, JWT auto-refresh with request queueing, smart retries, circuit breaker, TTL cache with stale-while-revali [...]
flutter_network_plus #
An enterprise-grade networking foundation for Flutter. Not a Dio
wrapper: flutter_network_plus owns its transport through a small
HttpClientAdapter abstraction, which makes SSL pinning, mocking, and
hermetic testing first-class rather than bolted on.
final client = NetworkClient(
environments: EnvironmentRegistry.single(
NetworkEnvironment(name: 'prod', baseUrl: 'https://api.example.com'),
),
);
final result = await client.get<User>('users/42', decoder: User.fromJson);
switch (result) {
case NetworkSuccess(:final value):
print('Hello, ${value.name}');
case NetworkFailure(:final exception):
print('Request failed: $exception');
}
Why #
Most Flutter networking stacks are a Dio instance plus a folder of
interceptors copy-pasted between projects. That works until you need
single-flight token refresh, a circuit breaker, an offline queue that
survives restarts, and a certificate pin — at which point the interceptor
soup becomes the hardest code in the app to reason about.
flutter_network_plus provides those pieces as composable, individually
testable units behind one NetworkClient facade, with a Dart 3 sealed
result type instead of exceptions-as-control-flow.
Features #
| Area | What you get |
|---|---|
| Auth | JWT/OAuth refresh flow, single-flight refresh, pending-request queueing, proactive expiry refresh, secure token storage |
| Verbs | GET / POST / PUT / PATCH / DELETE, multipart upload with progress, resumable download manager |
| Reliability | Retry policy with exponential backoff + jitter, Retry-After, circuit breaker, per-phase timeouts |
| Caching | Memory + disk stores, TTL, five cache policies incl. stale-while-revalidate |
| Offline | Persistent request queue, connectivity-aware auto-sync |
| Security | SSL pinning (certificate + SPKI), HMAC request signing, credential-redacting cURL |
| DX | cURL generator, pluggable logging, live network inspector / API timeline, mock adapter, environment switching |
| Monitoring | Analytics events, Crashlytics hooks, OpenTelemetry-style observers |
Architecture #
Everything above the socket is pure Dart. The only component that touches
a real connection is an HttpClientAdapter; swap in MockHttpClientAdapter
and the entire pipeline (auth, retry, cache, circuit breaker, offline)
runs deterministically in unit tests.
NetworkClient ──► RequestExecutor ──► Interceptor pipeline ──► HttpClientAdapter
(facade) (retry loop, Headers → user → Signing │
breaker guard, → Auth → Cache → Offline IoHttpClientAdapter
observability) (reverse on the way back) MockHttpClientAdapter
The design follows Clean Architecture and SOLID: each concern is an
interface (TokenStorage, CacheStore, ConnectivityMonitor,
HttpClientAdapter, RequestSigner, NetworkObserver) with a default
implementation you can replace. It is DI-framework-agnostic — plain
constructor injection, no dependency on GetX, get_it, or Riverpod.
Installation #
dependencies:
flutter_network_plus: ^0.1.0
Requires Dart 3.5+ and Flutter 3.35+. Mobile and desktop are supported; web is planned for a future release.
Usage #
Configure a client #
final client = NetworkClient(
environments: EnvironmentRegistry(
environments: [
NetworkEnvironment(name: 'staging', baseUrl: 'https://staging.api.example.com'),
NetworkEnvironment(name: 'prod', baseUrl: 'https://api.example.com'),
],
initial: 'prod',
),
timeout: const TimeoutConfig(connect: Duration(seconds: 10), receive: Duration(seconds: 30)),
retryPolicy: const RetryPolicy(maxAttempts: 3),
logger: const PrintNetworkLogger(),
);
Authentication with auto-refresh #
final client = NetworkClient(
environments: environments,
auth: AuthConfig(
storage: SecureTokenStorage(),
refresher: (current, transport) async {
final result = await transport.post<Map<String, Object?>>(
'oauth/token',
body: {'grant_type': 'refresh_token', 'refresh_token': current.refreshToken},
);
final json = result.getOrThrow();
return AuthTokenPair(
accessToken: json['access_token']! as String,
refreshToken: json['refresh_token'] as String?,
expiresAt: DateTime.now().add(Duration(seconds: json['expires_in']! as int)),
);
},
onAuthFailure: () => appRouter.goToLogin(),
),
);
Ten requests hitting a 401 at once trigger exactly one refresh; the other nine wait for it and retry with the fresh token.
Caching #
final client = NetworkClient(
environments: environments,
cache: CacheConfig(
store: TieredCacheStore(
fast: MemoryCacheStore(),
slow: await DiskCacheStore.open(),
),
defaultTtl: const Duration(minutes: 5),
),
);
// Instant load from cache, refreshed in the background.
await client.get<Feed>('feed',
decoder: Feed.fromJson, cachePolicy: CachePolicy.staleWhileRevalidate);
Reliability #
final client = NetworkClient(
environments: environments,
retryPolicy: const RetryPolicy(
maxAttempts: 4,
baseDelay: Duration(milliseconds: 300),
jitter: 0.25,
),
circuitBreaker: CircuitBreaker(failureThreshold: 5, resetTimeout: Duration(seconds: 30)),
);
Offline queue #
final client = NetworkClient(
environments: environments,
offline: OfflineConfig(
store: await FileOfflineQueueStore.open(),
connectivity: ConnectivityPlusMonitor(),
),
);
// Accepted while offline and replayed FIFO when connectivity returns.
await client.post<void>('events', body: event, queueIfOffline: true);
Uploads and downloads #
await client.upload<void>('avatar', body: MultipartBody(files: [
MultipartFile.fromBytes(field: 'file', filename: 'a.png', bytes: bytes),
]), onProgress: (p) => print(p.fraction));
final task = client.download('files/report.pdf', savePath: '/tmp/report.pdf');
task.progress.listen((p) => print(p.fraction));
await task.pause();
await task.resume();
Security #
final client = NetworkClient(
environments: environments,
sslPinning: SslPinningConfig(spkiSha256Pins: {'e3b0c44298fc1c14...'}),
signer: HmacRequestSigner(secret: utf8.encode(mySecret), keyId: 'v1'),
);
Developer experience #
final inspector = NetworkInspector();
final client = NetworkClient(
environments: environments,
inspector: inspector,
observers: [AnalyticsNetworkObserver((event, params) => analytics.log(event, params))],
);
inspector.events.listen(print); // live API timeline
print(CurlGenerator.generate(request)); // copy-paste reproduction
Testing #
MockHttpClientAdapter exercises the full pipeline without a socket:
final adapter = MockHttpClientAdapter()
..onGet('/users/*', (call) => MockResponse.json({'id': 1, 'name': 'Ada'}))
..onPost('/login', (call) => MockResponse.json({'token': 'x'}, statusCode: 201));
final client = NetworkClient(environments: env, adapter: adapter);
final result = await client.get<User>('users/1', decoder: User.fromJson);
expect(result.getOrThrow().name, 'Ada');
expect(adapter.capturedRequests, hasLength(1));
Example app #
The example/ app demonstrates cached lists with pull-to-
refresh, a live inspector timeline, resumable downloads, and runtime
environment switching.
cd example && flutter run
License #
MIT — see LICENSE.