flutter_network_guard 0.1.2
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.
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);
}
}