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