Flutter 中的Error的捕获及处理

2021-12-10 15:25:32 浏览数 (1)

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中还需要定义一个友好的错误页面。

0 人点赞