flutter_network_state

A network-aware state management and request orchestration engine for Flutter.
Not just another connectivity checker.

pub version Dart 3 Flutter License: MIT


What is this?

flutter_network_state is a decision engine that sits between your app and the network. It unifies:

  • 🌐 Online/Offline detection β€” stream-based, always reactive
  • πŸ” Real internet check β€” DNS lookup verification, not just interface detection
  • 🎯 Smart request strategies β€” NetworkFirst, CacheFirst, CacheOnly, NetworkOnly
  • πŸ“¦ In-memory caching β€” with TTL and pluggable storage
  • πŸ“‹ Offline request queue β€” failed requests are queued and replayed automatically
  • πŸ”„ Automatic sync β€” queue drains when connectivity is restored
  • πŸ”Œ Dio interceptor β€” drop-in auto-retry and offline queueing
  • 🧱 Reactive widgets β€” NetworkBuilder and ConnectivityBuilder for instant UI binding
  • 🧩 State management ready β€” built-in support for Provider, Cubit, Bloc, and Riverpod
  • πŸ” Retry policies β€” exponential, linear, constant backoff with jitter

Think of it as Dio + Bloc + Offline Sync + Connectivity in one clean abstraction.


Getting Started

Installation

dependencies:
  flutter_network_state: ^1.0.0
flutter pub get

Minimal Example

import 'package:flutter_network_state/flutter_network_state.dart';

final manager = NetworkManager();

// Listen to state changes
manager.stateStream.listen((state) {
  switch (state) {
    case Idle()    => print('Ready');
    case Loading() => print('Loading...');
    case Offline() => print('Offline (${state.queuedRequests} queued)');
    case Syncing() => print('Syncing ${(state.progress * 100).toStringAsFixed(0)}%');
    case Success() => print('Data: ${state.data}');
    case Error()   => print('Error: ${state.message}');
  }
});

// Make a request with strategy
final user = await manager.request(
  () => api.getUser(),
  cacheKey: 'user',
  strategy: CacheFirst(),
);

That's it. The manager handles connectivity detection, caching, offline queueing, and automatic sync β€” all behind that single request() call.


Core Concepts

Network States

All states are Dart 3 sealed classes β€” fully exhaustive in switch:

State Description
Idle No operation in progress
Loading A request is executing
Offline Device has no connectivity. Includes queuedRequests count
Syncing Queue is being replayed after reconnect. Includes progress (0.0–1.0)
Success<T> Request completed. Contains data and fromCache flag
Error Request failed. Contains message, exception, and isRetryable

Request Strategies

Control how each request is resolved:

Strategy Behavior
NetworkFirst() Try network β†’ fall back to cache on failure (default)
CacheFirst() Try cache β†’ fall back to network on miss or expiry
CacheOnly() Only use cache β€” never hits the network
NetworkOnly() Only use network β€” never reads or writes cache
// Cache-first with custom TTL
final config = await manager.request(
  () => api.getAppConfig(),
  cacheKey: 'app_config',
  strategy: CacheFirst(),
  cacheTtl: Duration(hours: 1),
);

// Network-only for mutations
await manager.request(
  () => api.updateProfile(data),
  strategy: NetworkOnly(),
);

State Management Integration

flutter_network_state ships with built-in support for the most popular Flutter state management solutions β€” without adding any of them as dependencies. Pick the one you use:

Provider (ChangeNotifier)

Use NetworkNotifier, a ready-made ChangeNotifier:

// Setup
ChangeNotifierProvider(
  create: (_) => NetworkNotifier(networkManager),
  child: MyApp(),
);

// In your widget
class UserPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final notifier = context.watch<NetworkNotifier>();

    return switch (notifier.state) {
      Idle()    => Text('Tap to load'),
      Loading() => CircularProgressIndicator(),
      Offline() => Text('Offline (${(notifier.state as Offline).queuedRequests} queued)'),
      Success() => Text('Hello, ${(notifier.state as Success).data}'),
      Error()   => Text('Error: ${(notifier.state as Error).message}'),
      _         => SizedBox.shrink(),
    };
  }
}

