Flutter中的异常处理

Flutter中的异常处理

说明

FLutter中的错误不会导致应用程序奔溃,只会终止执行出错代码之后的逻辑,在导致Widget.build()返回为null的错误会导致Widget构建失败,并返回红底黄字的错误原因Widget(在Release模式则会显示为灰底区域),对于异步方法产生异常等Flutter框架没有捕获的情况,会交由当前代码所在的Zone处理。

为什么 flutter 触发异常的时候不会崩溃?
这个和 flutter 的消息循环机制有关,任务分两种,一个是微任务 microtask,一个是事件 event,他们有自己的队列,每个任务是相互独立的,一旦某个任务触发异常,也就是导致这个任务后续代码无法执行,并不会影响其他任务执行

本文基于Flutter (Channel stable, 2.2.3)

详细说明

Flutter中的错误处理分为以下几种:

try...catch

对于普通的错误,可以通过try...catch来捕获:

1
2
3
4
5
6
7
8
9
10
try {
var list = [1, 2];
var three = list[3];
} on RangeError catch (e) {
print("这里是捕获RangeError类型的异常 $e");
} catch (e) {
print("这里是兜底的捕获异常 $e");
} finally {
print("这里是无论如何都会执行的代码");
}

对于,异步异常,可以使用await等待其执行完毕,将其变为同步任务,否则无法则捕获。

ErrorWidget.builder

当在Widget构建过程中出现错误,导致build()方法返回nullFlutter框架会调用ErrorWidget.builder返回一个Widget替代出错的Widget

默认情况下,debug模式返回的是红底黄字的错误提示,而release模式返回的是灰色Widget

可以在RunApp方法中替换这个默认的错误界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
runApp(MaterialApp(
home: Scaffold(
appBar: AppBar(),
body: BodyWidget(),
),
builder: (context, widget) {
// Widget在Build时出错的话,展示此Widget,
// 如果不定义的话,debug下为红底黄字错误信息,release会显示为灰色布局
// errorDetails在release模式下为空
ErrorWidget.builder = (FlutterErrorDetails errorDetails) {
return MainErrorWidget(widget, errorDetails);
};
return widget ?? Container();
},
));

上述代码中的MainErrorWidget是一个自定义的展示错误信息的页面。

  • MainErrorWidget的一种实现方式

    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
    class MainErrorWidget extends StatelessWidget {
    Widget? parentWidget;

    FlutterErrorDetails errorDetails;

    MainErrorWidget(this.parentWidget, this.errorDetails);

    @override
    Widget build(BuildContext context) {
    print("3. 布局出现错误,展示错误页面,此处错误在release中也会调用FlutterError.onError");

    Widget error = Card(
    child: SingleChildScrollView(
    child: Container(
    padding: const EdgeInsets.all(20),
    color: Colors.green,
    child: Text(
    '布局出现错误,以下是错误信息:\n$errorDetails',
    style: TextStyle(fontSize: 10, color: Colors.white),
    ),
    ),
    ),
    );
    if (parentWidget is Scaffold || parentWidget is Navigator) {
    debugPrint(
    "widget${parentWidget?.key?.toString()} ($parentWidget) is Scaffold ${parentWidget is Scaffold} or Navigator ${parentWidget is Navigator}");
    // error = Container(child: error);
    }
    return error;
    }
    }

需要注意的是,错误WidgetDebugRelease模式下有一些区别:

  • Debug模式下ErrorWidget.builder会返回错误详细信息FlutterErrorDetailsRelease下则不会;
  • Debug模式下,Widget等出错会打印Exception caught by widgets library ... 等提示并输出错误堆栈信息,但是Release模式下不会;
  • Debug模式下,出错不会调用FlutterError.onErrorRelease模式下会。

FlutterError.onError

上述几种情况都没有处理的,被Flutter框架引起的异常,会在这里被处理。

Flutter 2.2.3中,Debug模式下如onPressed中的未捕获错误等都会被Widget等捕获,而不会走到这里来,在Release模式下则会调用FlutterError.onError

在这里可以对错误进行处理,比如输出到控制台、交给Zone统一处理、直接结束掉APP等:

  • FlutterError.dumpErrorToConsole(details); 输出到控制台
  • exit(1); 退出APP
  • Zone.current.handleUncaughtError(details.exception, details.stack); 交给Zone统一处理
  • defaultOnError?.call(details); 自己处理完异常后,也要把异常向上抛 【推荐】,其中defaultOnError 可以预先缓存final defaultOnError = FlutterError.onError;

runZonedGuarded(onError)

上述几种情况都没有处理的异常,会被发送到这里处理,可以类比为Android中的Thread.UncaughtExceptionHandler

1
2
3
4
5
6
7
8
runZonedGuarded(() async {
runApp(...);
},
(Object error, StackTrace stack) {
// 没有被Flutter捕获的错误,全局未捕获异常处理,类似于Android的Thread.UncaughtExceptionHandler
/// 比如异步的方法
print("2. runZonedGuarded.onError $error");
});

Zone可以理解为一个沙盒,其中的代码出错,包括异步的都可以捕获到。但是如果是另外一个沙盒中的错误则无法处理。

参考文章

Flutter 官网异常处理

Flutter异常处理