flutter_network_state 1.0.0 copy "flutter_network_state: ^1.0.0" to clipboard
flutter_network_state: ^1.0.0 copied to clipboard

A network-aware state management and request orchestration engine for Flutter. Provides offline/online detection, request queuing, cache strategies, automatic sync, Dio integration, and Bloc-friendly [...]

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
  • 🎯 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
  • 🧩 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();

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,
  ),
);

Architecture #

lib/
 β”œβ”€β”€ core/
 β”‚    β”œβ”€β”€ 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
 β”œβ”€β”€ 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 ❌ ❌ βœ…

License #

MIT β€” see LICENSE for details.

6
likes
0
points
36
downloads

Publisher

verified publisherridltech.my.id

Weekly Downloads

A network-aware state management and request orchestration engine for Flutter. Provides offline/online detection, request queuing, cache strategies, automatic sync, Dio integration, and Bloc-friendly state streams.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

connectivity_plus, dio, flutter, meta

More

Packages that depend on flutter_network_state