// Trigger a request
final notifier = context.read<NetworkNotifier>();
await notifier.request(
  () => api.getUser(),
  cacheKey: 'user',
  strategy: CacheFirst(),
);

Cubit (No flutter_bloc needed)

Use NetworkCubit, a lightweight Cubit-like base class:

class UserCubit extends NetworkCubit {
  UserCubit(NetworkManager manager) : super(manager);

  Future<void> loadUser() async {
    await executeRequest(
      () => api.getUser(),
      cacheKey: 'user',
      strategy: CacheFirst(),
    );
  }

  Future<void> updateProfile(Map<String, dynamic> data) async {
    await executeRequest(
      () => api.updateProfile(data),
      strategy: NetworkOnly(),
    );
  }
}

// Usage
final cubit = UserCubit(networkManager);

// Listen to state changes
cubit.stream.listen((state) {
  switch (state) {
    case Success<User>() => print('Got user: ${state.data.name}');
    case Loading()       => print('Loading...');
    case Offline()       => print('Offline');
    case Error()         => print('Error: ${state.message}');
    _                    => null;
  }
});

await cubit.loadUser();

// Don't forget to close
await cubit.close();

Bloc (flutter_bloc)

If you already use flutter_bloc, use the executeAndEmit() extension:

class UserCubit extends Cubit<NetworkState> {
  UserCubit(this._manager) : super(const Idle());
  final NetworkManager _manager;

  Future<void> loadUser() async {
    await _manager.executeAndEmit(
      () => api.getUser(),
      emit: emit,
      cacheKey: 'user',
      strategy: CacheFirst(),
    );
  }
}

// Or bind the global connectivity state stream
class ConnectivityCubit extends Cubit<NetworkState> {
  ConnectivityCubit(this._manager) : super(const Idle());
  final NetworkManager _manager;
  late final StreamSubscription _sub;

  void init() {
    _sub = _manager.bindToEmit(emit);
  }

  @override
  Future<void> close() {
    _sub.cancel();
    return super.close();
  }
}

Riverpod

Use NetworkStateNotifier with Riverpod providers:

// 1. Provide the NetworkManager
final networkManagerProvider = Provider<NetworkManager>((ref) {
  final manager = NetworkManager();
  ref.onDispose(() => manager.dispose());
  return manager;
});

// 2. Stream the global connectivity state
final networkStateProvider = StreamProvider<NetworkState>((ref) {
  return ref.watch(networkManagerProvider).stateStream;
});

// 3. Create feature-specific notifiers
final userProvider = Provider<NetworkStateNotifier>((ref) {
  final notifier = ref.watch(networkManagerProvider).createNotifier();
  ref.onDispose(() => notifier.dispose());
  return notifier;
});

// In your widget
class UserPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userNotifier = ref.watch(userProvider);

    return switch (userNotifier.state) {
      Loading() => CircularProgressIndicator(),
      Success() => Text('${(userNotifier.state as Success).data}'),
      Offline() => Text('Offline'),
      Error()   => Text('Error'),
      _         => ElevatedButton(
        onPressed: () => ref.read(userProvider).execute(
          () => api.getUser(),
          cacheKey: 'user',
          strategy: CacheFirst(),
        ),
        child: Text('Load User'),
      ),
    };
  }
}

Stream Transformer (Any architecture)

Turn any Stream<T> into Stream<NetworkState>:

final stateStream = myDataStream.asNetworkStates();
// Each emission becomes Success<T>, errors become Error

Dio Integration

Drop the interceptor into any existing Dio instance:

final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));

dio.interceptors.add(
  NetworkDioInterceptor(
    monitor: manager.monitor,
    queue: manager.queue,
    config: DioInterceptorConfig(
      retryPolicy: RetryPolicy(
        maxRetries: 3,
        backoff: ExponentialBackoff(baseDelay: Duration(seconds: 1)),
      ),
      queueWhenOffline: true,
      logRequests: true,
      logResponses: true,
    ),
  ),
);

