feat: Add error handling utilities, NetworkService, RetryHelper, and OfflineIndicator
This commit is contained in:
parent
d403749092
commit
5f7dfa7a93
41
sojorn_app/lib/services/network_service.dart
Normal file
41
sojorn_app/lib/services/network_service.dart
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import 'dart:async';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
|
||||
/// Service for monitoring network connectivity status
|
||||
class NetworkService {
|
||||
static final NetworkService _instance = NetworkService._internal();
|
||||
factory NetworkService() => _instance;
|
||||
NetworkService._internal();
|
||||
|
||||
final Connectivity _connectivity = Connectivity();
|
||||
final StreamController<bool> _connectionController =
|
||||
StreamController<bool>.broadcast();
|
||||
|
||||
Stream<bool> get connectionStream => _connectionController.stream;
|
||||
bool _isConnected = true;
|
||||
|
||||
bool get isConnected => _isConnected;
|
||||
|
||||
/// Initialize the network service and start monitoring
|
||||
void initialize() {
|
||||
_connectivity.onConnectivityChanged.listen((List<ConnectivityResult> results) {
|
||||
final result = results.isNotEmpty ? results.first : ConnectivityResult.none;
|
||||
_isConnected = result != ConnectivityResult.none;
|
||||
_connectionController.add(_isConnected);
|
||||
});
|
||||
|
||||
// Check initial state
|
||||
_checkConnection();
|
||||
}
|
||||
|
||||
Future<void> _checkConnection() async {
|
||||
final results = await _connectivity.checkConnectivity();
|
||||
final result = results.isNotEmpty ? results.first : ConnectivityResult.none;
|
||||
_isConnected = result != ConnectivityResult.none;
|
||||
_connectionController.add(_isConnected);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_connectionController.close();
|
||||
}
|
||||
}
|
||||
87
sojorn_app/lib/utils/error_handler.dart
Normal file
87
sojorn_app/lib/utils/error_handler.dart
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Global error handler for consistent error messaging and logging
|
||||
class ErrorHandler {
|
||||
/// Handle an error and optionally show a snackbar to the user
|
||||
static void handleError(
|
||||
dynamic error, {
|
||||
required BuildContext context,
|
||||
String? userMessage,
|
||||
bool showSnackbar = true,
|
||||
}) {
|
||||
final displayMessage = _getDisplayMessage(error, userMessage);
|
||||
|
||||
// Log to console (in production, send to analytics/crash reporting)
|
||||
_logError(error, displayMessage);
|
||||
|
||||
if (showSnackbar && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(displayMessage),
|
||||
action: SnackBarAction(
|
||||
label: 'Dismiss',
|
||||
onPressed: () {},
|
||||
),
|
||||
duration: const Duration(seconds: 4),
|
||||
backgroundColor: Colors.red[700],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get user-friendly error message
|
||||
static String _getDisplayMessage(dynamic error, String? userMessage) {
|
||||
if (userMessage != null) return userMessage;
|
||||
|
||||
if (error is SocketException) {
|
||||
return 'No internet connection. Please check your network.';
|
||||
} else if (error is TimeoutException) {
|
||||
return 'Request timed out. Please try again.';
|
||||
} else if (error is FormatException) {
|
||||
return 'Invalid data format received.';
|
||||
} else if (error.toString().contains('401')) {
|
||||
return 'Authentication error. Please sign in again.';
|
||||
} else if (error.toString().contains('403')) {
|
||||
return 'You don\'t have permission to do that.';
|
||||
} else if (error.toString().contains('404')) {
|
||||
return 'Resource not found.';
|
||||
} else if (error.toString().contains('500')) {
|
||||
return 'Server error. Please try again later.';
|
||||
} else {
|
||||
return 'Something went wrong. Please try again.';
|
||||
}
|
||||
}
|
||||
|
||||
/// Log error for debugging/analytics
|
||||
static void _logError(dynamic error, String message) {
|
||||
// In production, send to Sentry, Firebase Crashlytics, etc.
|
||||
debugPrint('ERROR: $message');
|
||||
debugPrint('Details: ${error.toString()}');
|
||||
if (error is Error) {
|
||||
debugPrint('Stack trace: ${error.stackTrace}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for async operations with automatic error handling
|
||||
Future<T?> safeExecute<T>({
|
||||
required Future<T> Function() operation,
|
||||
required BuildContext context,
|
||||
String? errorMessage,
|
||||
bool showError = true,
|
||||
}) async {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (e) {
|
||||
if (showError) {
|
||||
ErrorHandler.handleError(
|
||||
e,
|
||||
context: context,
|
||||
userMessage: errorMessage,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
60
sojorn_app/lib/utils/retry_helper.dart
Normal file
60
sojorn_app/lib/utils/retry_helper.dart
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import 'dart:async';
|
||||
|
||||
/// Helper for retrying failed operations with exponential backoff
|
||||
class RetryHelper {
|
||||
/// Retry an operation with exponential backoff
|
||||
static Future<T> retry<T>({
|
||||
required Future<T> Function() operation,
|
||||
int maxAttempts = 3,
|
||||
Duration initialDelay = const Duration(seconds: 1),
|
||||
double backoffMultiplier = 2.0,
|
||||
bool Function(dynamic error)? retryIf,
|
||||
}) async {
|
||||
int attempt = 0;
|
||||
Duration delay = initialDelay;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (e) {
|
||||
attempt++;
|
||||
|
||||
// Check if we should retry this error
|
||||
if (retryIf != null && !retryIf(e)) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
if (attempt >= maxAttempts) {
|
||||
rethrow; // Give up after max attempts
|
||||
}
|
||||
|
||||
// Wait before retrying with exponential backoff
|
||||
await Future.delayed(delay);
|
||||
delay = Duration(
|
||||
milliseconds: (delay.inMilliseconds * backoffMultiplier).round(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retry specifically for network operations
|
||||
static Future<T> retryNetwork<T>({
|
||||
required Future<T> Function() operation,
|
||||
int maxAttempts = 3,
|
||||
}) async {
|
||||
return retry(
|
||||
operation: operation,
|
||||
maxAttempts: maxAttempts,
|
||||
retryIf: (error) {
|
||||
// Retry on network errors, timeouts, and 5xx server errors
|
||||
final errorStr = error.toString().toLowerCase();
|
||||
return errorStr.contains('socket') ||
|
||||
errorStr.contains('timeout') ||
|
||||
errorStr.contains('500') ||
|
||||
errorStr.contains('502') ||
|
||||
errorStr.contains('503') ||
|
||||
errorStr.contains('504');
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
43
sojorn_app/lib/widgets/offline_indicator.dart
Normal file
43
sojorn_app/lib/widgets/offline_indicator.dart
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../services/network_service.dart';
|
||||
|
||||
/// Banner that appears at top of screen when offline
|
||||
class OfflineIndicator extends StatelessWidget {
|
||||
const OfflineIndicator({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<bool>(
|
||||
stream: NetworkService().connectionStream,
|
||||
initialData: NetworkService().isConnected,
|
||||
builder: (context, snapshot) {
|
||||
final isConnected = snapshot.data ?? true;
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
height: isConnected ? 0 : 30,
|
||||
color: Colors.orange[700],
|
||||
child: !isConnected
|
||||
? Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.wifi_off, size: 16, color: Colors.white),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'No internet connection',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue