Flutter中异常处理
Dart是单进程机制,所以在这个进程中出现问题时仅仅会影响当前进程,在事件循环中,当某个任务发生异常并没有被捕获时,程序并不会退出,而直接导致的结果是当前任务的后续代码就不会被执行了,也就是说一个任务中的异常是不会影响其它任务执行的。
Flutter 异常
Flutter 异常指的是,Flutter 程序中 Dart 代码运行时意外发生的错误事件。我们可以通过与 Java 类似的 try-catch 机制来捕获它。但与 Java 不同的是,Dart 程序不强制要求我们必须处理异常。
这是因为,Dart 采用事件循环的机制来运行任务,所以各个任务的运行状态是互相独立的。也就是说,即便某个任务出现了异常我们没有捕获它,Dart 程序也不会退出,只会导致当前任务后续的代码不会被执行,用户仍可以继续使用其他功能。
Dart 异常,根据来源又可以细分为 App 异常和 Framework 异常。Flutter 为这两种异常提供了不同的捕获方式,接下来我们就一起看看吧。
App 异常的捕获方式
App 异常,就是应用代码的异常,通常由未处理应用层其他模块所抛出的异常引起。根据异常代码的执行时序,App 异常可以分为两类,即同步异常和异步异常:同步异常可以通过 try-catch 机制捕获,异步异常则需要采用 Future 提供的 catchError 语句捕获。
这两种异常的捕获方式,如下代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| try { throw StateError('This is a Dart exception.'); } catch(e) { print(e); }
Future.delayed(Duration(seconds: 1)) .then((e) => throw StateError('This is a Dart exception in Future.')) .catchError((e)=>print(e));
try { Future.delayed(Duration(seconds: 1)) .then((e) => throw StateError('This is a Dart exception in Future.')) } catch(e) { print("This line will never be executed. "); }
|
需要注意的是,这两种方式是不能混用的。可以看到,在上面的代码中,我们是无法使用 try-catch 去捕获一个异步调用所抛出的异常的。
同步的 try-catch 和异步的 catchError,为我们提供了直接捕获特定异常的能力,而如果我们想集中管理代码中的所有异常,Flutter 也提供了 Zone.runZoned 方法。
我们可以给代码执行对象指定一个 Zone,在 Dart 中,Zone 表示一个代码执行的环境范围,其概念类似沙盒,不同沙盒之间是互相隔离的。如果我们想要观察沙盒中代码执行出现的异常,沙盒提供了 onError 回调函数,拦截那些在代码执行对象中的未捕获异常。
在下面的代码中,我们将可能抛出异常的语句放置在了 Zone 里。可以看到,在没有使用 try-catch 和 catchError 的情况下,无论是同步异常还是异步异常,都可以通过 Zone 直接捕获到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| runZoned(() { throw StateError('This is a Dart exception.'); }, onError: (dynamic e, StackTrace stack) { print('Sync error caught by zone'); }); runZoned(() { Future.delayed(Duration(seconds: 1)) .then((e) => throw StateError('This is a Dart exception in Future.')); }, onError: (dynamic e, StackTrace stack) { print('Async error aught by zone'); });
|
因此,如果我们想要集中捕获 Flutter 应用中的未处理异常,可以把 main 函数中的 runApp 语句也放置在 Zone 中。这样在检测到代码中运行异常时,我们就能根据获取到的异常上下文信息,进行统一处理了:
1 2 3 4 5
| runZoned<Future<Null>>(() async { runApp(MyApp()); }, onError: (error, stackTrace) async { });
|
下面,我们再看看 Framework 异常应该如何捕获吧。
Flutter 框架异常捕获
Flutter 框架为我们在很多关键的方法进行了异常捕获。这里举一个例子,当我们布局发生越界或不和规范时,Flutter就会自动弹出一个错误界面,这是因为Flutter已经在执行build方法时添加了异常捕获,最终的源码如下:
1 2 3 4 5 6 7 8 9 10 11 12
| @override void performRebuild() { ... try { built = build(); } catch (e, stack) { built = ErrorWidget.builder(_debugReportException('building $this', e, stack)); } ... }
|
可以看到,在发生异常时,Flutter 默认的处理方式时弹一个 ErrorWidget ,但如果我们想自己捕获异常并上报到报警平台的话应该怎么做?我们进入 _debugReportException()
方法看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| FlutterErrorDetails _debugReportException( String context, dynamic 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; }
|
我们发现,错误是通过 FlutterError.reportError
方法上报的,继续跟踪:
1 2 3 4 5
| static void reportError(FlutterErrorDetails details) { ... if (onError != null) onError(details); }
|
我们发现 onError
是 FlutterError
的一个静态属性,它有一个默认的处理方法
dumpErrorToConsole
,到这里就清晰了,如果我们想自己上报异常,只需要提供一个自定义的错误处理回调即可,如:
1 2 3 4 5 6
| void main() { FlutterError.onError = (FlutterErrorDetails details) { reportError(details); }; ... }
|
在这里我们使用 Zone 提供的 handleUncaughtError 语句,将 Flutter 框架的异常统一转发到当前的 Zone 中,这样我们就可以统一使用 Zone 去处理应用内的所有异常了:
1 2 3 4 5 6 7 8 9 10
| FlutterError.onError = (FlutterErrorDetails details) async { Zone.current.handleUncaughtError(details.exception, details.stack); }; runZoned<Future<Null>>(() async { runApp(MyApp()); }, onError: (error, stackTrace) async { });
|
同样的我们可以使用ErrorWidget.builder来自定义错误界面
1 2 3 4 5 6 7 8 9 10 11 12 13
| ErrorWidget.builder = (FlutterErrorDetails flutterErrorDetails) { return Scaffold( body: Center( child: Column(children: [ Icon( Icons.error, color: Colors.red, size: 100, ), Text(flutterErrorDetails.exceptionAsString()) ]), )); };
|
一个局中显示的错误图片和错误文本
异常处理
在错误界面我们可以根据Zone中的错误回调处理所有捕获的异常,当然,我们可以考虑把 错误文件存储到文件,上传到服务器或者上传到错误分析平台。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| import 'dart:async';
import 'package:flutter/material.dart';
main() { FlutterError.onError = (FlutterErrorDetails details) async { Zone.current.handleUncaughtError(details.exception, details.stack); };
ErrorWidget.builder = (FlutterErrorDetails flutterErrorDetails) { return Scaffold( body: Container( padding: EdgeInsets.only(left: 20, right: 20), child: Column(children: [ Icon( Icons.error, color: Colors.red, size: 100, ), Text( flutterErrorDetails.exceptionAsString(), style: TextStyle(color: Colors.blue, fontSize: 14), textAlign: TextAlign.start, ) ]), )); };
runZoned<Future<Null>>(() async { runApp(new MaterialApp( home: HomePage(), )); }, onError: (error, stackTrace) async { print(error.toString()); }); }
class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("ExceptionTitle"), ), body: Center( child: RaisedButton( child: Text("异常"), onPressed: () { throw new Exception("自定义异常"); }, ), ), ); } }
|
我们定义一个界面中间是一个按钮,点击按钮就会抛出一个自定义异常,在main方法中我们将FlutterError中的错误回调到Zone中,并把捕获到的异常信息打印到控制台。

点击异常按钮,观察控制台输出

同样的我们自定义了错误界面,当界面构建发生错误时就会显示我们自定义的错误界面

小结
- App 异常,我们可以将代码执行块放置到 Zone 中,通过 onError 回调进行统一处理
- Framework 异常,我们可以使用 FlutterError.onError 回调进行拦截
- 通过将FlutterError.onError转发到Zone中可以统一进行异常处理
- ErrorWidget.builder可以自定义错误界面