The interceptor automatically:

  • βœ… Retries transient failures (timeouts, 500s, 502s, 503s, 429s)
  • βœ… Queues requests when the device is offline
  • βœ… Replays queued requests when connectivity is restored

Retry Policies

Every retry policy supports configurable backoff:

// Exponential backoff with jitter (default)
RetryPolicy(
  maxRetries: 3,
  backoff: ExponentialBackoff(
    baseDelay: Duration(seconds: 1),
    maxDelay: Duration(seconds: 30),
    withJitter: true, // Β±25% to prevent thundering herd
  ),
);

// Constant delay
RetryPolicy(
  maxRetries: 5,
  backoff: ConstantBackoff(duration: Duration(seconds: 2)),
);

// Linear backoff
RetryPolicy(
  maxRetries: 4,
  backoff: LinearBackoff(baseDelay: Duration(seconds: 1)),
);

// Disable retries
RetryPolicy.none;

Custom retry predicate:

RetryPolicy(
  maxRetries: 3,
  shouldRetry: (error, attempt) {
    return error is TimeoutException;
  },
);

Cache Management

Basic operations

// Store
manager.cache.set('key', myData, ttl: Duration(hours: 1));

// Retrieve (null if expired)
final data = manager.cache.get<MyModel>('key');

// Check existence (respects TTL)
manager.cache.has('key');

// Invalidate
manager.invalidateCache('key');

// Clear all
manager.clearCache();

Custom storage backend

Implement CacheStore to use Hive, Isar, SharedPreferences, or any persistence layer:

class HiveCacheStore implements CacheStore {
  final Box _box;
  HiveCacheStore(this._box);

  @override
  dynamic get(String key) => _box.get(key);
  @override
  void set(String key, dynamic value) => _box.put(key, value);
  @override
  void remove(String key) => _box.delete(key);
  @override
  void clear() => _box.clear();
  @override
  bool containsKey(String key) => _box.containsKey(key);
}

final manager = NetworkManager(
  cacheManager: CacheManager(store: HiveCacheStore(box)),
);

Offline Queue & Sync

Automatic flow

  1. Request fails while offline β†’ automatically queued
  2. Device reconnects β†’ SyncEngine drains the queue
  3. State stream emits Syncing(progress) β†’ UI shows progress
  4. All done β†’ state returns to Idle

Manual queue operations

// Enqueue manually
manager.queue.enqueue(
  QueuedRequest(
    id: 'update_profile',
    execute: () => api.updateProfile(data),
    retryPolicy: RetryPolicy(maxRetries: 5),
  ),
);

// Inspect
print('Pending: ${manager.queue.length}');

// Force sync
final results = await manager.syncNow();

// Clear
manager.clearQueue();

Persistent queue (survives app restarts)

By default, the queue is in-memory and lost on restart. For persistence, use PersistentQueueStore:

import 'package:path_provider/path_provider.dart';

// 1. Create the persistent store
final appDir = await getApplicationDocumentsDirectory();
final store = PersistentQueueStore(
  directory: appDir,
  executor: (request) async {
    // This callback replays persisted requests using your HTTP client
    final response = await dio.request(
      request.url,
      options: Options(method: request.method, headers: request.headers),
      data: request.body,
    );
    return response.data;
  },
);
await store.initialize(); // Load queue from disk

// 2. Inject into NetworkManager
final manager = NetworkManager(
  queue: RequestQueue(store: store),
);

// 3. Enqueue serializable requests (persisted to disk)
await store.enqueueSerializable(SerializableRequest(
  id: 'update_profile',
  method: 'PUT',
  url: 'https://api.example.com/profile',
  headers: {'Authorization': 'Bearer $token'},
  body: jsonEncode({'name': 'John'}),
  cacheKey: 'profile',
  maxRetries: 5,
));

// Queue survives app restarts β€” requests replay automatically on reconnect

Hive queue store

For apps already using Hive, see doc/hive_queue_store.md β€” copy the implementation into your project:

# pubspec.yaml
dependencies:
  hive: ^2.2.3
  hive_flutter: ^1.1.0
import 'package:hive_flutter/hive_flutter.dart';

