Flutter 3.3 中的错误捕获与处理

Flutter 框架捕获在框架本身触发的回调期间发生的错误,包括在构建、布局和绘制阶段遇到的错误,随后这些错误会转发给 FlutterError.onError 处理程序。非 Flutter 回调中发生的错误无法被 Flutter 框架捕获,但可以通过 PlatformDispatcher 捕获并处理。

调用堆栈上没有 Flutter 回调时发生的错误,由 PlatformDispatcher 捕获并转发给其错误回调处理。其默认处理程序仅将错误打印输出。

默认情况下,FlutterError.onError 会调用 FlutterError.presentError 将错误转储到设备日志中。从 IDE 运行时,检查器(inspect)会覆盖此行为,因此错误会被转发到 IDE 的控制台,从而允许开发者检查消息中提及的对象。

考虑在自定义错误处理程序中调用 FlutterError.presentError ,以便在控制台中查看日志。

构建阶段发生错误时,将调用 ErrorWidget.builder 构建一个占位部件以替换构建失败的组件

调试模式中,默认会构建一个以红色为背景显示错误消息的部件。在发布模式中,则以灰色为背景。

Flutter 捕获的错误

实例演示,让应用程序在发布模式下捕获到错误时立即退出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() {
  FlutterError.onError = (details) {
    FlutterError.presentError(details);
    if (kReleaseMode) exit(1);
  };
  runApp(const MyApp());
}

// rest of `flutter create` code...

顶级常量 kReleaseMode 表示应用程序是否在发布模式下编译。

自定义错误小部件

要自定义在构建器构建小部件失败时显示的小部件,请使用 MaterialApp.builder。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      builder: (context, widget) {
        Widget error = const Text('...rendering error...');
        if (widget is Scaffold || widget is Navigator) {
          error = Scaffold(body: Center(child: error));
        }
        ErrorWidget.builder = (errorDetails) => error;
        if (widget != null) return widget;
        throw ('widget is null');
      },
    );
  }
}

Flutter 未捕获的错误

下面实例中,当 invokeMethod 抛出错误时,并不会转发到 Flutter.onError。

1
2
3
4
5
6
7
OutlinedButton(
  child: const Text('Click me!'),
  onPressed: () async {
    const channel = MethodChannel('crashy-custom-channel')
    await channel.invokeMethod('blah')
  },
)

如本文起始部分所述,这个错误会被转发到 PlatformDispatcher。要捕获此类错误,需使用 PlatformDispatcher.instance.onError。详见下列实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import 'package:flutter/material.dart';
import 'dart:ui';

void main() {
  MyBackend myBackend = MyBackend();
  PlatformDispatcher.instance.onError = (error, stack) {
    myBackend.sendError(error, stack);
    return true;
  };
  runApp(const MyApp());
}

处理所有类型的错误

此处提供最小实例,演示如何捕获全部错误。

 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
import 'package:flutter/material.dart';
import 'dart:ui';

Future<void> main() async {
  await myErrorsHandler.initialize();

  // Flutter 捕获的错误
  FlutterError.onError = (details) {
    FlutterError.presentError(details);
    myErrorsHandler.onErrorDetails(details);
  };
  // Flutter 未捕获的错误
  PlatformDispatcher.instance.onError = (error, stack) {
    myErrorsHandler.onError(error, stack);
    return true;
  };
  
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      builder: (context, widget) {
        Widget error = const Text('...rendering error...');
        if (widget is Scaffold || widget is Navigator) {
          error = Scaffold(body: Center(child: error));
        }
        ErrorWidget.builder = (errorDetails) => error;
        if (widget != null) return widget;
        throw ('widget is null');
      },
    );
  }
}

番外篇

在以前版本中 (< 3.3),必须使用自定义 Zone 才能捕获 Flutter 未捕获的错误。但是,自定义 Zone 对 Dart 核心库中一些优化是有不利的,这会减慢应用程序的启动速度。在 Flutter 3.3 及之后的版本中,推荐通过设置 PlatformDispatcher.onError 处理错误。

参考资料

Flutter 官方文档: Handling errors in Flutter

Medium 文章: What’s new in Flutter 3.3