Files
iiEsaywebUIapp/lib/core/widgets/error_boundary.dart
2025-09-23 11:00:25 +05:30

346 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../shared/theme/theme_extensions.dart';
import '../error/enhanced_error_service.dart';
import 'package:conduit/l10n/app_localizations.dart';
/// Error boundary widget that catches and handles errors in child widgets
class ErrorBoundary extends ConsumerStatefulWidget {
final Widget child;
final Widget Function(Object error, StackTrace? stack)? errorBuilder;
final void Function(Object error, StackTrace? stack)? onError;
final bool showErrorDialog;
final bool allowRetry;
const ErrorBoundary({
super.key,
required this.child,
this.errorBuilder,
this.onError,
this.showErrorDialog = false,
this.allowRetry = true,
});
@override
ConsumerState<ErrorBoundary> createState() => _ErrorBoundaryState();
}
class _ErrorBoundaryState extends ConsumerState<ErrorBoundary> {
Object? _error;
StackTrace? _stackTrace;
bool _hasError = false;
void Function(FlutterErrorDetails details)? _previousOnError;
void _scheduleHandleError(Object error, StackTrace? stack) {
// Defer to next frame to avoid setState during build exceptions
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_handleError(error, stack);
}
});
}
@override
void initState() {
super.initState();
// Set up Flutter error handling for this widget
_previousOnError = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
// Forward to any previously registered handler to avoid interfering
_previousOnError?.call(details);
// Defer handling to avoid setState during build
_scheduleHandleError(details.exception, details.stack);
};
}
@override
void dispose() {
// Restore previous error handler to avoid leaking global state
if (FlutterError.onError != _previousOnError) {
FlutterError.onError = _previousOnError;
}
super.dispose();
}
void _handleError(Object error, StackTrace? stack) {
// Log error
enhancedErrorService.logError(
error,
context: 'ErrorBoundary',
stackTrace: stack,
);
// Call custom error handler if provided
widget.onError?.call(error, stack);
// Update state
if (mounted) {
setState(() {
_error = error;
_stackTrace = stack;
_hasError = true;
});
// Show error dialog if requested
if (widget.showErrorDialog && context.mounted) {
enhancedErrorService.showErrorDialog(context, error);
}
}
}
void _retry() {
setState(() {
_error = null;
_stackTrace = null;
_hasError = false;
});
}
@override
Widget build(BuildContext context) {
if (_hasError && _error != null) {
// Use custom error builder if provided
if (widget.errorBuilder != null) {
return widget.errorBuilder!(_error!, _stackTrace);
}
// Default error UI
// Respect ambient text direction when available; fall back to LTR.
TextDirection direction;
try {
direction = Directionality.of(context);
} catch (_) {
direction = TextDirection.ltr;
}
return Directionality(
textDirection: direction,
child: Scaffold(
backgroundColor: context.conduitTheme.surfaceBackground,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: context.conduitTheme.error,
),
const SizedBox(height: 16),
Text(
AppLocalizations.of(context)?.errorMessage ??
'An unexpected error occurred',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: context.conduitTheme.textPrimary,
),
),
const SizedBox(height: 8),
Text(
enhancedErrorService.getUserMessage(_error!),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: context.conduitTheme.textSecondary,
),
),
if (widget.allowRetry) ...[
const SizedBox(height: 24),
FilledButton.icon(
onPressed: _retry,
icon: const Icon(Icons.refresh),
label: Text(
AppLocalizations.of(context)?.retry ?? 'Retry',
),
),
],
],
),
),
),
),
);
}
// Wrap child in error handler
return Builder(
builder: (context) {
ErrorWidget.builder = (FlutterErrorDetails details) {
// Defer handling to avoid setState during build of error widgets
_scheduleHandleError(details.exception, details.stack);
return const SizedBox.shrink();
};
try {
return widget.child;
} catch (error, stack) {
// Defer handling to avoid setState during build
_scheduleHandleError(error, stack);
return const SizedBox.shrink();
}
},
);
}
}
/// Widget that handles async operations with proper error handling
class AsyncErrorBoundary extends ConsumerWidget {
final Future<Widget> Function() builder;
final Widget? loadingWidget;
final Widget Function(Object error)? errorWidget;
final bool showRetry;
const AsyncErrorBoundary({
super.key,
required this.builder,
this.loadingWidget,
this.errorWidget,
this.showRetry = true,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return FutureBuilder<Widget>(
future: builder(),
builder: (context, snapshot) {
// Loading state
if (snapshot.connectionState == ConnectionState.waiting) {
return loadingWidget ??
const Center(child: CircularProgressIndicator());
}
// Error state
if (snapshot.hasError) {
final error = snapshot.error!;
// Log error
enhancedErrorService.logError(
error,
context: 'AsyncErrorBoundary',
stackTrace: snapshot.stackTrace,
);
// Use custom error widget if provided
if (errorWidget != null) {
return errorWidget!(error);
}
// Default error widget
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 48,
color: context.conduitTheme.error,
),
const SizedBox(height: 16),
Text(
enhancedErrorService.getUserMessage(error),
textAlign: TextAlign.center,
),
if (showRetry) ...[
const SizedBox(height: 16),
FilledButton.icon(
onPressed: () {
// Force rebuild to retry
(context as Element).markNeedsBuild();
},
icon: const Icon(Icons.refresh),
label: Text(
AppLocalizations.of(context)?.retry ?? 'Retry',
),
),
],
],
),
),
);
}
// Success state
return snapshot.data ?? const SizedBox.shrink();
},
);
}
}
/// Stream error boundary for handling stream errors
class StreamErrorBoundary<T> extends ConsumerWidget {
final Stream<T> stream;
final Widget Function(T data) builder;
final Widget? loadingWidget;
final Widget Function(Object error)? errorWidget;
final T? initialData;
const StreamErrorBoundary({
super.key,
required this.stream,
required this.builder,
this.loadingWidget,
this.errorWidget,
this.initialData,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return StreamBuilder<T>(
stream: stream,
initialData: initialData,
builder: (context, snapshot) {
// Error state
if (snapshot.hasError) {
final error = snapshot.error!;
// Log error
enhancedErrorService.logError(
error,
context: 'StreamErrorBoundary',
stackTrace: snapshot.stackTrace,
);
// Use custom error widget if provided
if (errorWidget != null) {
return errorWidget!(error);
}
// Default error widget
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 48,
color: context.conduitTheme.error,
),
const SizedBox(height: 16),
Text(
enhancedErrorService.getUserMessage(error),
textAlign: TextAlign.center,
),
],
),
),
);
}
// Loading state
if (!snapshot.hasData) {
return loadingWidget ??
const Center(child: CircularProgressIndicator());
}
// Success state
return builder(snapshot.data as T);
},
);
}
}