Files
iiEsaywebUIapp/lib/core/widgets/error_boundary.dart

414 lines
13 KiB
Dart
Raw Normal View History

2025-08-10 01:20:45 +05:30
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';
2025-08-10 01:20:45 +05:30
/// 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;
2025-08-16 15:51:27 +05:30
void Function(FlutterErrorDetails details)? _previousOnError;
bool _shouldIgnoreError(Object error) {
// Ignore RenderFlex overflow errors (layout issues)
final errorString = error.toString();
return errorString.contains('RenderFlex') ||
errorString.contains('overflow') && errorString.contains('pixels');
}
2025-08-16 15:51:27 +05:30
void _scheduleHandleError(Object error, StackTrace? stack) {
// Skip errors that should be ignored
if (_shouldIgnoreError(error)) {
return;
}
2025-08-16 15:51:27 +05:30
// Defer to next frame to avoid setState during build exceptions
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_handleError(error, stack);
}
});
}
2025-08-10 01:20:45 +05:30
@override
void initState() {
super.initState();
// Set up Flutter error handling for this widget
2025-08-16 15:51:27 +05:30
_previousOnError = FlutterError.onError;
2025-08-10 01:20:45 +05:30
FlutterError.onError = (FlutterErrorDetails details) {
// Forward to any previously registered handler to avoid interfering
2025-08-16 15:51:27 +05:30
_previousOnError?.call(details);
// Defer handling to avoid setState during build
_scheduleHandleError(details.exception, details.stack);
2025-08-10 01:20:45 +05:30
};
}
2025-08-16 15:51:27 +05:30
@override
void dispose() {
// Restore previous error handler to avoid leaking global state
if (FlutterError.onError != _previousOnError) {
FlutterError.onError = _previousOnError;
}
super.dispose();
}
2025-08-10 01:20:45 +05:30
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
2025-09-23 11:00:25 +05:30
// Respect ambient text direction when available; fall back to LTR.
TextDirection direction;
try {
direction = Directionality.of(context);
} catch (_) {
direction = TextDirection.ltr;
}
2025-08-17 00:05:30 +05:30
return Directionality(
2025-09-23 11:00:25 +05:30
textDirection: direction,
2025-08-17 00:05:30 +05:30
child: Scaffold(
backgroundColor: context.conduitTheme.surfaceBackground,
2025-08-24 14:35:17 +05:30
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.pagePadding,
vertical: Spacing.lg,
),
child: Container(
constraints: const BoxConstraints(maxWidth: 480),
decoration: BoxDecoration(
color: context.conduitTheme.cardBackground,
borderRadius: BorderRadius.circular(
context.conduitTheme.radiusLg,
2025-08-24 14:35:17 +05:30
),
border: Border.all(
color: context.conduitTheme.cardBorder,
width: BorderWidth.regular,
2025-08-24 14:35:17 +05:30
),
boxShadow: context.conduitTheme.cardShadows,
2025-08-10 01:20:45 +05:30
),
padding: const EdgeInsets.all(Spacing.xl),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Error icon with gradient background
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: context.conduitTheme.errorBackground,
shape: BoxShape.circle,
),
child: Icon(
Icons.error_outline_rounded,
size: 40,
color: context.conduitTheme.error,
),
2025-08-24 14:35:17 +05:30
),
const SizedBox(height: Spacing.lg),
// Error title
Text(
AppLocalizations.of(context)?.errorMessage ??
'Something went wrong',
style: context.conduitTheme.headingSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: Spacing.sm),
// Error description
Text(
enhancedErrorService.getUserMessage(_error!),
textAlign: TextAlign.center,
style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary,
),
),
if (widget.allowRetry) ...[
const SizedBox(height: Spacing.xl),
// Retry button
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _retry,
icon: const Icon(Icons.refresh_rounded),
style: FilledButton.styleFrom(
backgroundColor: context.conduitTheme.buttonPrimary,
foregroundColor: context.conduitTheme.buttonPrimaryText,
padding: const EdgeInsets.symmetric(
horizontal: Spacing.lg,
vertical: Spacing.md,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
context.conduitTheme.radiusMd,
),
),
elevation: 0,
),
label: Text(
AppLocalizations.of(context)?.retry ?? 'Try Again',
style: context.conduitTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w600,
color: context.conduitTheme.buttonPrimaryText,
),
),
),
),
],
],
),
),
2025-08-24 14:35:17 +05:30
),
2025-08-10 01:20:45 +05:30
),
),
),
2025-08-24 14:35:17 +05:30
);
2025-08-10 01:20:45 +05:30
}
// Wrap child in error handler
return Builder(
builder: (context) {
ErrorWidget.builder = (FlutterErrorDetails details) {
2025-08-16 15:51:27 +05:30
// Defer handling to avoid setState during build of error widgets
_scheduleHandleError(details.exception, details.stack);
2025-08-10 01:20:45 +05:30
return const SizedBox.shrink();
};
try {
return widget.child;
} catch (error, stack) {
2025-08-16 15:51:27 +05:30
// Defer handling to avoid setState during build
_scheduleHandleError(error, stack);
2025-08-10 01:20:45 +05:30
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',
),
2025-08-10 01:20:45 +05:30
),
],
],
),
),
);
}
// 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);
},
);
}
}