-
-
Save SergeShkurko/a57d5a96c405481d10db171efa287e65 to your computer and use it in GitHub Desktop.
An Flutter Error Handler Routine [null safety, flutter v3]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ignore_for_file: use_string_buffers | |
import 'dart:async' show runZoned, runZonedGuarded; | |
import 'dart:isolate' show Isolate, RawReceivePort; | |
import 'dart:ui' as ui | |
show | |
Paragraph, | |
ParagraphBuilder, | |
ParagraphConstraints, | |
ParagraphStyle, | |
TextStyle; | |
import 'package:flutter/foundation.dart' | |
show | |
DiagnosticPropertiesBuilder, | |
DiagnosticsTreeStyle, | |
FlutterError, | |
FlutterErrorDetails, | |
FlutterExceptionHandler, | |
StringProperty; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/rendering.dart' | |
show | |
Color, | |
DiagnosticPropertiesBuilder, | |
EdgeInsets, | |
FlutterError, | |
Offset, | |
Paint, | |
PaintingContext, | |
RenderBox, | |
Size, | |
StringProperty, | |
TextAlign, | |
TextDirection; | |
import 'package:flutter/widgets.dart' as w show runApp; | |
/// | |
/// | |
/// typedef FlutterExceptionHandler = void Function(FlutterErrorDetails details); | |
/// | |
/// Example 1: | |
/// | |
/// void main() => ErrorHandler.runApp(MyApp()); | |
/// | |
/// Example 2: | |
/// | |
/// ErrorHandler.onError = (FlutterErrorDetails details) async { | |
/// await _reportError(details.exception, details.stack); | |
/// }; | |
/// | |
/// Example 3: | |
/// | |
/// // Returns to the standard 'Red Screen' | |
/// ErrorHandler.builder = null; | |
/// | |
class ErrorHandler { | |
// | |
ErrorHandler({ | |
FlutterExceptionHandler? handler, | |
ErrorWidgetBuilder? builder, | |
}) { | |
_oldOnError = FlutterError.onError; | |
_oldBuilder = ErrorWidget.builder; | |
set(builder: builder, handler: handler); | |
} | |
ErrorHandler.init() { | |
/// Assigns a default ErrorWidget. | |
ErrorHandler( | |
builder: _defaultErrorWidget, | |
); | |
} | |
FlutterExceptionHandler? _onError; | |
FlutterExceptionHandler? _oldOnError; | |
late ErrorWidgetBuilder _oldBuilder; | |
bool _inHandler = false; | |
static bool ranApp = false; | |
/// Set to null to use the 'old' handler. | |
// ignore: avoid_setters_without_getters | |
set onError(FlutterExceptionHandler handler) { | |
// So you can assign null and use the original error routine. | |
_onError = handler; | |
set(handler: handler); | |
} | |
/// Set the ErrorWidget.builder | |
/// If assigned null, use the 'old' builder. | |
// ignore: avoid_setters_without_getters | |
set builder(ErrorWidgetBuilder? value) { | |
set(builder: value ?? _oldBuilder); | |
} | |
void set({ | |
Widget Function(FlutterErrorDetails details)? builder, | |
void Function(FlutterErrorDetails details)? handler, | |
}) { | |
// | |
if (builder != null) { | |
ErrorWidget.builder = builder; | |
} | |
_onError = handler; | |
FlutterError.onError = (details) { | |
// Prevent an infinite loop and fall back to the original handler. | |
if (_inHandler) { | |
_onError = null; | |
try { | |
_oldOnError?.call(details); | |
} catch (ex) { | |
// intentionally left empty. | |
} | |
return; | |
} | |
// If there's an error in the error handler, we want to know about it. | |
_inHandler = true; | |
final handler = _onError ?? _oldOnError; | |
handler?.call(details); | |
_inHandler = false; | |
}; | |
} | |
void dispose() { | |
// Restore the error widget routine. | |
ErrorWidget.builder = _oldBuilder; | |
// Return the original error routine. | |
FlutterError.onError = _oldOnError; | |
} | |
/// Determines if running in an IDE or in production. | |
static bool get inDebugger { | |
var inDebugMode = false; | |
// assert is removed in production. | |
assert(inDebugMode = true); | |
return inDebugMode; | |
} | |
/// Produce the FlutterErrorDetails object and invoke the FlutterError routine. | |
static FlutterErrorDetails _reportError( | |
Object exception, | |
Object? stack, | |
) { | |
final details = FlutterErrorDetails( | |
exception: exception, | |
stack: | |
stack is String ? StackTrace.fromString(stack) : stack as StackTrace, | |
library: 'error_handler.dart', | |
); | |
// Call the Flutter 'onError' routine. | |
FlutterError.reportError(details); | |
// Returned to the ErrorWidget object in many cases. | |
return details; | |
} | |
/// Wrap the Flutter app in the Error Handler. | |
static void runApp(Widget myApp, [ErrorHandler? handler]) { | |
// Can't be used properly if being called again and so do not continue. | |
if (ranApp) return; | |
ranApp = true; | |
// Catch any errors in the Flutter framework. | |
handler ??= ErrorHandler(); | |
// Catch any errors in the main() function. | |
Isolate.current.addErrorListener( | |
RawReceivePort((pair) async { | |
final isolateError = pair as List<dynamic>; | |
_reportError( | |
isolateError.first.toString(), | |
isolateError.last.toString(), | |
); | |
}).sendPort, | |
); | |
// To catch any 'Dart' errors occurring 'outside' of the Flutter framework. | |
runZonedGuarded<Future<void>>( | |
() async { | |
w.runApp(myApp); | |
// Catch any errors in the error handling. | |
}, | |
_reportError, | |
); | |
} | |
} | |
/// This class is intentionally doing things using the low-level | |
/// primitives to avoid depending on any subsystems that may have ended | |
/// up in an unstable state -- after all, this class is mainly used when | |
/// things have gone wrong. | |
Widget _defaultErrorWidget(FlutterErrorDetails details) { | |
String message; | |
try { | |
message = 'ERROR\n\n${details.exception}\n\n'; | |
final stackTrace = details.stack.toString().split('\n'); | |
final length = stackTrace.length > 5 ? 5 : stackTrace.length; | |
for (var i = 0; i < length; i++) { | |
message += '${stackTrace[i]}\n'; | |
} | |
} catch (e) { | |
message = 'Error'; | |
} | |
final exception = details.exception; | |
return _WidgetError( | |
message: message, | |
error: exception is FlutterError ? exception : null, | |
); | |
} | |
class _WidgetError extends LeafRenderObjectWidget { | |
_WidgetError({this.message = '', FlutterError? error}) | |
: _flutterError = error, | |
super(key: UniqueKey()); | |
/// The message to display. | |
final String message; | |
final FlutterError? _flutterError; | |
@override | |
RenderBox createRenderObject(BuildContext context) => _ErrorBox(message); | |
@override | |
void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
super.debugFillProperties(properties); | |
if (_flutterError == null) { | |
properties.add(StringProperty('message', message, quoted: false)); | |
} else { | |
properties.add( | |
_flutterError!.toDiagnosticsNode( | |
style: DiagnosticsTreeStyle.whitespace, | |
), | |
); | |
} | |
} | |
} | |
class _ErrorBox extends RenderBox { | |
/// | |
/// A message can optionally be provided. If a message is provided, an attempt | |
/// will be made to render the message when the box paints. | |
_ErrorBox([this.message = '']) { | |
try { | |
if (message != '') { | |
/// | |
/// Generally, the much better way to draw text in a RenderObject is to | |
/// use the TextPainter class. If you're looking for code to crib from, | |
/// see the paragraph.dart file and the RenderParagraph class. | |
final builder = ui.ParagraphBuilder(paragraphStyle) | |
..pushStyle(textStyle) | |
..addText(message); | |
_paragraph = builder.build(); | |
} | |
} catch (error) { | |
// Intentionally left empty. | |
} | |
} | |
/// The message to attempt to display at paint time. | |
final String message; | |
late ui.Paragraph _paragraph; | |
@override | |
double computeMaxIntrinsicWidth(double height) { | |
return 100000.0; | |
} | |
@override | |
double computeMaxIntrinsicHeight(double width) { | |
return 100000.0; | |
} | |
@override | |
bool get sizedByParent => true; | |
@override | |
bool hitTestSelf(Offset position) => true; | |
@override | |
void performResize() { | |
size = constraints.constrain(const Size(100000.0, 100000.0)); | |
} | |
/// The distance to place around the text. | |
/// | |
/// This is intended to ensure that if the [RenderBox] is placed at the top left | |
/// of the screen, under the system's status bar, the error text is still visible in | |
/// the area below the status bar. | |
/// | |
/// The padding is ignored if the error box is smaller than the padding. | |
/// | |
/// See also: | |
/// | |
/// * [minimumWidth], which controls how wide the box must be before the | |
// horizontal padding is applied. | |
static EdgeInsets padding = const EdgeInsets.fromLTRB(34.0, 96.0, 34.0, 12.0); | |
/// The width below which the horizontal padding is not applied. | |
/// | |
/// If the left and right padding would reduce the available width to less than | |
/// this value, then the text is rendered flush with the left edge. | |
static double minimumWidth = 200.0; | |
/// The color to use when painting the background of [RenderBox] objects. | |
/// a light gray. | |
static Color backgroundColor = const Color(0xF0C0C0C0); | |
/// The text style to use when painting [RenderBox] objects. | |
/// a dark gray sans-serif font. | |
static ui.TextStyle textStyle = _initTextStyle(); | |
static ui.TextStyle _initTextStyle() { | |
final result = ui.TextStyle( | |
color: const Color(0xFF303030), | |
fontFamily: 'sans-serif', | |
fontSize: 18.0, | |
); | |
return result; | |
} | |
/// The paragraph style to use when painting [RenderBox] objects. | |
static ui.ParagraphStyle paragraphStyle = ui.ParagraphStyle( | |
textDirection: TextDirection.ltr, | |
textAlign: TextAlign.left, | |
); | |
@override | |
void paint(PaintingContext context, Offset offset) { | |
try { | |
context.canvas.drawRect(offset & size, Paint()..color = backgroundColor); | |
var width = size.width; | |
var left = 0.0; | |
var top = 0.0; | |
if (width > padding.left + minimumWidth + padding.right) { | |
width -= padding.left + padding.right; | |
left += padding.left; | |
} | |
_paragraph.layout(ui.ParagraphConstraints(width: width)); | |
if (size.height > padding.top + _paragraph.height + padding.bottom) { | |
top += padding.top; | |
} | |
context.canvas.drawParagraph(_paragraph, offset + Offset(left, top)); | |
} catch (e) { | |
// Intentionally left empty. | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment