flutter_network_guard
A network reliability toolkit for Flutter: connectivity detection, real
internet reachability, server health checks, network quality, retry with
backoff, request deduplication and cancellation, an offline queue, a cache,
and network-aware widgets. Every feature is opt-in and works with any state
management (Provider, Riverpod, BLoC, GetX, Cubit, ValueNotifier,
setState).
| Live demo | Online | Offline |
|---|---|---|
![]() |
![]() |
![]() |
Why
A Wi-Fi icon does not mean the internet works, and a working internet does not
mean your API is up. This package keeps those signals separate
(hasLocalConnection, hasInternet, serverReachable) and adds the tools
apps usually reinvent: retries that back off, protection against double-taps,
and writes that survive going offline.
Features
| Feature | What it does |
|---|---|
| Connectivity + real reachability | Wi-Fi/mobile/ethernet type, plus an actual request to confirm internet works. |
| Server health | Monitors your own API(s) separately from general internet. |
| Network quality | excellent / good / fair / poor, estimated from latency. |
execute() |
One call combining offline check, retry, dedup, cancel, timeout and cache; returns a typed result and never throws. |
| Offline queue | Persisted, prioritised queue of writes, replayed when internet returns. |
| Cache | In-memory TTL cache with 5 policies incl. stale-while-revalidate. |
| Widgets | Status banner, offline screen swap, guarded button, debug panel, Listenable wrapper. |
| Adapters | Optional Dio interceptor and package:http client. |
Installation
dependencies:
flutter_network_guard: ^0.3.0
flutter pub get
Requires Dart ^3.8.0 / Flutter >=3.32.0. The package depends on
connectivity_plus, http, shared_preferences and dio (Dio is only used if
you import its adapter).
Quick start
import 'package:flutter/material.dart';
import 'package:flutter_network_guard/flutter_network_guard.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await NetworkGuard.initialize();
runApp(const MyApp());
}
// Anywhere afterwards:
if (NetworkGuard.instance.isOnline) { /* safe to call the API */ }
NetworkGuard.instance.networkStream.listen((info) {
print('${info.status} via ${info.type}, quality ${info.quality}');
});
With no configuration it only tracks connectivity and checks real internet
reachability every 30 seconds. networkStream emits when the status, type,
reachability, server state or quality changes (not on every latency change).
Usage
Protected calls with execute()
final result = await NetworkGuard.instance.execute<List<Order>>(
key: 'get_orders', // enables dedup / cache
retryPolicy: const RetryPolicy(maxAttempts: 3),
timeout: const Duration(seconds: 10),
request: () => api.getOrders(),
);
switch (result) {
case NetworkSuccess(:final value, :final fromCache):
show(value); // fromCache: served from CacheManager
case NetworkOffline():
showOffline(); // request was never called
case NetworkFailure(:final error):
showError(error); // failed after all retries
default:
break; // NetworkTimeout, NetworkCancelled, NetworkQueued
}
| Parameter | Meaning |
|---|---|
request |
The async call (required). |
key |
Deduplication key; required when cachePolicy is used (else ArgumentError). |
retryPolicy |
Defaults to RetryPolicy.none (single attempt). |
deduplicationPolicy |
reuseInFlight (default), ignoreNew, replacePrevious. |
cancelToken |
Cancel from elsewhere. |
timeout |
Per-attempt timeout. |
cachePolicy, cacheTtl |
Use the configured CacheManager. |
When offline (and no cache applies) execute() returns NetworkOffline
immediately. Avoid auto-retrying non-idempotent writes (RetryPolicy.none)
unless your backend supports idempotency keys.
Retry
const RetryPolicy(
maxAttempts: 3, // includes the first attempt
initialDelay: Duration(seconds: 1),
maxDelay: Duration(seconds: 10),
strategy: RetryStrategy.exponential, // fixed | linear | exponential
jitter: true,
);
Use retryIf to decide per error, and onRetry to show "Retrying (2/3)…".
A cancelled request is never retried. For Dio, dioRetryPredicate() retries
only timeouts, connection errors and retryableStatusCodes (408, 425, 429,
500, 502, 503, 504 by default):
import 'package:flutter_network_guard/adapters/dio/network_guard_interceptor.dart';
RetryPolicy(maxAttempts: 3, retryIf: dioRetryPredicate())
Deduplication and cancellation
final token = CancelToken();
NetworkGuard.instance.execute(cancelToken: token, request: () => api.report());
token.cancel(); // stops waiting; does not abort the socket itself
RequestDeduplicator can also be used standalone. RequestManager.runLatest
drops stale responses (search-as-you-type).
Server health
await NetworkGuard.initialize(
config: NetworkGuardConfig(
serverHealthChecks: [
ServerHealthConfig(id: 'api', url: 'https://api.example.com/health'),
],
onServerRestored: (info) => print('API is back'),
),
);
NetworkGuard.instance.serverHealth['api']?.isReachable;
Use an endpoint on your own domain, especially for web (CORS).
Offline queue
final queue = OfflineQueue(
storage: const SharedPreferencesQueueStorage(), // survives restarts
maxQueueSize: 200,
retryDelay: (retryCount) => Duration(seconds: 5 * retryCount), // optional
executor: (task) async {
final res = await http.post(
Uri.parse('https://api.example.com${task.endpoint}'),
headers: task.headers,
body: jsonEncode(task.body),
);
return res.statusCode == 200; // false or a throw => retried later
},
);
await NetworkGuard.initialize(
config: NetworkGuardConfig(offlineQueue: queue), // auto-processes on reconnect
);
await NetworkGuard.instance.enqueue(
QueuedRequest(id: 'profile_1', endpoint: '/profile', body: {'name': 'Jane'}),
);
Also: queue.pending, failed, completed, retry(), remove(id),
clearCompleted(), clear(), and queue.changes / queue.stats for a live
"3 items waiting" badge. Queueing is best-effort, not a delivery guarantee;
show queue.failed to the user. Do not queue tokens or passwords with the
default storage (it is plain JSON); implement QueueStorage with encrypted
storage for sensitive data.
Cache
final cache = CacheManager(maxEntries: 100);
await NetworkGuard.initialize(config: NetworkGuardConfig(cacheManager: cache));
final result = await NetworkGuard.instance.execute<Profile>(
key: 'profile',
cachePolicy: CachePolicy.staleWhileRevalidate,
cacheTtl: const Duration(minutes: 10),
request: () => api.getProfile(),
);
Policies: networkOnly, cacheFirst, networkFirst (falls back to cache),
cacheOnly, staleWhileRevalidate. NetworkSuccess.fromCache tells you which
source was used.
Widgets
const NetworkStatusBanner(); // offline / "back online" banner
NetworkAware(child: Home(), offlineBuilder: (c, info) => OfflinePage());
NetworkGuardBuilder(builder: (c, info) => Text(info.status.name));
NetworkGuardButton(onPressed: submit, child: Text('Submit')); // no double-taps, disabled offline
const NetworkDebugPanel(); // debug builds only
NetworkGuardListenable(); // ChangeNotifier for Provider/ListenableBuilder
Dio and package:http
final dio = Dio()..interceptors.add(NetworkGuardInterceptor());
final client = NetworkGuardHttpClient(http.Client());
Both fail fast while offline. Import them from
package:flutter_network_guard/adapters/dio/network_guard_interceptor.dart
and .../adapters/http/network_guard_http_client.dart.
Logging and metrics
Logging is off by default: NetworkGuardConfig(enableLogging: true, logLevel: LogLevel.info).
NetworkMetrics is an in-memory counter you feed yourself (recordRequest,
recordCache, recordQueued) and read via snapshot.
Configuration
NetworkGuardConfig (all optional):
| Field | Default |
|---|---|
enableMonitoring |
true |
checkInterval |
30 s |
checkDebounceDuration |
300 ms |
checkOnResume |
true |
internetCheckUrls |
Cloudflare + Google DNS (HTTPS) |
internetCheckTimeout |
5 s |
qualityThresholds |
100 / 300 / 800 ms |
serverHealthChecks |
[] |
eventHistorySize |
50 |
offlineQueue, autoProcessQueueOnRestore |
null, true |
cacheManager |
null |
enableLogging, logLevel |
false, LogLevel.error |
onOnline, onOffline, onInternetRestored, onServerRestored, onNetworkChanged |
null |
Platform notes
- Android / iOS: use
https://endpoints. Plainhttp://is blocked by default (cleartext policy / ATS) unless you add an exception. - macOS: sandboxed apps need
com.apple.security.network.clientset totruein the entitlements, otherwise every check looks like "offline". - Web: checks run from the browser, so endpoints must allow CORS. Point
internetCheckUrlsand server health at your own domain. - All platforms: nothing runs while the app is backgrounded or terminated.
Limitations
NetworkQualityis a latency estimate, not a bandwidth measurement.- Cancellation stops waiting for a call; it does not abort the socket.
- Queued requests are not guaranteed to be delivered.
- Checks do not run in the background.
Contributing
Issues and pull requests are welcome at
github.com/jamzulqarnain/flutter_network_guard.
Please run dart format ., flutter analyze and flutter test first.
License
MIT, see LICENSE.
About the author
Zulqarnain Hafeez is a Flutter developer from Rahim Yar Khan, Pakistan, building and shipping cross-platform apps for Android, iOS, Web and Windows. His work includes apps live on Google Play and the App Store, and offline-first Windows POS and inventory systems used by real businesses, with a focus on REST APIs, Firebase and maintainable architecture.
Available for freelance Flutter projects.
- GitHub: github.com/jamzulqarnain
- LinkedIn: Zulqarnain Hafeez
- Portfolio: zulqarnainportfolio-81029.web.app
- Email: zulqarnain.jam25@gmail.com
Also by the author: crossbuild_custom_widgets, a theme-aware Flutter UI kit.


