Flutter空安全迁移报错:Null不是String类型子类型
问题定位
你遇到的两个问题根源如下:
- 第一个
type 'Null' is not a subtype of type 'String'报错,和你加的空安全运算符无关,是测试代码构造User对象时传入的JSON字段和User.fromJson的读取逻辑不匹配:- 缺少
username、photo_url两个非空必填字段 - 最后上线时间的key写为
lastSeen,和fromJson里读取的last_seen不匹配
三个非空字段从JSON里取到的都是null,自然会触发类型转换错误。
- 缺少
- 硬编码id后测试挂起无响应,有三个原因:
- 你在MessageService的
messages方法里对可空参数activeUser直接做!强制解包是错误写法,入参为空时会直接抛运行时异常 - 测试逻辑没有正确初始化、释放RethinkDB连接和流订阅,后台常驻的流监听会阻止测试进程退出
- 没有给异步操作加超时,数据库连接异常时会无限等待
- 你在MessageService的
- 你之前做的空安全适配里,只有给
_changefeed加late修饰是正确的,另外两处!属于不考虑边界情况的暴力解包,会埋下运行时崩溃隐患。
修复方案
1. 修正User类与测试JSON的字段匹配问题
首先补全测试中构造User的JSON字段,修正key名:
final user = User.fromJson({ 'id': '1234', 'username': 'test_user', 'photo_url': 'https://test.com/avatar.png', 'active': true, 'last_seen': DateTime.now().toIso8601String(), });
同时优化User类的空安全适配,增加空值兜底,修正toJson方法缺失id字段、DateTime直接序列化的问题:
import 'package:flutter/foundation.dart'; class User { String? id; final String username; final String photoUrl; final bool active; final DateTime lastseen; User({ required this.username, required this.photoUrl, required this.active, required this.lastseen, this.id, }); Map<String, dynamic> toJson() => { 'id': id, 'username': username, 'photo_url': photoUrl, 'active': active, 'last_seen': lastseen.toIso8601String(), }; factory User.fromJson(Map<String, dynamic> json) { return User( id: json['id'] as String?, username: json['username'] as String? ?? '', photoUrl: json['photo_url'] as String? ?? '', active: json['active'] as bool? ?? false, lastseen: json['last_seen'] is DateTime ? json['last_seen'] : DateTime.tryParse(json['last_seen'].toString()) ?? DateTime.now(), ); } }
2. 修正MessageService的错误空安全写法
去掉messages方法里对activeUser的暴力强制解包,增加空判断,同时在启动消息监听前校验用户id是否为空:
@override Stream<Message> messages({User? activeUser}) { if (activeUser != null) { _startReceivingMessages(activeUser); } return _controller.stream; } // 对应修改_startReceivingMessages的入口判断 _startReceivingMessages(User user) { if (user.id == null) return; _changefeed = r .table('messages') .filter({'to': user.id}) .changes({'include_initial': true}) .run(_connection) .asStream() .cast<Feed>() .listen((event) { event .forEach((feedData) { if (feedData['new_val'] == null) return; final message = _messageFromFeed(feedData); _controller.sink.add(message); _removeDeliveredMessage(message); }) .catchError((err) => print(err)) .onError((error, stackTrace) => print(error)); }); }
3. 修正测试用例的资源初始化与释放逻辑
测试必须在启动前初始化数据库连接、建表,结束后释放连接和服务资源,同时给异步操作加超时避免无限挂起,参考结构如下:
import 'package:rethink_db_ns/rethink_db_ns.dart'; import 'package:test/test.dart'; // 引入对应model、service文件 void main() { final r = RethinkDb(); late Connection connection; late MessageService sut; setUp(() async { // 确保本地RethinkDB服务已启动,默认端口28015 connection = await r.connect(host: 'localhost', port: 28015); // 测试前自动建表,已存在则捕获异常跳过 await r.db('test').tableCreate('messages').run(connection).catchError((e) {}); sut = MessageService(r, connection); }); tearDown(() async { await sut.dispose(); await connection.close(); }); test('sent message successfully', () async { final user = User.fromJson({ 'id': '1234', 'username': 'test_user', 'photo_url': 'https://test.com/avatar.png', 'active': true, 'last_seen': DateTime.now().toIso8601String(), }); Message message = Message( from: user.id!, to: '3456', timestamp: DateTime.now(), contents: 'this is a message', ); // 5秒超时,避免连接异常时测试无限挂起 final res = await sut.send(message).timeout(const Duration(seconds: 5)); expect(res, true); }); }
注意:跑测试前必须确认本地RethinkDB服务处于正常运行状态,否则数据库连接会一直等待,表现为测试无响应。
内容的提问来源于stack exchange,提问作者JohnBradens
相关产品推荐
相关产品推荐

