Flutter 最常出现的错误

2023-10-27

哔哩哔哩漫画APP实践Flutter也有大半年时间了,我针对线上收集到的错误进行分析,挑选出了一些有一般代表性的错误,列在本文,可供实践 Flutter 的初学者们作为一点参考。

典型错误一:无法掌握的Future

典型错误信息:NoSuchMethodError: The method 'markNeedsBuild' was called on null.

这个错误常出现在异步任务(Future)处理,比如某个页面请求一个网络API数据,根据数据刷新 Widget State。

异步任务结束在页面被pop之后,但没有检查State 是否还是 mounted,继续调用 setState 就会出现这个错误。

示例代码

一段很常见的获取网络数据的代码,调用 requestApi(),等待Future从中获取response,进而setState刷新 Widget:

1
2
3
4
5
6
7
8
9
10
class AWidgetState extends State<AWidget> {
  // ...
  var data;
  void loadData() async {
    var response = await requestApi(...);
    setState((){
      this.data = response.data;
    })
  }
}

原因分析

response 的获取为async-await异步任务,完全有可能在AWidgetState被 dispose之后才等到返回,那时候和该State 绑定的 Element 已经不在了。故而在setState时需要容错。

解决办法: setState之前检查是否 mounted

1
2
3
4
5
6
7
8
9
10
11
12
class AWidgetState extends State {
  // ...
  var data;
  void loadData() async {
    var response = await requestApi(...);
    if (mounted) {
      setState((){
        this.data = response.data;
      })
    }
  }
}

这个mounted检查很重要,其实只要涉及到异步还有各种回调(callback),都不要忘了检查该值。

比如,在 FrameCallback里执行一个动画(AnimationController):

1
2
3
4
5
6
@override
void initState(){
  WidgetsBinding.instance.addPostFrameCallback((_) {
    if (mounted) _animationController.forward();
  });
}

AnimationController有可能随着 State 一起 dispose了,但是FrameCallback仍然会被执行,进而导致异常。

又比如,在动画监听的回调里搞点事:

1
2
3
4
5
6
7
8
@override
void initState(){
  _animationController.animation.addListener(_handleAnimationTick);
}

void _handleAnimationTick() {
  if (mounted) updateWidget(...);
}

同样的在_handleAnimationTick被回调前,State 也有可能已经被dispose了。

如果你还不理解为什么,请仔细回味一下Event loop 还有复习一下 Dart 的线程模型。

典型错误二:Navigator.of(context) 是个 null

典型错误信息:NoSuchMethodError: The method 'pop' was called on null.

常在 showDialog 后处理 dialog 的 pop() 出现。

示例代码

在某个方法里获取网络数据,为了更好的提示用户,会先弹一个 loading 窗,之后再根据数据执行别的操作…

1
2
3
4
5
6
7
8
9
10
11
12
13
// show loading dialog on request data
showDialog<void>(
  context: context,
  barrierDismissible: false,
  builder: (_) {
    return Center(
      child: CircularIndicator(),
    );
  },
);
var data = (await requestApi(...)).data;
// got it, pop dialog
Navigator.of(context).pop();

原因分析:

出错的原因在于—— Android 原生的返回键:虽然代码指定了barrierDismissible: false,用户不可以点半透明区域关闭弹窗,但当用户点击返回键时,Flutter 引擎代码会调用 NavigationChannel.popRoute(),最终这个 loading dialog 甚至包括页面也被关掉,进而导致Navigator.of(context)返回的是null,因为该context已经被unmount,从一个已经凋零的树叶上是找不到它的根的,于是错误出现。

另外,代码里的Navigator.of(context) 所用的context也不是很正确,它其实是属于showDialog调用者的而非 dialog 所有,理论上应该用builder里传过来的context,沿着错误的树干虽然也能找到根,但实际上不是那么回事,特别是当你的APP里有Navigator嵌套时更应该注意。

解决办法

首先,确保 Navigator.of(context) 的 context 是 dialog 的context
其次,在 Dialog Widget 外层可以包一个 WillPopScope 处理返回键,或者保险起见检查 null,以应对被手动关闭的情况。

showDialog 时传入 GlobalKey,通过 GlobalKey去获取正确的context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GlobalKey key = GlobalKey();

showDialog<void>(
  context: context,
  barrierDismissible: false,
  builder: (_) {
     // Disallow pop by 'back' button when barrierDismissible is false
    return WillPopScope(
      onWillPop: () => Future.value(false),
      child: KeyedSubtree(
        key: key,
        child: Center(
          child: CircularIndicator(),
        )
      )
    );
  },
);
var data = (await requestApi(...)).data;

if (key.currentContext != null) {
  Navigator.of(key.currentContext)?.pop();
}

key.currentContext 为null意为着该 dialog 已经被dispose,亦即已经从 WidgetTree 中unmount

其实,类似的XXX.of(context)方法在 Flutter 代码里很常见,比如 MediaQuery.of(context)Theme.of(context)DefaultTextStyle.of(context)DefaultAssetBundle.of(context)等等,都要注意传入的context是来自正确节点的,否则会有惊喜在等你。

写 Flutter 代码时,脑海里一定要对context的树干脉络有清晰的认知,如果你还不是很理解context,可以看看 《深入理解BuildContext》 - Vadaski

典型错误三:ScrollController 里薛定谔的 position

在获取ScrollControllerpositionoffset,或者调用jumpTo()等方法时,常出现StateError错误。

错误信息:StateError Bad state: Too many elementsStateError Bad state: No element

示例代码

在某个按钮点击后,通过ScrollController 控制ListView滚动到开头:

1
2
3
4
5
final ScrollController _primaryScrollController = ScrollController();
// 回到开头
void _handleTap() {
  if(_primaryScrollController.offset > 0) _primaryScrollController.jumpTo(0.0)
}

原因分析

先看ScrollController的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ScrollController extends ChangeNotifier {
  //...
  @protected
  Iterable<ScrollPosition> get positions => _positions;
  final List<ScrollPosition> _positions = <ScrollPosition>[];
  
  double get offset => position.pixels;
  
  ScrollPosition get position {
    assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
    assert(_positions.length == 1, 'ScrollController attached to multiple scroll views.');
    return _positions.single;
  }
  //...
}

很明显,ScrollController 的 offest 是从 position 中获得,而position 则是来自变量 _positions

StateError错误,就是_positions.single 这一行抛出:

1
2
3
4
5
6
7
8
9
10
11
abstract class Iterable<E> {
  //...
  E get single {
    Iterator<E> it = iterator;
    if (!it.moveNext()) throw IterableElementError.noElement();
    E result = it.current;
    if (it.moveNext()) throw IterableElementError.tooMany();
    return result;
  }
//...
}

那么问题来了,这个_positions 为什么忽而一滴不剩,忽而却嫌它给的太多了呢?ˊ_>ˋ

还是要回到 ScrollController 的源码里找找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ScrollController extends ChangeNotifier {
  // ...
  void attach(ScrollPosition position) {
    assert(!_positions.contains(position));
    _positions.add(position);
    position.addListener(notifyListeners);
  }

  void detach(ScrollPosition position) {
    assert(_positions.contains(position));
    position.removeListener(notifyListeners);
    _positions.remove(position);
  }
}
  1. 为什么没有数据(No element):
    ScrollController还没有 attach 一个 position。原因有两个:一个可能是还没被 mount 到树上(没有被Scrollable使用到);另外一个就是已经被 detach了。

  2. 为什么多了(Too many elements):
    ScrollController还没来得及 detach旧的 position,就又attach了一个新的。原因多半是因为ScrollController的用法不对,同一时间被多个 Scrollable关注到了。

解决办法

针对 No element 错误,只需判断一下 _positions是不是空的就行了,即hasClients

1
2
3
4
5
final ScrollController _primaryScrollController = ScrollController();
// 回到开头
void _handleTap() {
  if(_primaryScrollController.hasClients && _primaryScrollController.offset > 0) _primaryScrollController.jumpTo(0.0)
}

针对 Too many elements 错误,确保ScrollController只会被一个 Scrollable绑定,别让它劈腿了,且被正确 dispose()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class WidgetState extends State {
  final ScrollController _primaryScrollController = ScrollController();

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _primaryScrollController,
      itemCount: _itemCount,
      itemBuilder: _buildItem,
    )
  }

  int get _itemCount => ...;
  Widget _buildItem(context, index) => ...;

  @override
  void dispose() {
    super.dispose();
    _primaryScrollController.dispose();
  }
}

典型错误四:四处碰壁 null

Dart 这个语言可静可动,类型系统也独树一帜。万物都可以赋值null,就导致写惯了 Java 代码的同志们常常因为bool int double这种看起来是”primitive”的类型被null附体而头晕。

典型错误信息:

  • Failed assertion: boolean expression must not be null
  • NoSuchMethodError: The method '>' was called on null.
  • NoSuchMethodError: The method '+' was called on null.
  • NoSuchMethodError: The method '*' was called on null.

