Flutter 框架可以捕获运行期间的错误,包括构建期间、布局期间和绘制期间。
所有 Flutter 的错误均会被回调方法 FlutterError.onError 捕获。默认情况下,会调用 FlutterError.dumpErrorToConsole 方法,正如方法名表示的那样,将错误转储到当前的设备日志中。当从 IDE 运行应用时,检查器重写了该方法,错误也被发送到 IDE 的控制台,可以在控制台中检查出错的对象。
当构建期间发生错误时,回调函数 ErrorWidget.builder 会被调用,来生成一个新的 widget,用来代替构建失败的 widget。默认情况,debug 模式下会显示一个红色背景的错误页面, release 模式下会展示一个灰色背景的空白页面。
如果在调用堆栈上没有 Flutter 回调的情况下发生错误(这里可以理解为FlutterError.onError仅仅可以捕获主线程的错误,而其他异步线程的错误则需要Zone来捕获),它们由发生区域的 Zone 处理。 Zone 在默认情况下仅会打印错误,而不会执行其他任何操作。
这些回调方法都可以被重写,通常在 void main() 方法中重写。
下面来看看如何处理。
捕获Flutter错误
重写FlutterError的onError即可,如下
代码语言:javascript复制import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
FlutterError.onError = (FlutterErrorDetails details) {
FlutterError.dumpErrorToConsole(details);
if (kReleaseMode)
... //处理线上错误,如统计上传
};
runApp(MyApp());
}
上面我们重写了FlutterError.onError,这样就可以捕获到错误,第一行代码就是将error展示到控制台,这样我开发时就会在控制台很方便的看到错误。下面代码就是在线上环境下,对错误进一步处理,比如统计上传。
自定义ErrorWidget
上面我们知道,构建时发生错误会默认展示一个错误页面,但是这个页面很不友好,我们可以自定义一个错误页面。定义一个自定义的 error widget,以当 builder 构建 widget 失败时显示,请使用 MaterialApp.builder。
代码语言:javascript复制class MyApp extends StatelessWidget {
...
@override
Widget build(BuildContext context) {
return MaterialApp(
...
builder: (BuildContext context, Widget widget) {
Widget error = Text('...rendering error...');
if (widget is Scaffold || widget is Navigator)
error = Scaffold(body: Center(child: error));
ErrorWidget.builder = (FlutterErrorDetails errorDetails) => error;
return widget;
},
);
}
}
在App下的builder中,自定义一个error页面,然后赋值给ErrorWidget.builder即可。这样再出现错误的时候就可以展示一个友好的页面。
无法捕获的错误
假设一个 onPressed 回调调用了异步方法,例如 MethodChannel.invokeMethod (或者其他 plugin 的方法):
代码语言:javascript复制OutlinedButton(
child: Text('Click me!'),
onPressed: () async {
final channel = const MethodChannel('crashy-custom-channel');
await channel.invokeMethod('blah');
},
),
如果 invokeMethod 抛出了错误,它不会传递至 FlutterError.onError,而是直接进入 runApp 的 Zone。
如果你想捕获这样的错误,请使用 runZonedGuarded。代码如下:
代码语言:javascript复制import 'dart:async';
void main() {
runZonedGuarded(() {
runApp(MyApp());
}, (Object error, StackTrace stack) {
... //处理错误
});
}
请注意,如果你的应用在 runApp 中调用了 WidgetsFlutterBinding.ensureInitialized() 方法来进行一些初始化操作(例如 Firebase.initializeApp()),则必须在 runZonedGuarded 中调用 WidgetsFlutterBinding.ensureInitialized():
代码语言:javascript复制runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
如果 `WidgetsFlutterBinding.ensureInitialized()` 在外部调用,错误将不会被捕获到。
完整代码
如果要处理上面全部问题,代码如下:
代码语言:javascript复制import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized();
...
await myErrorsHandler.initialize();
FlutterError.onError = (FlutterErrorDetails details) {
FlutterError.dumpErrorToConsole(details);
myErrorsHandler.onError(details);
};
runApp(MyApp());
}, (Object error, StackTrace stack) {
myErrorsHandler.onError(error, stack);
});
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
...
builder: (BuildContext context, Widget widget) {
Widget error = Text('...rendering error...');
if (widget is Scaffold || widget is Navigator)
error = Scaffold(body: Center(child: error));
ErrorWidget.builder = (FlutterErrorDetails errorDetails) => error;
return widget;
},
);
}
}
先通过runZonedGuarded处理异步错误,在通过FlutterError.onError处理,这些错误都通过一个我们自定义的myErrorsHandler来集中处理即可,比如统计上传。然后在app中还需要定义一个友好的错误页面。