赞
踩
- class Overlay extends StatefulWidget{
- ...
- static OverlayState of(
- BuildContext context, {
- bool rootOverlay = false,
- Widget debugRequiredFor,
- })
- ...
- }
- // rootOverlay:
- // 值为false, 就近查找,找到树中最近的节点;
- // 如果为true, 则去找最顶层的节点。
- 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);
- });
- }
-
- }
- class _Theatre extends MultiChildRenderObjectWidget
-
- 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',
- ),
- ),
- ),
- );
- }
- }
Dart 空指针异常 这类错误是在flutter非常常见的异常,如果不加以关注,那么sentry会收集到非常的错误数据。
_CastError Null check operator used on a null value
"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异步函数的场景如下:
- Future、async/await
- Timer
- Stream
- Native Channel
解决方案
出现异常的地方使用 if (mounted) setState(...);
注意:mounted == context.mounted,但使用context前,必须保证context不为null
重写State类
由于setState异常一般不影响应用的时候,但为了解决每次setState都需要做mounted判断的处理,比较繁琐,可以封装一个父类,对setState异常做捕获,并用日志记录。
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(); } }
安全写法
可以自定义Dialog,在每次widget dispose的时候,把widget内场景的Dialog全部销毁,避免父组件已销毁,Dialog未销毁,触发的回调函数引发异常。代码如下所示:
- 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允许在申明函数的时候,不申明函数参数,如下所示:
建议:所有的函数申明,都申明函数参数,如下图所示:
RangeError (index): Invalid value: Valid value range is empty: 1
建议:通过index取List数组值的时候,都做index是否溢出判断,如下:
Bad state: Cannot add new events after calling close
解决方案:可以使用isClosed属性判断Stream是否已经关闭
其实Stream在flutter开发过程中,使用不多,但这里提出来是因为很多三方库内部会使用到Stream进行数据通信。
因此我们调用三方库的接口,需要保障调用顺序,例如:audioplayer dispose()之后,继续调用release()等接口会出现异常。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。