Flutter/Dart报Blob不是String子类型异常排查与修复
问题根因
抛出type 'Blob' is not a subtype of type 'String'的核心原因是mysql1库的默认返回规则:当查询结果的字符串字段未指定字符集解码时,库会直接以Blob(二进制字节数组)类型返回字段值,而非预期的String类型。
直接对该值做as String强转无效,因为Blob是独立的字节类型,并非String的子类型,没有做字节到字符串的解码逻辑时强转必然触发类型错误。
除了Blob类型问题,现有代码还存在多处会直接阻断运行的bug:
- 构造
BanditData对象时读取分数的key写为score,但createScoreDataStruct方法中存储分数列表的key为scores,运行时会触发key不存在的错误 - 外层遍历
bandits、内层按scores长度取dates[i]时,没有做长度对齐校验,当dates和scores长度不一致时会触发数组越界 FutureBuilder的泛型声明、返回值读取逻辑和getBanditData实际返回的结构不匹配,即使数据拉取成功也无法正确传给图表组件
修复方案
1. 全局解决Blob类型转换问题(推荐)
在数据库连接初始化时配置字符集,开启自动将Blob解析为字符串的开关,一劳永逸解决所有字段的Blob类型问题:
修改Database类的connect方法:
Future<MySqlConnection> connect() async { var settings = ConnectionSettings( // 保留原有数据库连接配置:host、port、用户名、密码、库名等 ); var conn = await MySqlConnection.connect(settings); // 新增以下两行配置 await conn.query("SET NAMES utf8mb4;"); // 设置连接字符集为utf8mb4,支持中文及特殊字符 conn.results.blobsToStrings = true; // 开启结果集自动将Blob类型转换为String return conn; }
如果不想修改全局配置,也可以在每次读取字符串字段时手动判断类型转换:
// 示例:getBandits方法中读取bandit名称字段时 for (var row in results) { final rawName = row["bandit"]; final banditName = rawName is Blob ? String.fromCharCodes(rawName.toBytes()) : rawName.toString(); items.add({'value': row["ID"], 'label': banditName}); }
所有返回字符串的字段(比如Bane字段、可能返回Blob的时间字段)都可以按这个逻辑做兼容。
2. 修复数据构造逻辑的bug
重写getBanditData方法,修正key拼写错误,增加长度校验避免越界,对齐数据结构:
Future<List<List<BanditData>>> getBanditData() async { final dates = await database.getDistinctDatesList(); final playerScoreStruct = await database.createScoreDataStruct(); List<List<BanditData>> banditData = []; // 直接遍历已构造好的玩家分数结构,无需重复查询bandits列表 for (final player in playerScoreStruct) { List<BanditData> singleLineData = []; final scoreList = player['scores'] as List<int>; // 取日期列表和分数列表的最小长度,避免数组越界 final validLength = dates.length > scoreList.length ? scoreList.length : dates.length; for (int i = 0; i < validLength; i++) { singleLineData.add(BanditData( player['name'] as String, dates[i], scoreList[i] )); } banditData.add(singleLineData); } return banditData; }
3. 修正FutureBuilder的类型匹配
getBanditData返回类型为Future<List<List<BanditData>>>,对应修改FutureBuilder的泛型,后续渲染图表时直接遍历返回的列表为每个玩家生成一条折线系列即可:
// 对应修改FutureBuilder部分 body: FutureBuilder( future: getBanditData(), builder: (context, AsyncSnapshot<List<List<BanditData>>> snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return Center(child: Text('数据加载失败: ${snapshot.error}')); } if (!snapshot.hasData || snapshot.data!.isEmpty) { return const Center(child: Text('暂无数据')); } final allLineData = snapshot.data!; return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Expanded( child: SfCartesianChart( primaryXAxis: CategoryAxis(), // 遍历allLineData,为每个玩家生成一个LineSeries即可 series: allLineData.map((lineData) { return LineSeries<BanditData, String>( dataSource: lineData, xValueMapper: (BanditData data, _) => data.date, yValueMapper: (BanditData data, _) => data.score, name: lineData.first.name, ); }).toList(), ) ) ], ); } )
优化建议
- 现有
insertScore、updateDbActive1、updateDbActive2方法中,conn.close()前没有加await,高并发场景下容易出现数据库连接泄漏,建议补上await getPlayerScoreFromDate中的累计分数逻辑如果符合业务预期可以保留,如果需要展示单日期分数而非累计分数,去掉score += value的累加逻辑直接存单次value即可
内容的提问来源于stack exchange,提问作者bragi
相关产品推荐
相关产品推荐

