flutter_network_guard 0.1.2 copy "flutter_network_guard: ^0.1.2" to clipboard
flutter_network_guard: ^0.1.2 copied to clipboard

Network reliability toolkit for Flutter — real internet reachability, server health checks, retry with backoff, offline queue, caching and network-aware widgets. State-management agnostic.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_network_guard/flutter_network_guard.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final cache = CacheManager();
  final queue = OfflineQueue(
    storage: const SharedPreferencesQueueStorage(),
    executor: (task) async {
      // Replace with your real HTTP client call, e.g.:
      // final response = await http.post(Uri.parse('https://api.example.com${task.endpoint}'),
      //     body: task.body);
      // return response.statusCode == 200;
      await Future<void>.delayed(const Duration(milliseconds: 300));
      return true; // pretend every queued task succeeds
    },
  );

  await NetworkGuard.initialize(
    config: NetworkGuardConfig(
      enableLogging: true,
      logLevel: LogLevel.info,
      offlineQueue: queue,
      cacheManager: cache,
      onInternetRestored: (info) {
        debugPrint('Internet restored: $info');
      },
    ),
  );

  runApp(const NetworkGuardExampleApp());
}

/// Root widget. Sets up a modern Material 3 theme (light + dark,
/// following the system setting) that the whole demo dashboard uses.
class NetworkGuardExampleApp extends StatelessWidget {
  const NetworkGuardExampleApp({super.key});

  static const _seed = Color(0xFF4F6BFF);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'flutter_network_guard',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: _seed,
        brightness: Brightness.light,
        useMaterial3: true,
        scaffoldBackgroundColor: const Color(0xFFF2F4FB),
        cardTheme: const CardThemeData(elevation: 0, margin: EdgeInsets.zero),
      ),
      darkTheme: ThemeData(
        colorSchemeSeed: _seed,
        brightness: Brightness.dark,
        useMaterial3: true,
        scaffoldBackgroundColor: const Color(0xFF0E1016),
        cardTheme: const CardThemeData(elevation: 0, margin: EdgeInsets.zero),
      ),
      themeMode: ThemeMode.light,
      home: const DashboardScreen(),
    );
  }
}

/// Breakpoints used across the dashboard to decide how many columns
/// to lay content out in.
class _Breakpoints {
  static const compact = 640.0; // phones
  static const medium = 1000.0; // tablets / small laptops
}

/// A single dashboard screen showing every major module: live status,
/// a protected API call demo, offline queueing, cache/metrics, and a
/// debug panel. The layout reflows from one column (phone) to two
/// columns (tablet) to a three-pane, sidebar-backed layout (desktop /
/// large laptop screens) using [LayoutBuilder].
class DashboardScreen extends StatefulWidget {
  const DashboardScreen({super.key});

  @override
  State<DashboardScreen> createState() => _DashboardScreenState();
}

class _DashboardScreenState extends State<DashboardScreen> {
  final NetworkMetrics _metrics = NetworkMetrics();
  String _lastResult = 'No request made yet.';
  bool _isCalling = false;

  Future<void> _simulateApiCall({required bool shouldFail}) async {
    setState(() => _isCalling = true);

    final result = await NetworkGuard.instance.execute<String>(
      key: 'demo_call',
      retryPolicy: const RetryPolicy(
        maxAttempts: 3,
        initialDelay: Duration(milliseconds: 500),
      ),
      request: () async {
        await Future<void>.delayed(const Duration(milliseconds: 400));
        if (shouldFail) throw Exception('Simulated failure');
        return 'Hello from the "server" at ${DateTime.now()}';
      },
    );

    setState(() {
      _isCalling = false;
      switch (result) {
        case NetworkSuccess(:final value, :final attempts):
          _lastResult = 'Success ($attempts attempt(s)): $value';
          _metrics.recordRequest(
            success: true,
            attempts: attempts,
            latency: result.latency,
          );
        case NetworkFailure(:final error, :final attempts):
          _lastResult = 'Failed after $attempts attempt(s): $error';
          _metrics.recordRequest(success: false, attempts: attempts);
        case NetworkOffline():
          _lastResult = 'Offline — request not attempted.';
          _metrics.recordRequest(success: false, wasOffline: true);
        case NetworkTimeout():
          _lastResult = 'Timed out.';
          _metrics.recordRequest(success: false, timedOut: true);
        case NetworkCancelled():
          _lastResult = 'Cancelled.';
        case NetworkQueued(:final taskId):
          _lastResult = 'Queued as $taskId.';
      }
    });
  }

