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 中也可以使用 asyncawait 关键字来直观地处理异步代码,强烈推荐在 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 时,会看到控制台会输出错误信息。

image.png

控制台会给出具体的错误堆栈信息,方便开发时排查,也会看到该错误是被手势捕获了。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 方法将错误信息进行上报的,reportErrorFlutterError 的一个静态方法。继续往下挖,会发现 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,例如 ErrorWidgetErrorCatcherErrorListener,以便优雅地显示错误。这些 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...,同时控制台也会给出错误详情。

image.png

这是因为在 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 也会加红显示错误信息。

image.png

控制台信息直接说是未处理的异常,说明异常没有被 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 的异常的。

image.png

对于未被捕获到的异常,我们可以使用 runZonedGuarded 来捕获。

runZonedGuardedDart 提供的一个方法,可以给执行对象指定一个 ZoneZone 可以理解为一段代码 执行的环境范围,为了便于理解,可以理解将 Zone 类比代码执行沙箱。runZonedGuarded 常用于 Flutter 应用程序中捕获和处理未捕获的错误。它允许开发者指定一个回调函数,当指定代码块中发生未处理的错误时,该函数将被调用。具体用法可以看官网文档。

我们将代码用 runZoneGuarded 包裹一下。

runZonedGuarded(() => runApp(const MyApp()),
    (Object error, StackTrace stackTrace) {
  // 在此处处理错误,例如将错误日志发送到远程服务器或显示错误信息给用户
  // print('error: $error');
  // print('stackTrace: $stackTrace');
});

触发错误,会发现 runZonedGuarded 捕获到了错误信息。

异常上报

最后,我们再来说说异常上报。通过前面分析可知,我们结合 FlutterError.onErrorrunZonedGuarded 来捕获应用中的异常,最后进行上报。

 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 库就是这么实现的。


见贤思齐
66 声望8 粉丝

写代码的