示例代码

这种错误,较常发生在使用服务端返回的数据model时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class StyleItem {
  final String name;
  final int id;
  final bool hasNew;

  StyleItem.fromJson(Map<String, dynamic> json):
    this.name = json['name'],
    this.id = json['id'],
    this.hasNew = json['has_new'];
}

StyleItem item = StyleItem.fromJson(jsonDecode(...));

Widget build(StyleItem item) {
  if (item.hasNew && item.id > 0) {
    return Text(item.name);
  }
  return SizedBox.shrink();
}

原因分析

StyleItem.fromJson() 对数据没有容错处理,应当认为 map 里的value都有可能是 null

解决办法:容错

1
2
3
4
5
6
7
8
9
10
class StyleItem {
  final String name;
  final int id;
  final bool hasNew;

  StyleItem.fromJson(Map<String, dynamic> json):
    this.name = json['name'],
    this.id = json['id'] ?? 0,
    this.hasNew = json['has_new'] ?? false;
}

一定要习惯 Dart 的类型系统,什么都有可能是null,比如下面一段代码,你细品有几处可能报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Test {
  double fraction(Rect boundsA, Rect boundsB) {
    double areaA = boundsA.width * boundsA.height;
    double areaB = boundsB.width * boundsB.height;
    return areaA / areaB;
  }
  
  void requestData(params, void onDone(data)) {
    _requestApi(params).then((response) => onDone(response.data));
  }
  
  Future<dynamic> _requestApi(params) => ...;
}

小提示,onDone()也可以是null >﹏<。

在和原生用 MethodChannel传数据时更要特别注意,小心驶得万年船。

典型错误五:泛型里的 dynamic 一点也不 dynamic

典型错误信息:

  • type 'List<dynamic>' is not a subtype of type 'List<int>'
  • type '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'Map<String, String>'

常发生在给某个List、Map 变量赋值时。

示例代码

这种错误,也较常发生在使用服务端返回的数据model时。

1
2
3
4
5
6
7
8
9
10
11
class Model {
  final List<int> ids;
  final Map<String, String> ext;

  Model.fromJson(Map<String, dynamic> json):
    this.ids = json['ids'],
    this.ext= json['ext'];
}

var json = jsonDecode("""{"ids": [1,2,3], "ext": {"key": "value"}}""");
Model m = Model.fromJson(json);

原因分析

jsonDecode()这个方法转换出来的map的泛型是Map<String, dynamic>,意为 value 可能是任何类型(dynamic),当 value 是容器类型时,它其实是List<dynamic>或者Map<dynamic, dynamic>等等。

而 Dart 的类型系统中,虽然dynamic可以代表所有类型,在赋值时,如果数据类型事实上匹配(运行时类型相等)是可以被自动转换,但泛型里 dynamic 是不可以自动转换的。可以认为 List<dynamic> 和 List<int>是两种运行时类型

解决办法:使用 List.from, Map.from

1
2
3
4
5
6
7
8
class Model {
  final List<int> ids;
  final Map<String, String> ext;

  Model.fromJson(Map<String, dynamic> json):
    this.ids = List.from(json['ids'] ?? const []),
    this.ext= Map.from(json['ext'] ?? const {});
}

总结

