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).

pub package License: MIT platforms

Live demo Online Offline
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. Plain http:// is blocked by default (cleartext policy / ATS) unless you add an exception.
  • macOS: sandboxed apps need com.apple.security.network.client set to true in the entitlements, otherwise every check looks like "offline".
  • Web: checks run from the browser, so endpoints must allow CORS. Point internetCheckUrls and server health at your own domain.
  • All platforms: nothing runs while the app is backgrounded or terminated.

Limitations

  • NetworkQuality is 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.

Also by the author: crossbuild_custom_widgets, a theme-aware Flutter UI kit.