在 Flutter
开发中,我们经常遇见类似这样的错误提示:Null check operator used on a null value。
本文就来说说 Flutter
中的异常处理。
注意:本文对异常和错误没有进行严格的区分,就是混着用;异常捕获、异常处理、异常上报等也是相似的概念,大家知道意思就行。
Dart 中的错误
我们先来看看 Dart
中常见的错误类型。
- 编译时错误:这些错误发生在开发过程中,如果不修复它们,应用甚至无法编译。比如,语法错误和类型错误,通常编辑器会直接飘红提示。
- 运行时错误:这些错误发生在应用运行时,可能由各种因素触发。比如空指针异常、越界异常等。
- 逻辑错误:这些错误最难捕获,因为它们不会导致应用崩溃,但可能导致意外和不正确的行为。
Dart 中的错误处理技术
try-catch
和其他语言类似,Dart
最常见异常捕获技术就是使用 try-catch
。
try {
// Code that might throw an exception
int result = 10 ~/ 0; // This will throw a 'DivisionByZero' exception
} catch (e) {
// Handle the exception
print('An error occurred: $e');
}
on 关键字
on
关键字可以让开发者捕获特定类型的异常,这在处理不同的错误场景时非常有用。
try {
// ..
} on SocketException catch (e) {
// ..
} on PlatformException catch (e) {
// ..
} catch (e) {
// ..
}
throw 关键字
throw
关键字用于明确抛出异常。这让开发者可以处理异常情况,并在代码中给出错误信息。
void _launchURL() async => await canLaunch(_url)
? await launch(_url)
: throw 'Could not launch $_url';
assert 关键字
assert
主要用于在开发过程中验证有关应用程序状态的假设。如果断言失败,则会引发异常,这可以帮助开发者尽早捕获和调试错误。可发这可以使用 assert
语句来检查代码是否按预期运行,并在错误变得严重之前识别它们。
assert(myValue != null, 'myValue cannot be null');
async/await
和 Javascript
类似,Dart
中也可以使用 async
和 await
关键字来直观地处理异步代码,强烈推荐在 async
函数中使用 try-catch
块来优雅地处理错误。
Future<void> fetchData() async {
try {
// 可能会抛出异常的异步代码
var data = await fetchDataFromServer();
// 处理数据
} catch (e) {
// 处理错误
print('Error fetching data: $e');
}
}
Stream
流是异步事件的集合,这些事件会随时间持续发出数据。listen()
函数用于在使用流时订阅事件。我们可以使用 onError()
函数来捕获和处理流生命周期中发生的任何错误。
// Handling errors in a Stream
Stream<int>.periodic(Duration(seconds: 1), (count) => count)
.map((count) {
// Uncomment the line below to trigger an error
// if (count == 2) throw Exception('Error in stream');
return count;
})
.listen(
(data) => print('Stream data: $data'),
onError: (error) => print('Error in stream: $error'),
);
Flutter 框架中的错误处理
介绍完 Dart
中异常处理的技术之后,我们来看看 Flutter
中关于异常处理需要特别关注的点。
onError
Flutter
提供了一个全局异常处理函数 onError
,用于对异常详情进行上报。请看下面的例子。
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
void generateError() {
// nullable variable explicitly set to null
String? nullableString;
print(nullableString!);
}
@override
Widget build(BuildContext context) {
return Scaffold(
// ...
body: Center(
child: ElevatedButton(
child: const Text("Generate Error"),
onPressed: () => generateError(),
),
),
);
}
}
当点击 Generate Error 时,会看到控制台会输出错误信息。
控制台会给出具体的错误堆栈信息,方便开发时排查,也会看到该错误是被手势捕获了。Flutter
源码中很多逻辑里都加上了 try-catch
,出现异常时,在 catch
里面将错误信息进行上报。
@protected
@pragma('vm:notify-debugger-on-exception')
T? invokeCallback<T>(String name, RecognizerCallback<T> callback, { String Function()? debugReport }) {
T? result;
try {
// ...
result = callback();
} catch (exception, stack) {
InformationCollector? collector;
// 这里进行异常上报
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'gesture',
context: ErrorDescription('while handling a gesture'),
informationCollector: collector,
));
}
return result;
}
从源码里面我们可以发现是通过 reportError
方法将错误信息进行上报的,reportError
是 FlutterError
的一个静态方法。继续往下挖,会发现 reportError
最终调的方法是 FlutterError
的另一个静态方法 onError
。
static void reportError(FlutterErrorDetails details) {
onError?.call(details);
}
而 onError
默认是执行 dumpErrorToConsole
,将错误输出至控制台。这就是为什么我们在开发阶段出现错误时,会在控制台显示出来。
static FlutterExceptionHandler? onError = presentError;
static FlutterExceptionHandler presentError = dumpErrorToConsole;
static void dumpErrorToConsole(FlutterErrorDetails details, { bool forceReport = false }) {
// ...
// 将错误详情输出至控制台
}
到这里我们就清晰了,如果我们想自己处理异常,比如上报至 Sentry
或自己的服务器,只需重写 FlutterError.onError
即可。
void main() {
FlutterError.onError = (FlutterErrorDetails details) {
// 在此处处理错误,例如将错误日志发送到远程服务器或显示错误信息给用户
// print('Flutter Error: ${details.exception}');
// print('Stack trace:\n${details.stack}');
};
runApp(const MyApp());
}
重写后,控制台就不会打印错误详情了。
Error Widgets
在开发阶段,除了将异常信息输出至控制台,对于某些错误类型,Flutter
框架提供了展示错误的 widgets
,例如 ErrorWidget
、ErrorCatcher
和 ErrorListener
,以便优雅地显示错误。这些 widgets
允许开发者自定义错误消息,并在出现问题时向用户提供有用的信息。请看下面例子。
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text('Flutter Demo Home Page'),
),
body: const Center(
child: ErrorDemoWidget(),
),
);
}
}
class ErrorDemoWidget extends StatelessWidget {
const ErrorDemoWidget({super.key});
@override
Widget build(BuildContext context) {
String? nullableString;
return Text(nullableString!);
}
}
运行上述代码之后,会看到一个红色页面,提示 Null check operator used on a null value...,同时控制台也会给出错误详情。
这是因为在 build
时,Flutter
也加上了 try-catch
,当出现错误时,在 catch
里面对错误进行了处理。
@override
@pragma('vm:notify-debugger-on-exception')
void performRebuild() {
// ...
try {
// ...
// 执行 build 方法
built = build();
// ...
} catch (e, stack) {
// ...
// 有错误,显示错误页面
built = ErrorWidget.builder(
_reportException(
ErrorDescription('building $this'),
e,
stack,
),
);
} finally {
// ...
}
// ...
}
我们来看看 ErrorWidget.builder
方法。
static ErrorWidgetBuilder builder = _defaultErrorWidgetBuilder;
static Widget _defaultErrorWidgetBuilder(FlutterErrorDetails details) {
String message = '';
assert(() {
message = '${_stringify(details.exception)}\nSee also: https://flutter.dev/docs/testing/errors';
return true;
}());
final Object exception = details.exception;
return ErrorWidget.withDetails(message: message, error: exception is FlutterError ? exception : null);
}
开发时出现错误看到的红色页面的信息就是这里设置的。我们再来看看 _reportException
方法。
FlutterErrorDetails _reportException(
DiagnosticsNode context,
Object exception,
StackTrace? stack, {
InformationCollector? informationCollector,
}) {
final FlutterErrorDetails details = FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets library',
context: context,
informationCollector: informationCollector,
);
// 上报错误信息,默认输出至控制台
FlutterError.reportError(details);
return details;
}
显然,我们也可以自定义错误页面的样式,只需重写 ErrorWidget.builder
即可。
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
builder: (BuildContext context, Widget? widget) {
// 重写 ErrorWidget.builder 方法
ErrorWidget.builder = (FlutterErrorDetails details) {
return const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.error_outline_outlined,
color: Colors.red,
size: 100,
),
Text(
'Oops... something went wrong',
),
],
),
);
};
return widget!;
},
home: const MyHomePage(),
);
}
runZonedGuarded
在实际开发过程中,往往涉及到网络请求和文件 I/O
等异步操作,我们将上述 generateError
函数变为异步函数。
void generateError() async {
// nullable variable explicitly set to null
String? nullableString;
print(nullableString!);
}
再次触发异常,会发现控制台输出的信息不一样,同时 Logcat 也会加红显示错误信息。
控制台信息直接说是未处理的异常,说明异常没有被 FlutterError.onError
捕获到。这时我们可以加上 try-catch
,主动捕获。
在 Dart
中,同步异常可以通过 try/catch
捕获,而异步异常则比较麻烦,请看下面的例子。
void generateError() async {
try {
Future.delayed(const Duration(seconds: 1))
.then((e) => Future.error("Test Exception"));
} catch (e) {
print('e: $e');
}
}
上述代码即使是加上了 try-catch
也是捕获不了 Future
的异常的。
对于未被捕获到的异常,我们可以使用 runZonedGuarded
来捕获。
runZonedGuarded
是 Dart
提供的一个方法,可以给执行对象指定一个 Zone
。Zone
可以理解为一段代码 执行的环境范围,为了便于理解,可以理解将 Zone
类比代码执行沙箱。runZonedGuarded
常用于 Flutter
应用程序中捕获和处理未捕获的错误。它允许开发者指定一个回调函数,当指定代码块中发生未处理的错误时,该函数将被调用。具体用法可以看官网文档。
我们将代码用 runZoneGuarded
包裹一下。
runZonedGuarded(() => runApp(const MyApp()),
(Object error, StackTrace stackTrace) {
// 在此处处理错误,例如将错误日志发送到远程服务器或显示错误信息给用户
// print('error: $error');
// print('stackTrace: $stackTrace');
});
触发错误,会发现 runZonedGuarded
捕获到了错误信息。
异常上报
最后,我们再来说说异常上报。通过前面分析可知,我们结合 FlutterError.onError
和 runZonedGuarded
来捕获应用中的异常,最后进行上报。
void main() {
FlutterError.onError = (FlutterErrorDetails details) {
await _reportError(
details.exception,
details.stack,
errorDetails: details,
);
};
runZonedGuarded(
() => runApp(const MyApp()),
_reportError,
);
}
Future<void> _reportError(
Object error,
dynamic stackTrace, {
FlutterErrorDetails? errorDetails,
}) async {
// 上报逻辑
}
异常收集 catcher 库就是这么实现的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。