Last active
September 20, 2022 21:51
-
-
Save Andrious/6e3e568f510c6b84cdb38b178465829d to your computer and use it in GitHub Desktop.
An Flutter Error Handler Routine
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
import 'dart:async' show runZoned; | |
import 'dart:isolate' show Isolate, RawReceivePort; | |
import 'dart:ui' as ui | |
show | |
Paragraph, | |
ParagraphBuilder, | |
ParagraphConstraints, | |
ParagraphStyle, | |
TextStyle; | |
import 'package:flutter/foundation.dart' show FlutterExceptionHandler; | |
import 'package:flutter/widgets.dart' as w show runApp; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/foundation.dart' | |
show | |
DiagnosticPropertiesBuilder, | |
DiagnosticsTreeStyle, | |
FlutterError, | |
FlutterErrorDetails, | |
FlutterExceptionHandler, | |
StringProperty; | |
import 'package:flutter/rendering.dart' | |
show | |
Color, | |
DiagnosticPropertiesBuilder, | |
EdgeInsets, | |
FlutterError, | |
Offset, | |
Paint, | |
PaintingContext, | |
RenderBox, | |
Size, | |
StringProperty, | |
TextAlign, | |
TextDirection; | |
/// | |
/// | |
/// 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: (FlutterErrorDetails details) => _defaultErrorWidget(details)); | |
} | |
FlutterExceptionHandler _onError; | |
FlutterExceptionHandler _oldOnError; | |
ErrorWidgetBuilder _oldBuilder; | |
bool _inHandler = false; | |
static bool ranApp = false; | |
/// Set to null to use the 'old' handler. | |
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. | |
set builder(ErrorWidgetBuilder builder) { | |
if (builder == null) builder = _oldBuilder; | |
set(builder: builder); | |
} | |
void set({ | |
Widget Function(FlutterErrorDetails details) builder, | |
void Function(FlutterErrorDetails details) handler, | |
}) { | |
// | |
if (builder != null) ErrorWidget.builder = builder; | |
if (handler != null) _onError = handler; | |
FlutterError.onError = (FlutterErrorDetails details) { | |
// Prevent an infinite loop and fall back to the original handler. | |
if (_inHandler) { | |
if (_onError != null && _oldOnError != null) { | |
_onError = null; | |
try { | |
_oldOnError(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 FlutterExceptionHandler handler = | |
_onError == null ? _oldOnError : _onError; | |
if (handler != null) { | |
handler(details); | |
_inHandler = false; | |
} | |
}; | |
} | |
void dispose() { | |
// Restore the error widget routine. | |
if (_oldBuilder != null) ErrorWidget.builder = _oldBuilder; | |
// Return the original error routine. | |
if (_oldOnError != null) 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( | |
dynamic exception, | |
dynamic stack, | |
) { | |
final FlutterErrorDetails details = FlutterErrorDetails( | |
exception: exception, | |
stack: stack is String ? StackTrace.fromString(stack) : stack, | |
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(new RawReceivePort((dynamic pair) async { | |
var isolateError = pair as List<dynamic>; | |
_reportError( | |
isolateError.first.toString(), | |
isolateError.last.toString(), | |
); | |
}).sendPort); | |
// To catch any 'Dart' errors occurring 'outside' of the Flutter framework. | |
runZoned<Future<void>>(() async { | |
w.runApp(myApp); | |
// Catch any errors in the error handling. | |
}, onError: (error, stackTrace) { | |
_reportError(error, stackTrace); | |
}); | |
} | |
} | |
/// 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.toString() + "\n\n"; | |
List<String> stackTrace = details.stack.toString().split("\n"); | |
int length = stackTrace.length > 5 ? 5 : stackTrace.length; | |
for (var i = 0; i < length; i++) { | |
message += stackTrace[i] + "\n"; | |
} | |
} catch (e) { | |
message = 'Error'; | |
} | |
final Object 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 ui.ParagraphBuilder builder = ui.ParagraphBuilder(paragraphStyle); | |
builder.pushStyle(textStyle); | |
builder.addText(message); | |
_paragraph = builder.build(); | |
} | |
} catch (error) { | |
// Intentionally left empty. | |
} | |
} | |
/// The message to attempt to display at paint time. | |
final String message; | |
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() { | |
ui.TextStyle 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); | |
if (_paragraph != null) { | |
double width = size.width; | |
double left = 0.0; | |
double 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. | |
} | |
} | |
} |
Certainly.
Excellent update to 'null safety & Flutter v. 3'!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks for your great detailed error implementation of flutter error!
null safety & flutter v3 compatible fork: https://gist.github.com/SergeShkurko/a57d5a96c405481d10db171efa287e65