await Hive.initFlutter();
final box = await Hive.openBox<String>('network_queue');

final store = HiveQueueStore(
  box: box,
  executor: (req) async {
    return await dio.request(
      req.url,
      options: Options(method: req.method, headers: req.headers),
      data: req.body,
    );
  },
);

final manager = NetworkManager(queue: RequestQueue(store: store));

sqflite queue store

For apps using SQLite, see doc/sqflite_queue_store.md β€” copy the implementation into your project:

# pubspec.yaml
dependencies:
  sqflite: ^2.3.0
  path: ^1.8.0
final store = await SqfliteQueueStore.open(
  executor: (req) async {
    return await dio.request(
      req.url,
      options: Options(method: req.method, headers: req.headers),
      data: req.body,
    );
  },
);

final manager = NetworkManager(queue: RequestQueue(store: store));

// Don't forget to close when done
await store.close();

Which one should I use?

Store Best for
PersistentQueueStore (built-in) Simple apps, no extra dependencies
HiveQueueStore Apps already using Hive
SqfliteQueueStore Apps needing SQL queries or large queues

Logging

// Default β€” prints to console
final manager = NetworkManager();

// Silent
final manager = NetworkManager(logger: NetworkLogger.silent);

// Custom β€” forward to Crashlytics, Sentry, etc.
final manager = NetworkManager(
  logger: NetworkLogger.custom(
    (level, message) {
      FirebaseCrashlytics.instance.log('[$level] $message');
    },
    minLevel: LogLevel.warning,
  ),
);

Configuration

final manager = NetworkManager(
  config: NetworkManagerConfig(
    defaultStrategy: NetworkFirst(),
    defaultCacheTtl: Duration(minutes: 5),
    retryPolicy: RetryPolicy(maxRetries: 3),
    autoStart: true,
  ),
);

Real Internet Check

connectivity_plus only checks if a network interface is available β€” it doesn't verify actual internet access (captive portals, DNS failures, etc.). InternetChecker solves this:

final checker = InternetChecker();

// One-shot check
final isOnline = await checker.hasInternetAccess();
print('Internet: $isOnline');

// Periodic monitoring (emits only on change)
checker.startPeriodicCheck();
checker.statusStream.listen((isOnline) {
  print('Internet changed: $isOnline');
});

// Custom config
final checker = InternetChecker(
  config: InternetCheckerConfig(
    lookupAddresses: ['google.com', 'cloudflare.com'],
    timeout: Duration(seconds: 5),
    checkInterval: Duration(seconds: 15),
  ),
);

Widgets

Drop-in widgets that rebuild automatically based on network state:

NetworkBuilder

NetworkBuilder(
  manager: networkManager,
  builder: (context, state) {
    return switch (state) {
      Idle()    => Text('Ready'),
      Loading() => CircularProgressIndicator(),
      Offline() => Text('Offline (${state.queuedRequests} queued)'),
      Syncing() => LinearProgressIndicator(value: state.progress),
      Success() => Text('Data: ${state.data}'),
      Error()   => Text('Error: ${state.message}'),
    };
  },
)

ConnectivityBuilder

ConnectivityBuilder(
  manager: networkManager,
  onlineBuilder: (context) => MyMainContent(),
  offlineBuilder: (context, queuedCount) => Column(
    children: [
      Icon(Icons.wifi_off),
      Text('You are offline'),
      Text('$queuedCount requests queued'),
    ],
  ),
)

Architecture