综上所述,这些典型错误,都不是什么疑难杂症,而是不理解或者不熟悉 Flutter 和 Dart 语言所导致的,关键是要学会容错处理。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Flutter 最常出现的错误 的相关文章

  • 【MySQL】免安装版MySQL安装教程

    前言 近日 重新安装了一下本地的数据库 参考了很多博客才将MySQL给安装好 为了方便以后安装 便结合了网上博客的安装方法以及自己的一些经验写下这篇博客 也希望能给你们带来帮助 一 MySQL是什么 MySQL 是一个关系型数据库管理系统
  • docker配置seata分布式事务并注册至nacos

    Docker配置分布式事务Seata并注册到Nacos服务中心 Docker常用基础命令 docker ps 查询当前服务器的服务 docker images 查看服务下载的镜像 docker ps a 查看所有的服务 包含未开启的服务 d
  • 【Protobuf(四)】消息格式

    protobuf是一种平台语言无关的消息序列化协议 相比于传统的json xml 序列后的空间更小 但是无法自解释 需要结合额外的proto定义文件才能反序列化 当然这样也更安全 下面记录一下protobuf消息格式 protobuf消息序
  • 统计与分布之伯努利分布与二项分布

    目录 目录 前文列表 伯努利分布 二项分布 前文列表 计数原理 组合与排列 统计与分布之高斯分布 统计与分布之泊松分布 伯努利分布 伯努利分布 Bernoulli Distribution 是一种离散分布 又称为 0 1 分布 或 两点分布
  • 测试用例编写

    今天主要编写了办公模块下快速创建板块中的测试用例 快速创建提日志 多个功能点钟含有相同的功能 而且功能以模块的形式呈现 包含的功能较多 可以把该模块提取出来单独编写测试用例 这样就不用再每个功能点下重复编写 在快速创建日志中 日报 周报 月
  • The kdb Kernel Debugger

    The kdb Kernel Debugger Many readers may be wondering why the kernel does not have any more advanced debugging features
  • leetcode:62. 不同路径

    题目来源 leetcode 题目描述 题目解析 从暴力搜索到动态规划 暴力搜索 class Solution 机器人从 i j 走到 m n 一共有几种方法 int process int i int j int m int n if i
  • ASP + SQL Server聊天室设计实例

    ASP SQL Server聊天室设计实例 目 录 第一章 绪论 1 1 设计思想 1 2 开发工具和相关技术简介 第二章 聊天室总体分析和设计 2 1 聊天室的运行原理 2 2 聊天室的功能 2 3 聊天室的页面结构设计 2 4 聊天室的
  • hive之full outer join(全连接)使用方法

    目录 介绍 语法 例子 创建顾客表 customers 创建订单表 orders full outer join语句 left join union right join语句 介绍 full outer join结合了 LEFT JOIN
  • vue 指定元素滚动到页面可视区域

    使用场景 1 点击页面tab 或步骤条的某一步 使其对应元素滚动到页面可视区域 2 使遍历而来的list滚动到页面可视区域 实现 使用el scrollIntoView API实现 scrollIntoView 方法会滚动元素的父容器 使被
  • db2 replace函数的用法_高效的10个Pandas函数,你都用过吗?

    作者 Soner Y ld r m 来源 towardsdatascience 翻译 编辑 Python大数据分析 Pandas是python中最主要的数据分析库之一 它提供了非常多的函数 方法 可以高效地处理并分析数据 让pandas如此
  • 类模板 构造函数_C++ 类模板(学习笔记:第9章 02)

    类模板 1 类模板的作用 使用类模板使用户可以为类声明一种模式 使得类中的某些数据成员 某些成员函数的参数 某些成员函数的返回值 能取任意类型 包括基本类型的和用户自定义类型 类模板的声明 类模板 template lt 模板参数表 gt
  • oracle 查询随机数据结构,批量随机键值查询测试

    摘要 当数据量巨大时 使用大批量随机键值集获取对应记录集合 不仅仅考验数据库软件本身 更在于程序员对数据的理解 如何在硬件资源有限的情况下将性能发挥到极致 点击 批量随机键值查询测试 来乾学院一探究竟 本次测试主要针对集算器组表索引实现的批

