flutter 弹窗之系列三

Overlay 部分源码

bash 复制代码
class Overlay extends StatefulWidget{
    ...
    static OverlayState of(
        BuildContext context, {
        bool rootOverlay = false,
        Widget debugRequiredFor,
      })
    ...
}
// rootOverlay: 
// 值为false, 就近查找,找到树中最近的节点; 
// 如果为true, 则去找最顶层的节点。
bash 复制代码
class OverlayState extends State<Overlay> with TickerProviderStateMixin {
    /// 存储所有的OverlayEntry
    final List<OverlayEntry> _entries = <OverlayEntry>[];

    /// 计算OverlayEntry的插入位置
    int _insertionIndex(OverlayEntry below, OverlayEntry above) {
        if (below != null) return _entries.indexOf(below);
        if (above != null) return _entries.indexOf(above) + 1;
        return _entries.length;
    }

    /// 添加一个OverlayEntry, 在`_insertionIndex(below, above)`
    /// OverlayEntry里可以放个[Positioned]来确定位置。
    void insert(OverlayEntry entry, { OverlayEntry below, OverlayEntry above })

    /// 同insert,添加多个
    void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry below, OverlayEntry above })

    /// 更新当前的Overlayentry。将newEntries更新旧有的部分
    /// 将旧有未更新的部分,添加到`_insertionIndex(below, above)`
    void rearrange(Iterable<OverlayEntry> newEntries, { OverlayEntry below, OverlayEntry above }){
        final old = LinkedHashSet<OverlayEntry>.from(_entries);
    
        setState(() {
          _entries..clear()..addAll(newEntriesList);
          old.removeAll(newEntriesList);
          _entries.insertAll(_insertionIndex(below, above), old);
        });
    }

}
bash 复制代码
class _Theatre extends MultiChildRenderObjectWidget

自定义弹出框 OverlayEntry

bash 复制代码
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {


  // 自定义Toast
   void _showOverlay({
    required String message,
  }) {
    // 创建一个OverlayEntry对象
    OverlayEntry overlayEntry = OverlayEntry(builder: (context) {
      // 外层使用Position进行定位,控制在Overlay中的位置
      return Positioned(
        top: MediaQuery.of(context).size.height * 0.7,
        child: Material(
          child: Container(
            width: MediaQuery.of(context).size.width,
            alignment: Alignment.center,
            child: Center(
              child: Card(
                color: Colors.grey,
                child: Padding(
                  padding: EdgeInsets.all(8),
                  child: Text(message),
                ),
              ),
            ),
          ),
        ),
      );
    });
    // 往Overlay中插入OverlayEntry
    Overlay.of(context).insert(overlayEntry);
    // 两秒后,移除Toast
    Future.delayed(const Duration(seconds: 2)).then((value) => overlayEntry.remove());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: GestureDetector(
          onTap: () {
            _showOverlay(message: "消息");
          },
          child: const Text(
            '\n显示Overlay\n',
          ),
        ),
      ),
    );
  }
}

避免常见sentry异常的编码技巧

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

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

setState调用异常

原因分析

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

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

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

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

解决方案

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

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

重写State类

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

bash 复制代码
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();
  }
}

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

安全写法

自定义Dialog

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

bash 复制代码
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;
    }
  }
}

非!判断

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

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

函数参数声明

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

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

List数组index溢出

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

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

封装扩展isRange函数

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

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

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

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

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

相关推荐
Geek_Vison2 小时前
保险APP已经用了Flutter,还能接入小程序吗?
flutter·小程序·uni-app·mpaas·小程序容器
FungLeo2 小时前
成为全栈·Flutter App 篇·go_router 与四 Tab App Shell:保留导航状态、深链和登录回跳
flutter·移动开发·成为全栈·go_router·路由设计·深链接
m0_738185822 小时前
Flutter 鸿蒙化实战:screen_protector 适配 OpenHarmony,防截屏/防泄露
flutter·华为·harmonyos·鸿蒙
梦想的颜色8 小时前
【编程实战】AI 时代 APP 开发全栈硬核指南:技术选型 + AI 架构 + 模型落地全维度决策
python·flutter·react native·react.js·ai·桌面应用·milvus
yuanlaile1 天前
2026年全新Flutter 跨平台开发全栈学习资料整理
flutter·getx·鸿蒙 next·flutter开发鸿蒙·flutter入门实战·flutter全栈
恋猫de小郭2 天前
Genkit Dart 1.0 发布,Flutter 原生的 AI Agent 终于完整了
android·前端·flutter
第10086个小白2 天前
flutter桌面端打包方式(Inno Setup Compiler)版本
前端·flutter
陆断枫2 天前
Flutter 3.47 动画引擎盘点:Impeller 全平台,和那些"新特性"
flutter
程序员老刘2 天前
Flet 1.0 思路很好,可惜来晚了
flutter·ai编程·客户端
m0_738185823 天前
Flutter 鸿蒙化实战:qr_code_scanner_plus 适配 OpenHarmony,二维码扫描
flutter·华为·harmonyos·鸿蒙