lib/
 β”œβ”€β”€ core/
 β”‚    β”œβ”€β”€ internet_checker.dart     ← Real internet verification (DNS)
 β”‚    β”œβ”€β”€ network_state.dart        ← Sealed state hierarchy
 β”‚    β”œβ”€β”€ network_status.dart       ← Connectivity monitor
 β”‚    β”œβ”€β”€ network_strategy.dart     ← Request strategies
 β”‚    β”œβ”€β”€ retry_policy.dart         ← Backoff strategies
 β”‚    └── logger.dart               ← Pluggable logger
 β”œβ”€β”€ manager/
 β”‚    └── network_manager.dart      ← Central orchestrator
 β”œβ”€β”€ queue/
 β”‚    └── request_queue.dart        ← Offline request queue
 β”œβ”€β”€ cache/
 β”‚    └── cache_manager.dart        ← TTL cache
 β”œβ”€β”€ sync/
 β”‚    └── sync_engine.dart          ← Auto-sync on reconnect
 β”œβ”€β”€ dio/
 β”‚    └── dio_interceptor.dart      ← Dio interceptor
 β”œβ”€β”€ widgets/
 β”‚    └── network_builder.dart      ← NetworkBuilder & ConnectivityBuilder
 β”œβ”€β”€ extensions/
 β”‚    β”œβ”€β”€ bloc_extension.dart       ← Bloc helpers
 β”‚    β”œβ”€β”€ cubit_extension.dart      ← Standalone Cubit base class
 β”‚    β”œβ”€β”€ provider_extension.dart   ← ChangeNotifier for Provider
 β”‚    └── riverpod_extension.dart   ← StateNotifier for Riverpod
 └── flutter_network_state.dart     ← Barrel export

Every sub-component can be injected individually for testing or customization.


Requirements

Requirement Version
Dart SDK >=3.0.0 <4.0.0
Flutter >=3.10.0
Null safety βœ…

Dependencies

Package Purpose
connectivity_plus Platform connectivity detection
dio HTTP client integration
meta Annotation utilities

Note: No dependency on provider, flutter_bloc, or riverpod. All state management integrations are built on plain Dart streams and Flutter SDK primitives.


Comparison

Feature connectivity_plus dio_cache_interceptor flutter_network_state
Online/Offline detection βœ… ❌ βœ…
Request strategies ❌ Partial βœ… 4 strategies
Cache with TTL ❌ βœ… βœ… + pluggable
Offline queue ❌ ❌ βœ…
Auto sync on reconnect ❌ ❌ βœ…
Retry with backoff ❌ ❌ βœ… 3 strategies
Dio interceptor ❌ βœ… βœ…
Provider support ❌ ❌ βœ…
Cubit support ❌ ❌ βœ…
Bloc support ❌ ❌ βœ…
Riverpod support ❌ ❌ βœ…
Unified state stream ❌ ❌ βœ…

πŸ’– Support

If this package helps you build better Flutter apps, consider supporting the development:

Support on SociaBuzz

Your support helps keep this package maintained and up-to-date. Every contribution is greatly appreciated! πŸ™


License

MIT β€” see LICENSE for details.

Libraries

cache/cache_manager
In-memory cache with TTL (time-to-live) support.
core/internet_checker
Real internet connectivity verification.
core/logger
Lightweight logging abstraction for the plugin.
core/network_state
Network states that represent the current lifecycle of a network-aware operation. These states are designed to be consumed by any state management solution (Bloc, Riverpod, Provider, etc.) via streams.
core/network_status
Reactive network connectivity monitor built on top of connectivity_plus.
core/network_strategy
Strategies that control how NetworkManager resolves a request.
core/retry_policy
Configurable retry policy with exponential back-off, jitter, and user-defined retry-eligibility predicates.
dio/dio_interceptor
Dio interceptor that integrates with NetworkManager.
extensions/bloc_extension
Convenience extensions for integrating NetworkManager with the Bloc pattern (or any StreamController-based state management).
extensions/cubit_extension
Lightweight Cubit-style base class for network-aware state management.
extensions/provider_extension
Extensions for integrating NetworkManager with ChangeNotifier-based state management (Provider, GetIt + ChangeNotifier, etc.).
extensions/riverpod_extension
Helpers for integrating NetworkManager with Riverpod.
flutter_network_state
flutter_network_state
manager/network_manager
The central orchestrator of flutter_network_state.
queue/persistent_queue_store
Persistent queue store that survives app restarts.
queue/request_queue
Offline request queue that stores failed / deferred requests and replays them when connectivity is restored.
sync/sync_engine
Sync engine that orchestrates connectivity changes and the offline request queue.
widgets/network_builder
A convenience widget that rebuilds based on NetworkState changes.