前言
最近组里童鞋,在写代码的时候,多次出现
Debug 都是好好的,Release 咋变成白屏了。
实际上 Debug
模式下,只是 ui
看起来好的,但其实上控制台已经有报错信息了,可能是因为 Debug
模式下,信息太多,没看到。
项目是重写过 ErrorWidget.builder
,按道理说应该能看到错误信息的,但实际上
却是白屏。
ErrorWidget.builder
我翻看了网络上大部分的文章,都表达的是用来重写发生错误的时候用于展示错误信息的 widget
。但是实际上,并不是这么简单的。
To define a customized error widget that displays whenever the builder fails to build a widget
Handling errors in Flutter | Flutter
在 Flutter 里处理错误 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter
分析
错误代码
下面是几种错误代码演示:
Stack(
children: <Widget>[
GestureDetector(
child: Positioned(
child: Container(
height: 10,
width: 10,
color: Colors.red,
),
),
)
],
)
Stack(
children: <Widget>[
Expanded(child: Container(),),
],
)
Column(
children: <Widget>[
Positioned(child: Container())
],
)
报错的原因,应该很明白,没有弄清楚 Positioned
的 parent
必须是 Stack
,Expanded
的 parent
必须是 Column,Row,Flex
.
白屏原因
我们先看一下 ErrorWidget.builder
的注释
/// The configurable factory for [ErrorWidget].
///
/// When an error occurs while building a widget, the broken widget is
/// replaced by the widget returned by this function. By default, an
/// [ErrorWidget] is returned.
///
the broken widget is replaced by the widget returned by this function
大家注意这一句话,出错的 widget
会被这个方法创建的 widget
替换掉。如果你用 profile
模式跑上面的代码,你会发现 ErrorWidget.builder
会被不停的触发。白屏的原因找到了,你提供的 ErrorWidget.builder
在这种场景下,依然是错误的,造成了无限循环。
官方默认处理
我们再看看这段注释。
/// The system is typically in an unstable state when this function is called.
/// An exception has just been thrown in the middle of build (and possibly
/// layout), so surrounding widgets and render objects may be in a rather
/// fragile state. The framework itself (especially the [BuildOwner]) may also
/// be confused, and additional exceptions are quite likely to be thrown.
///
/// Because of this, it is highly recommended that the widget returned from
/// this function perform the least amount of work possible. A
/// [LeafRenderObjectWidget] is the best choice, especially one that
/// corresponds to a [RenderBox] that can handle the most absurd of incoming
/// constraints. The default constructor maps to a [RenderErrorBox].
因为调用这个方法的时候,整个结构已经不稳定,如果你继续使用复杂的 widget
, 可能会造成额外的异常,所以推荐使用最小结构 LeafRenderObjectWidget
,处理不合理的约束。
官方默认的是 ErrorWidget(RenderErrorBox)
。
错误文本
release
无错误信息,debug
为 exception
。
String message = '';
assert(() {
message = '${_stringify(details.exception)}\nSee also: https://flutter.dev/docs/testing/errors';
return true;
}());
final Object exception = details.exception;
错误背景
release
灰色背景,debug
红色背景。
static Color _initBackgroundColor() {
// release 灰色
Color result = const Color(0xF0C0C0C0);
assert(() {
// debug 红色
result = const Color(0xF0900000);
return true;
}());
return result;
}
约束
宽高 (100000.0*100000.0)
const double _kMaxWidth = 100000.0;
const double _kMaxHeight = 100000.0;
@override
double computeMaxIntrinsicWidth(double height) {
return _kMaxWidth;
}
@override
double computeMaxIntrinsicHeight(double width) {
return _kMaxHeight;
}
@override
bool get sizedByParent => true;
@override
bool hitTestSelf(Offset position) => true;
@override
Size computeDryLayout(BoxConstraints constraints) {
return constraints.constrain(const Size(_kMaxWidth, _kMaxHeight));
}
绘制文本
最终 RenderErrorBox
的 paint
中绘制出错误信息
@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 (error) {
// If an error happens here we're in a terrible state, so we really should
// just forget about it and let the developer deal with the already-reported
// errors. It's unlikely that these errors are going to help with that.
}
}
优化
从上面分析看来,ErrorWidget.builder
真的不是简简单单重写就行了。如果想在 release
环境下面显示出来错误信息,那我们应该怎么做呢?
方案1
还是用官方的 ErrorWidget
,只是去掉对 message
只在 debug
下面赋值的限制。缺点,看不完全信息,没法滚动。
Widget _defaultErrorWidgetBuilder(FlutterErrorDetails details) {
String message =
'${details.exception}\nSee also: https://flutter.dev/docs/testing/errors';
final Object exception = details.exception;
return ErrorWidget.withDetails(
message: message, error: exception is FlutterError ? exception : null);
}
void main() {
ErrorWidget.builder = _defaultErrorWidgetBuilder;
}
方案2
发生错误的时候弹一个框来显示。
- 优点,可以定制,显示信息的界面
- 缺点,也不是百分百靠谱,毕竟你不知道发生错误情况是对哪一部分影响
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
void main() {
FlutterError.onError = _onError;
runZonedGuarded<void>(() async {
runApp(const MyApp());
},
((error, stack) => _onError(FlutterErrorDetails(
exception: error,
stack: stack,
))));
}
void _onError(FlutterErrorDetails details) {
// 根据自己情况上报异常
//
// 显示异常
WidgetsBinding.instance.addPostFrameCallback(
(timeStamp) {
showDialog(
context: MyApp.navigatorKey.currentContext!,
builder: (b) {
return Padding(
padding: const EdgeInsets.all(20.0),
child: GestureDetector(
onTap: () {
Navigator.of(b).pop();
exit(1);
},
child: Material(
child: Container(
padding: EdgeInsets.all(10),
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Text('exception:'),
Text('${details.exception}'),
Text('stack:'),
Text('${details.stack}'),
],
)),
),
),
),
);
},
);
},
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
navigatorKey: navigatorKey,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Stack(
children: <Widget>[
Expanded(
child: Container(),
),
],
),
);
}
}
结语
我在 ErrorWidget.builder not working on profile/release · Issue #109382 · flutter/flutter (github.com) 中跟官方有相关的讨论。
When writing an ErrorWidget you have to take great care that it cannot fail to build as there is no back up.
希望官方能在文档中,着重提醒这个问题,毕竟不是每个开发者都能够 take great care
。
也希望官方能提供出更加安全,简单的方式来自定义展示错误信息。
最后再次强调,
ErrorWidget.builder
不是用来给你自定义展示错误信息的,它是发生错误的时候,用来替换错误widget
的备份。
网友评论