随机推荐

  • 15个 Android 通用流行框架

    转载自 http www techug com 15 android framework biz MjM5OTA1MDUyMA mid 407358558 idx 2 sn b21877f23bf4063fa311185009c1f0b7
  • PLSQL显示优化

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 首选项 Tools gt Preferences 1 字体大小调整 高分屏看的清 2 设置关键字自动大写 3 设置关键字颜色 实现效果 转载于 https my oschi
  • kdj超卖_KDJ买卖口诀:“J值大于100逐步卖,J值接近负值逐步买”,从贫穷到富有原来如此简单...

    KDJ指标又叫随机指标 是一种相当新颖 实用的技术分析指标 指标构成 K线是快速确认线 数值在90以上就是超买 数值在10以下就是超卖 D线是慢速主干线 数值在80以上就是超买 数值在20一下就是超卖 J线是方向敏感线 当J值大于100 特
  • 编程小记—— C/C++中 x & -x 表示含义

    说明 看多了各种优秀看源代码的经常会遇到一些很常见的公式 本篇文章记录的 x x 就是其中的一种 含义 我们都知道 x 的值 其实就是在x的值的基础上进行按位取反 x 之后在增加1所得 也就是说 x x x x 1 x 为偶数 我们都知道
  • web信息收集

    title 信息收集 tags null categories 信息收集 null date 2021 03 20 18 40 54 keywords top img cover updated sticky description cop
  • 用 request请求对象 获取请求头里的 信息

    1 根据请求头名称获取一个值 String connection request getHeader connection System out println connection System out println getHeader
  • 8个重构技巧使得Python代码更Pythonic

    1 合并追加到列表声明 我们从一个简单的开始 不是声明一个空列表然后附加到它 而是直接用所有元素初始化列表 这缩短了代码并使意图更加明确 它的性能也稍微好一些 因为它避免了对 append 的函数调用 这同样适用于填充其他集合类型 如集合和
  • Angular4.0_开发准备

    启动Angular过程介绍 启动时加载了哪个页面 启动时加载了哪些脚本 这些脚本做了什么事 默认情况下是index对应的文件是启动时加载的页面 main ts是启动时的起点文件 main ts 核心模块提供的enableProdMode用来
  • 多媒体指令(灰度像素最大值)

    如果不是处理的灰度图像 那么最大值也就没什么意思了 彩色图也可以转成灰度图嘛 虽然用了汇编 不过没有使用多媒体指令 灰度图像的RGB都一样 没必要使用mmx寄存器了 直接对单个字节处理就行了 获得最小值和获得最大值原理一样 只需改一个指令
  • 基础指南 之 归并排序

    归并排序 两个有序数组的归并 数组 a 和数组 b 都是非降序的数组 数组长度分别为 m 和 n 将两个数组合并成一个升序数组 c 程序如下所示 void merge int a int m int b int n int c int i
  • com.rabbitmq.client.ShutdownSignalException: connection error;连接rabbitMQ失败

    com rabbitmq client ShutdownSignalException connection error 连接rabbitMQ失败 大概率原因是权限不足 rabbitmqctl set permissions p admin
  • 51单片机-LED篇

    目录 准备工作 点亮一个LED灯 写程序 烧录 LED闪烁 延时代码Delay500ms 烧录 LED流水灯 代码 对LED流水灯代码进行优化 增加复用性 延时代码 代码 准备工作 使用到的单片机是普中51单片机 使用到的软件是Keil u
  • 写作副业怎么弄?写文章的副业应该怎么做?

    现在越来越流行 斜杠青年 这个词了 人们总是希望在做好本职工作的基础上 还能够有另外一份获取收入的工作 也就是 副业 而在 副业 的众多选项里 很多人都看好 写作 这一项 但是 当我们普通人想要开启写作之路 赚取副业收入的时候 具体应该怎么
  • 用户友好性检测

    我们一般通过三个指标来检验一个网站是否对于用户友好 这三个指标分别是 链接的可用性 访问速度体验和查找信息的便捷度 一 链接的可用性 试想 一个访问者来到你的网站 点击一个超级链接 却发现浏览器只返回一个错误404 页面 如果网页中不可用链
  • Unity3D 引擎学习2022资料整理(二)

    Utils C APR Apache Portable Runtime 另一个跨平台的实用函数库 Apache2 0 官网 C Algorithms 一个常用算法和数据结构的集合 官网 CPL The Common Pipeline Lib
  • edge浏览器受信任_Edge 浏览器如何添加信任站点

    Microsoft Edge 无法添加信任站点 组策略没有批量设置 只能逐条设置 然后从DC推到所向域内客户端 如果你是用Site to Zone Assignment List Enabled策略或来设置信任站点的话 客户端确实无法手动添
  • OpenHarmony之docker容器的基本用法

    Docker使用示例 docker移植至OpenHarmony的过程可参考 https blog 51cto com u 14601312 5692202 下面以rk3568 OpenHarmony为例 介绍一下如何进行容器制作 导入及使用
  • 一招解决报错:pyassimp.errors.AssimpError: assimp library not found

    文章目录 1 问题描述 2 原因分析 3 解决方法 1 问题描述 在使用pip install pyassimp安装pyassimp库后 调用时会出现错误 File root anaconda3 envs kgn lib python3 8
  • qt5.12.10 在linux(国产系统)的源码编译、移植问题记录

    1 概述 Qt版本 Qt5 12 10 Qt 官网下载地址 Qt官网 路径 Qt5 12 10源码目录目录下下载 qt everywhere src 5 12 10 tar xz 编译平台 方德 其余架构亦可考 2 编译源码记录 1 下载源
  • Flutter 最常出现的错误

    哔哩哔哩漫画APP实践Flutter也有大半年时间了 我针对线上收集到的错误进行分析 挑选出了一些有一般代表性的错误 列在本文 可供实践 Flutter 的初学者们作为一点参考 典型错误一 无法掌握的Future 典型错误信息 NoSuch