  Future<void> _queueDemoTask() async {
    await NetworkGuard.instance.enqueue(
      QueuedRequest(
        id: 'demo_${DateTime.now().millisecondsSinceEpoch}',
        endpoint: '/demo',
        body: const {'note': 'queued from example app'},
      ),
    );
    _metrics.recordQueued();
    if (mounted) setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    final width = MediaQuery.sizeOf(context).width;
    final isMedium = width >= _Breakpoints.compact;
    final isWide = width >= _Breakpoints.medium;

    return Scaffold(
      body: Column(
        children: [
          _DashboardAppBar(isWide: isWide),
          const _StyledStatusBanner(),
          Expanded(
            child: LayoutBuilder(
              builder: (context, constraints) {
                final horizontalPadding = isWide
                    ? 32.0
                    : (isMedium ? 24.0 : 16.0);

                final mainColumn = _MainContent(
                  isMedium: isMedium,
                  isCalling: _isCalling,
                  lastResult: _lastResult,
                  onCallSucceed: () => _simulateApiCall(shouldFail: false),
                  onCallFail: () => _simulateApiCall(shouldFail: true),
                  onQueueTask: _queueDemoTask,
                );

                final sideColumn = _SideContent(metrics: _metrics);

                return Center(
                  child: ConstrainedBox(
                    constraints: const BoxConstraints(maxWidth: 1280),
                    child: SingleChildScrollView(
                      padding: EdgeInsets.symmetric(
                        horizontal: horizontalPadding,
                        vertical: 20,
                      ),
                      child: isWide
                          ? Row(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Expanded(flex: 3, child: mainColumn),
                                const SizedBox(width: 24),
                                Expanded(flex: 2, child: sideColumn),
                              ],
                            )
                          : Column(
                              children: [
                                mainColumn,
                                const SizedBox(height: 20),
                                sideColumn,
                              ],
                            ),
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// App bar
// ---------------------------------------------------------------------------

class _DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
  const _DashboardAppBar({required this.isWide});

  final bool isWide;

  @override
  Size get preferredSize => const Size.fromHeight(100);

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;

    return Container(
      height: preferredSize.height,
      padding: EdgeInsets.symmetric(horizontal: isWide ? 32 : 16),
      decoration: BoxDecoration(
        gradient: LinearGradient(
          colors: [scheme.primary, scheme.tertiary],
          begin: Alignment.centerLeft,
          end: Alignment.centerRight,
        ),
      ),
      child: SafeArea(
        bottom: false,
        child: Row(
          children: [
            Container(
              width: 40,
              height: 40,
              decoration: BoxDecoration(
                color: Colors.white.withValues(alpha: 0.18),
                borderRadius: BorderRadius.circular(12),
              ),
              child: const Icon(Icons.shield_moon_rounded, color: Colors.white),
            ),
            const SizedBox(width: 12),
            Expanded(
              child: Text(
                'flutter_network_guard',
                style: Theme.of(context).textTheme.titleLarge?.copyWith(
                  color: Colors.white,
                  fontWeight: FontWeight.w700,
                ),
                overflow: TextOverflow.ellipsis,
              ),
            ),
            const SizedBox(width: 12),
            const _LiveStatusPill(),
          ],
        ),
      ),
    );
  }
}

/// A small always-visible pill in the app bar reflecting the current
/// [NetworkInfo.status] via [NetworkGuardBuilder].
class _LiveStatusPill extends StatelessWidget {
  const _LiveStatusPill();

  @override
  Widget build(BuildContext context) {
    return NetworkGuardBuilder(
      builder: (context, info) {
        final color = _statusColor(info.status);
        return AnimatedContainer(
          duration: const Duration(milliseconds: 250),
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
          decoration: BoxDecoration(
            color: Colors.white.withValues(alpha: 0.16),
            borderRadius: BorderRadius.circular(999),
            border: Border.all(color: Colors.white.withValues(alpha: 0.25)),
          ),
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              Container(
                width: 8,
                height: 8,
                decoration: BoxDecoration(color: color, shape: BoxShape.circle),
              ),
              const SizedBox(width: 8),
              Text(
                _statusLabel(info.status),
                style: const TextStyle(
                  color: Colors.white,
                  fontWeight: FontWeight.w600,
                  fontSize: 13,
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}

// ---------------------------------------------------------------------------
// Status banner (re-styled version of NetworkStatusBanner using its
// `builder` hook so it matches the rest of the dashboard's look).
// ---------------------------------------------------------------------------

class _StyledStatusBanner extends StatelessWidget {
  const _StyledStatusBanner();

  @override
  Widget build(BuildContext context) {
    return NetworkStatusBanner(
      builder: (context, info, isBackOnline) {
        final offline = !info.status.isOnline;
        final color = offline
            ? const Color(0xFFE24C4C)
            : const Color(0xFF2FB170);
        final icon = offline ? Icons.wifi_off_rounded : Icons.wifi_rounded;
        final text = offline ? 'No internet connection' : 'Back online';

        return AnimatedContainer(
          duration: const Duration(milliseconds: 300),
          width: double.infinity,
          color: color,
          padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(icon, color: Colors.white, size: 18),
              const SizedBox(width: 8),
              Text(
                text,
                style: const TextStyle(
                  color: Colors.white,
                  fontWeight: FontWeight.w600,
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}

// ---------------------------------------------------------------------------
// Main column: live status grid, protected API demo, offline queue
// ---------------------------------------------------------------------------

class _MainContent extends StatelessWidget {
  const _MainContent({
    required this.isMedium,
    required this.isCalling,
    required this.lastResult,
    required this.onCallSucceed,
    required this.onCallFail,
    required this.onQueueTask,
  });

  final bool isMedium;
  final bool isCalling;
  final String lastResult;
  final VoidCallback onCallSucceed;
  final VoidCallback onCallFail;
  final Future<void> Function() onQueueTask;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        _SectionCard(
          icon: Icons.podcasts_rounded,
          title: 'Live status',
          subtitle: 'Updates automatically — powered by NetworkGuardBuilder',
          child: NetworkGuardBuilder(
            builder: (context, info) =>
                _LiveStatusGrid(info: info, isMedium: isMedium),
          ),
        ),
        const SizedBox(height: 20),
        _SectionCard(
          icon: Icons.bolt_rounded,
          title: 'Protected API call',
          subtitle: 'NetworkGuard.instance.execute() with retry + backoff',
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Wrap(
                spacing: 12,
                runSpacing: 12,
                children: [
                  _GradientButton(
                    label: 'Call (succeeds)',
                    icon: Icons.check_circle_outline_rounded,
                    loading: isCalling,
                    onPressed: onCallSucceed,
                  ),
                  _OutlineButton(
                    label: 'Call (fails + retries)',
                    icon: Icons.replay_rounded,
                    loading: isCalling,
                    onPressed: onCallFail,
                  ),
                ],
              ),
              const SizedBox(height: 16),
              _ResultBanner(text: lastResult),
            ],
          ),
        ),
        const SizedBox(height: 20),
        _SectionCard(
          icon: Icons.inventory_2_rounded,
          title: 'Offline queue',
          subtitle: 'Deferred writes, replayed automatically when back online',
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              NetworkGuardButton(
                onPressed: onQueueTask,
                builder: (context, onPressed, isLoading) => _GradientButton(
                  label: isLoading ? 'Queueing…' : 'Queue a demo task',
                  icon: Icons.add_task_rounded,
                  loading: isLoading,
                  onPressed: onPressed ?? () {},
                  enabled: onPressed != null,
                ),
              ),
              const SizedBox(height: 16),
              _QueueStatsRow(),
            ],
          ),
        ),
      ],
    );
  }
}

class _LiveStatusGrid extends StatelessWidget {
  const _LiveStatusGrid({required this.info, required this.isMedium});

  final NetworkInfo info;
  final bool isMedium;

  @override
  Widget build(BuildContext context) {
    final tiles = [
      _StatTile(
        icon: _statusIcon(info.status),
        label: 'Status',
        value: _statusLabel(info.status),
        color: _statusColor(info.status),
      ),
      _StatTile(
        icon: Icons.router_rounded,
        label: 'Connection',
        value: _connectionLabel(info.type),
        color: Theme.of(context).colorScheme.primary,
      ),
      _StatTile(
        icon: info.hasInternet
            ? Icons.public_rounded
            : Icons.public_off_rounded,
        label: 'Internet',
        value: info.hasInternet ? 'Reachable' : 'Unreachable',
        color: info.hasInternet
            ? const Color(0xFF2FB170)
            : const Color(0xFFE24C4C),
      ),
      _StatTile(
        icon: Icons.speed_rounded,
        label: 'Quality',
        value: _qualityLabel(info.quality),
        color: _qualityColor(info.quality),
      ),
    ];

    return GridView.count(
      crossAxisCount: isMedium ? 4 : 2,
      shrinkWrap: true,
      physics: const NeverScrollableScrollPhysics(),
      mainAxisSpacing: 12,
      crossAxisSpacing: 12,
      childAspectRatio: isMedium ? 1.3 : 1.5,
      children: tiles,
    );
  }
}

class _QueueStatsRow extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final queue = NetworkGuard.instance.queue;
    return Wrap(
      spacing: 10,
      runSpacing: 10,
      children: [
        _StatChip(
          icon: Icons.hourglass_empty_rounded,
          label: 'Pending',
          value: '${queue.pending.length}',
          color: const Color(0xFFE0A82E),
        ),
        _StatChip(
          icon: Icons.check_circle_rounded,
          label: 'Completed',
          value: '${queue.completed.length}',
          color: const Color(0xFF2FB170),
        ),
        _StatChip(
          icon: Icons.error_rounded,
          label: 'Failed',
          value: '${queue.failed.length}',
          color: const Color(0xFFE24C4C),
        ),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Side column: metrics + debug panel
// ---------------------------------------------------------------------------

class _SideContent extends StatelessWidget {
  const _SideContent({required this.metrics});

  final NetworkMetrics metrics;

  @override
  Widget build(BuildContext context) {
    final s = metrics.snapshot;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        _SectionCard(
          icon: Icons.insights_rounded,
          title: 'Metrics',
          subtitle: 'In-memory counters from NetworkMetrics',
          child: Column(
            children: [
              _MetricRow(label: 'Total requests', value: '${s.totalRequests}'),
              _MetricRow(
                label: 'Success rate',
                value: s.successRate == null
                    ? '—'
                    : '${(s.successRate! * 100).toStringAsFixed(0)}%',
              ),
              _MetricRow(label: 'Retries', value: '${s.retryCount}'),
              _MetricRow(label: 'Timeouts', value: '${s.timeoutCount}'),
              _MetricRow(
                label: 'Avg. latency',
                value: s.averageLatency == null
                    ? '—'
                    : '${s.averageLatency!.inMilliseconds}ms',
                showDivider: false,
              ),
            ],
          ),
        ),
        const SizedBox(height: 20),
        _SectionCard(
          icon: Icons.bug_report_rounded,
          title: 'Debug panel',
          subtitle: 'NetworkDebugPanel (forced on for this demo)',
          padding: EdgeInsets.zero,
          child: ClipRRect(
            borderRadius: BorderRadius.circular(16),
            child: const NetworkDebugPanel(forceEnabled: true),
          ),
        ),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Reusable building blocks
// ---------------------------------------------------------------------------

class _SectionCard extends StatelessWidget {
  const _SectionCard({
    required this.icon,
    required this.title,
    required this.child,
    this.subtitle,
    this.padding = const EdgeInsets.all(20),
  });

  final IconData icon;
  final String title;
  final String? subtitle;
  final Widget child;
  final EdgeInsets padding;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;

    return Container(
      decoration: BoxDecoration(
        color: scheme.surface,
        borderRadius: BorderRadius.circular(20),
        border: Border.all(color: scheme.outlineVariant.withValues(alpha: 0.4)),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withValues(alpha: 0.04),
            blurRadius: 16,
            offset: const Offset(0, 6),
          ),
        ],
      ),
      child: Padding(
        padding: padding,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Padding(
              padding: EdgeInsets.only(
                left: padding.left,
                right: padding.right,
                top: padding.top,
              ),
              child: Row(
                children: [
                  Container(
                    width: 36,
                    height: 36,
                    decoration: BoxDecoration(
                      color: scheme.primaryContainer,
                      borderRadius: BorderRadius.circular(10),
                    ),
                    child: Icon(
                      icon,
                      size: 18,
                      color: scheme.onPrimaryContainer,
                    ),
                  ),
                  const SizedBox(width: 12),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          title,
                          style: Theme.of(context).textTheme.titleMedium
                              ?.copyWith(fontWeight: FontWeight.w700),
                        ),
                        if (subtitle != null)
                          Text(
                            subtitle!,
                            style: Theme.of(context).textTheme.bodySmall
                                ?.copyWith(color: scheme.onSurfaceVariant),
                          ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
            SizedBox(height: padding == EdgeInsets.zero ? 0 : 18),
            child,
          ],
        ),
      ),
    );
  }
}

class _StatTile extends StatelessWidget {
  const _StatTile({
    required this.icon,
    required this.label,
    required this.value,
    required this.color,
  });

  final IconData icon;
  final String label;
  final String value;
  final Color color;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(14),
      decoration: BoxDecoration(
        color: color.withValues(alpha: 0.08),
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(icon, color: color, size: 22),
          const SizedBox(height: 8),
          Text(
            value,
            style: TextStyle(
              fontWeight: FontWeight.w700,
              color: color,
              fontSize: 15,
            ),
            overflow: TextOverflow.ellipsis,
          ),
          Text(
            label,
            style: Theme.of(context).textTheme.bodySmall?.copyWith(
              color: Theme.of(context).colorScheme.onSurfaceVariant,
            ),
          ),
        ],
      ),
    );
  }
}

class _StatChip extends StatelessWidget {
  const _StatChip({
    required this.icon,
    required this.label,
    required this.value,
    required this.color,
  });

  final IconData icon;
  final String label;
  final String value;
  final Color color;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
      decoration: BoxDecoration(
        color: color.withValues(alpha: 0.1),
        borderRadius: BorderRadius.circular(999),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(icon, size: 15, color: color),
          const SizedBox(width: 6),
          Text(
            '$value $label',
            style: TextStyle(
              color: color,
              fontWeight: FontWeight.w600,
              fontSize: 12.5,
            ),
          ),
        ],
      ),
    );
  }
}

class _MetricRow extends StatelessWidget {
  const _MetricRow({
    required this.label,
    required this.value,
    this.showDivider = true,
  });

  final String label;
  final String value;
  final bool showDivider;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return Column(
      children: [
        Padding(
          padding: const EdgeInsets.symmetric(vertical: 8),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text(label, style: TextStyle(color: scheme.onSurfaceVariant)),
              Text(value, style: const TextStyle(fontWeight: FontWeight.w700)),
            ],
          ),
        ),
        if (showDivider)
          Divider(
            height: 1,
            color: scheme.outlineVariant.withValues(alpha: 0.4),
          ),
      ],
    );
  }
}

class _ResultBanner extends StatelessWidget {
  const _ResultBanner({required this.text});

  final String text;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return AnimatedSwitcher(
      duration: const Duration(milliseconds: 200),
      child: Container(
        key: ValueKey(text),
        width: double.infinity,
        padding: const EdgeInsets.all(14),
        decoration: BoxDecoration(
          color: scheme.surfaceContainerHighest.withValues(alpha: 0.6),
          borderRadius: BorderRadius.circular(14),
        ),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Icon(
              Icons.terminal_rounded,
              size: 18,
              color: scheme.onSurfaceVariant,
            ),
            const SizedBox(width: 10),
            Expanded(
              child: Text(
                text,
                style: TextStyle(
                  color: scheme.onSurfaceVariant,
                  fontSize: 13.5,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _GradientButton extends StatelessWidget {
  const _GradientButton({
    required this.label,
    required this.icon,
    required this.onPressed,
    this.loading = false,
    this.enabled = true,
  });

  final String label;
  final IconData icon;
  final VoidCallback onPressed;
  final bool loading;
  final bool enabled;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    final active = enabled && !loading;

    return Opacity(
      opacity: active ? 1 : 0.6,
      child: DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(colors: [scheme.primary, scheme.tertiary]),
          borderRadius: BorderRadius.circular(14),
        ),
        child: Material(
          color: Colors.transparent,
          child: InkWell(
            borderRadius: BorderRadius.circular(14),
            onTap: active ? onPressed : null,
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 13),
              child: Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  if (loading)
                    const SizedBox(
                      width: 16,
                      height: 16,
                      child: CircularProgressIndicator(
                        strokeWidth: 2,
                        color: Colors.white,
                      ),
                    )
                  else
                    Icon(icon, size: 18, color: Colors.white),
                  const SizedBox(width: 10),
                  Text(
                    label,
                    style: const TextStyle(
                      color: Colors.white,
                      fontWeight: FontWeight.w600,
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _OutlineButton extends StatelessWidget {
  const _OutlineButton({
    required this.label,
    required this.icon,
    required this.onPressed,
    this.loading = false,
  });

  final String label;
  final IconData icon;
  final VoidCallback onPressed;
  final bool loading;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return OutlinedButton.icon(
      onPressed: loading ? null : onPressed,
      style: OutlinedButton.styleFrom(
        foregroundColor: scheme.primary,
        side: BorderSide(color: scheme.primary.withValues(alpha: 0.4)),
        padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 13),
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
      ),
      icon: Icon(icon, size: 18),
      label: Text(label, style: const TextStyle(fontWeight: FontWeight.w600)),
    );
  }
}

// ---------------------------------------------------------------------------
// Formatting helpers
// ---------------------------------------------------------------------------

Color _statusColor(NetworkStatus status) {
  switch (status) {
    case NetworkStatus.online:
      return const Color(0xFF2FB170);
    case NetworkStatus.unstable:
      return const Color(0xFFE0A82E);
    case NetworkStatus.offline:
      return const Color(0xFFE24C4C);
    case NetworkStatus.checking:
    case NetworkStatus.unknown:
      return const Color(0xFF9AA0B4);
  }
}

IconData _statusIcon(NetworkStatus status) {
  switch (status) {
    case NetworkStatus.online:
      return Icons.check_circle_rounded;
    case NetworkStatus.unstable:
      return Icons.warning_rounded;
    case NetworkStatus.offline:
      return Icons.cancel_rounded;
    case NetworkStatus.checking:
      return Icons.sync_rounded;
    case NetworkStatus.unknown:
      return Icons.help_rounded;
  }
}

String _statusLabel(NetworkStatus status) {
  switch (status) {
    case NetworkStatus.online:
      return 'Online';
    case NetworkStatus.unstable:
      return 'Unstable';
    case NetworkStatus.offline:
      return 'Offline';
    case NetworkStatus.checking:
      return 'Checking…';
    case NetworkStatus.unknown:
      return 'Unknown';
  }
}

String _connectionLabel(ConnectivityType type) {
  switch (type) {
    case ConnectivityType.wifi:
      return 'Wi-Fi';
    case ConnectivityType.mobile:
      return 'Mobile data';
    case ConnectivityType.ethernet:
      return 'Ethernet';
    case ConnectivityType.vpn:
      return 'VPN';
    case ConnectivityType.bluetooth:
      return 'Bluetooth';
    case ConnectivityType.other:
      return 'Other';
    case ConnectivityType.none:
      return 'None';
    case ConnectivityType.unknown:
      return 'Unknown';
  }
}

String _qualityLabel(NetworkQuality quality) {
  switch (quality) {
    case NetworkQuality.excellent:
      return 'Excellent';
    case NetworkQuality.good:
      return 'Good';
    case NetworkQuality.fair:
      return 'Fair';
    case NetworkQuality.poor:
      return 'Poor';
    case NetworkQuality.unknown:
      return 'Unknown';
  }
}

Color _qualityColor(NetworkQuality quality) {
  switch (quality) {
    case NetworkQuality.excellent:
      return const Color(0xFF2FB170);
    case NetworkQuality.good:
      return const Color(0xFF4F6BFF);
    case NetworkQuality.fair:
      return const Color(0xFFE0A82E);
    case NetworkQuality.poor:
      return const Color(0xFFE24C4C);
    case NetworkQuality.unknown:
      return const Color(0xFF9AA0B4);
  }
}
3
likes
130
points
140
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Network reliability toolkit for Flutter — real internet reachability, server health checks, retry with backoff, offline queue, caching and network-aware widgets. State-management agnostic.

Repository (GitHub)
View/report issues
Contributing

Topics

#connectivity #network #retry #offline #cache

License

MIT (license)

Dependencies

connectivity_plus, dio, flutter, http, shared_preferences

More

Packages that depend on flutter_network_guard