Flutter如何避免常见sentry异常的编码技巧

背景

笔者入坑Flutter开发已经2年多,参与或主导大大小小的Flutter应用也超过了5个以上,自认为已经熟练掌握Flutter开发,能写出一手高质量的代码。所以前一阵子把所有的Flutter项目都接入Sentry,定了一个目标,Flutter错误率控制在>=99.95%以上(异常率=1 - 异常次数 / 启动次数)。结果接入之后,被现实狠狠打了一巴掌,能达成目标的应用一个都没有,平均水平在95%左右,有一些应用甚至低于90%。为了达成目标,笔者特意花了几天时间,解决了200个以上Flutter异常问题,才最终将异常率提升到平均99.5%。

为此,笔者总结一下处理问题过程中的代码书写经验,避免后人踩坑。

如何书写高质量的flutter代码?

一、Dart 空指针异常

这类错误是在flutter非常常见的异常,如果不加以关注,那么sentry会收集到非常的错误数据。

报错信息

dart 复制代码
_CastError
Null check operator used on a null value

原因分析

arduino 复制代码
"Null check operator used on a null value" 是一个 Dart 语言中的错误提示,表示在一个空值上使用了非空检查操作符(!)。
这个错误通常发生在你使用了非空检查操作符(!)来访问一个可能为空的变量或表达式时。当你使用非空检查操作符(!)时,
你告诉编译器"我知道这个值可能为空,但是我确定它不会为空,请继续执行"。
但是,如果实际上这个值为空,那么就会抛出一个异常,即 "Null check operator used on a null value"。

1、setState调用异常

原因分析

组件unmount之后,还存在setState调用,由于_element = null,此时调用_element!.markNeesBuild()就会抛出异常.

一般是异步函数回调里调用setState,导致此问题出现。因此,异步函数后,调用setState需要特别关注。

目前Flutter异步函数的场景如下:

  1. Future、async/await
  2. Timer
  3. Stream
  4. Native Channel

解决方案

1、出现异常的地方使用 if (mounted) setState(...);

注意:mounted == context.mounted,但使用context前,必须保证context不为null

2、重写State类

由于setState异常一般不影响应用的时候,但为了解决每次setState都需要做mounted判断的处理,比较繁琐,可以封装一个父类,对setState异常做捕获,并用日志记录。

scala 复制代码
abstract class StateWidget<T extends StatefulWidget> extends State<T> {
  @protected
  void setState(VoidCallback fn) {
    super.setState(() {
      if (mounted) {
        fn();
      } else {
        Log.error('mounted=$mounted fn=$fn');
      }
    });
  }
}

/// demo
class TestWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => StateTestWidget();
}

class StateTestWidget extends StateWidget<TestWidget> {
  @override
  void initState() {
    super.initState();
  }
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    throw UnimplementedError();
  }
}

2、弹窗showDialog组件回调函数引发父组件异常

弹窗showDialog组件父组件被卸载,弹窗组件并不会被卸载,此时弹窗组件回调函数可能引发父组件空指针异常

风险写法

安全写法

自定义Dialog

可以自定义Dialog,在每次widget dispose的时候,把widget内场景的Dialog全部销毁,避免父组件已销毁,Dialog未销毁,触发的回调函数引发异常。代码如下所示:

ini 复制代码
class Dialog {
  BuildContext? _context;

  // 显示弹窗
  Future<void> show({
    required BuildContext context,
    required WidgetBuilder builder,
    bool barrierDismissible = true,
    Color? barrierColor = Colors.black54,
    String? barrierLabel,
    bool useSafeArea = true,
    bool useRootNavigator = true,
    RouteSettings? routeSettings,
    Offset? anchorPoint,
  }) async {
    remove();
    await showDialog(
      context: context,
      barrierDismissible: barrierDismissible,
      barrierColor: barrierColor,
      barrierLabel: barrierLabel,
      useSafeArea: useSafeArea,
      useRootNavigator: useRootNavigator,
      routeSettings: routeSettings,
      anchorPoint: anchorPoint,
      builder: (BuildContext context) {
        _context = context;
        return builder(context);
      },
    );
  }

  // 销毁弹窗
  void remove() {
    BuildContext? context = _context;
    if (context != null && context.mounted == true) {
      Navigator.pop(context);
      _context = null;
    }
  }
}

3、非!判断

建议少用非!写法,对null状态做判断,如下图所示:

说明:对于当前非常确定性非空的场景,也可能随着版本迭代的改动,导致非空。

二、函数参数声明

Dart允许在申明函数的时候,不申明函数参数,如下所示:

建议:所有的函数申明,都申明函数参数,如下图所示:

三、List数组index溢出

python 复制代码
RangeError (index): Invalid value: Valid value range is empty: 1

建议:通过index取List数组值的时候,都做index是否溢出判断,如下:

封装扩展isRange函数

四、Stream close()后,继续add()调用,出现异常

sql 复制代码
Bad state: Cannot add new events after calling close

解决方案:可以使用isClosed属性判断Stream是否已经关闭

其实Stream在flutter开发过程中,使用不多,但这里提出来是因为很多三方库内部会使用到Stream进行数据通信。

因此我们调用三方库的接口,需要保障调用顺序,例如:audioplayer dispose()之后,继续调用release()等接口会出现异常。

相关推荐
lichong9515 小时前
【Flutter&Dart】 listView.builder例子二(14 /100)
android·javascript·flutter·api·postman·postapi·foxapi
Sinyu101212 小时前
Flutter 动画实战:绘制波浪动效详解
android·前端·flutter
小小の灰色脑细胞12 小时前
【Flutter】使用ScrollController配合EasyRefresh实现列表预加载:在还未滑动到底部时加载下一页数据
flutter
sunly_12 小时前
Flutter:文章详情页,渲染富文本
android·javascript·flutter
Bonway_Huang12 小时前
Flutter pubspec.yaml 使用方式
flutter
那年星空12 小时前
Flutter 3.x 版本升级实战:让老项目焕发新生
android·flutter·架构
油焖茄子12 小时前
flutter 开启了服务并隐藏后如何关闭
flutter
明似水19 小时前
Flutter Xcode 16+ iOS 18.1 使用image_pickers无法弹出选择图片的视图问题
flutter·cocoa·xcode
sun_weitao1 天前
Flutter使用GestureDetector工具实现手势缩放效果
flutter
张二三1 天前
flutter 开发笔记(九):原生桥接
android·flutter·ios