Flutter Socket.io 聊天室开发爬坑日记
上次的文章中使用 socket.io 和 React 实现了一个简易版本的聊天室,文章连接, 今天主要记录下开发配套 Flutter 客户端时踩的一些坑,直接开发 Flutter 应用还是比较吃力。
Demo
demo 截图:
Flutter 连接 Socket.io
文档地址
项目文件增加库
flutter pub add socket_io_client
flutter pub get
+ socket_io_client: ^1.0.2
main.dart 文件中引入
import 'package:socket_io_client/socket_io_client.dart' as IO;
main() {
// Dart client
IO.Socket socket = IO.io('http://localhost:3001');
socket.onConnect((_) {
print('connect');
socket.emit('msg', 'test');
});
socket.on('message', (data) => print(data));
socket.onDisconnect((_) => print('disconnect'));
socket.on('fromServer', (_) => print(_));
}
vscode 提示 socket_io_client 异常
解决方案:
flutter clean
flutter pub get
Socket.io 连接失败
建立连接失败,http 响应如下: {"code":5,"message":"Unsupported protocol version"}
谷歌搜索是因为 Server 版本和 Client 版本支持的版本不一致导致的
stackoverflow.com/questions/6…
目前 Dart 版本的 socket_io_client 正式版本是 1.0.2
方案一:将 socket_io_client 版本升级到最新的测试版
- 修改
pubspec.yaml
文件中的版本 - 重新获取依赖包
- 重新启动就可以连接成功
flutter clean
flutter pub get
方案二:Server 端兼容 EIO3
Server 端初始化的时候配置 allowEIO3
选项兼容
const io = new Server(httpServer, {
/* options */
cors: {
origin: "*",
},
+ allowEIO3: true,
});
最终方案
最开始因为正式版本的 Socket.io 版本是 2.x,准备使用方案二,后面遇到 安卓模拟器无法成功连接,使用方案一解决了,Demo 中先使用方案一,后续在解决方案二的问题。
Flutter 发送消息异常
emit 方法只有两个参数
_socket.emit("message", jsonEncode(msg), () {
// notification.success({ message: "消息发送成功" });
// form.resetFields();
// inputRef.current.focus();
});
需要删除最后一个回调函数
_socket.emit("message", jsonEncode(data));
Flutter 发送消息后 nodejs 服务异常
调试报错提示
Uncaught TypeError TypeError: callback is not a function
at <anonymous> (e:\Github\flutter\flutter-app-demo\socket\server\index.js:30:5)
at emit (node:events:527:28)
报错的代码
socket.on("message", (arg, callback) => {
// TODO: 通过 Redis 或者 Kafaka 消息队列多进程处理
const msg = JSON.parse(arg);
socketList.publish(msg);
callback("got it");
});
删除 callback 回调可以解决
socket.on("message", (arg, callback) => {
// TODO: 通过 Redis 或者 Kafaka 消息队列多进程处理
const msg = JSON.parse(arg);
socketList.publish(msg);
- callback("got it");
});
StatelessWidget props 传递
组件定义
class MsgList extends StatelessWidget {
const MsgList({Key? key, required this.list}) : super(key: key);
final List<Map> list;
@override
Widget build(BuildContext context) {
return ListView(
children: list.map((it) {
return Text(it['message']);
}).toList(),
);
}
}
调用组件传递 props
MsgList(list: _list)
注意事项
1. 如果不声明成 required 的话需要使用默认值
const MsgList({Key? key, required this.list}) : super(key: key);
final List<Map> list;
String 类型 prop 下面两种方式都可以
const MsgList({Key? key, required this.userId}) : super(key: key);
const MsgList({Key? key, this.userId = ''}) : super(key: key);
List 类型的默认值该如何设置???
StatefulWidget props 传递
组件定义
class MsgForm extends StatefulWidget {
const MsgForm({
Key? key,
this.connected = false,
}) : super(key: key);
final bool connected;
@override
_MsgFormState createState() => _MsgFormState();
}
class _MsgFormState extends State<MsgForm> {
final _formKey = GlobalKey<FormState>();
_onPressed() {
if (_formKey.currentState!.validate()) {
print('onPressed');
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: <Widget>[
TextFormField(
minLines: 1,
maxLines: 3,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter some text';
}
return null;
},
),
Row(children: <Widget>[
Expanded(
child: Row(
children: [
Text(widget.connected ? "连接正常" : "正在重连..."),
],
),
),
ElevatedButton(
onPressed: _onPressed,
child: const Text('发送'),
),
]),
],
),
);
}
}
调用组件传递 props
MsgForm(connected: _connected)
注意事项
1. 创建 State 的子类时 State 后面的 StatefulWidget 子类不能忘记
创建 State 的子类时 State 后面的子类不带会导致无法获取到父组件传递过来的 prop,示例中会提示 widget.connected
未定义
class _MsgFormState extends State<MsgForm> {
// ...
}
Dart 语言特性相关问题
1. 文件名使用小写字母加下划线额方式
ChatRoom.dart 需要修改为 chat_room.dart
引用代码如下所示:
import './chat_room.dart';
2. 成员变量默认值
下面的代码会提示错误 A value of type 'Null' can't be assigned to a variable of type 'Socket'.
IO.Socket _socket = null;
变量前声明加上 late,在使用时再初始化
late IO.Socket _socket;
参考文档还有下面一种方式: dart.dev/null-safety…
IO.Socket? _socket = null;
3. 资源销毁后成员变量值清空
使用 late 方式定义变量在销毁资源时 _socket = null;
会报错 A value of type 'Null' can't be assigned to a variable of type 'Socket'.
late IO.Socket _socket;
void dispose() {
super.dispose();
if (null != _socket) {
_socket.disconnect();
_socket.destroy();
_socket.close();
print('close socket');
_socket = null;
}
}
声明的方式可以改成下面的方式
IO.Socket? _socket = null;
这样声明会带来另外一个问题,使用 _socket
时都需要加上 !
运算符,暂时还是使用 late 的方式,不知道是不是有更优雅的方式???
@override
void dispose() {
super.dispose();
_socket.disconnect();
_socket.destroy();
_socket.close();
print('close socket');
// if (null != _socket) {
// _socket.disconnect();
// _socket.destroy();
// _socket.close();
// print('close socket');
// _socket = null;
// }
}
4. async /await
Future<int> futureInt() async {
// 1
return 1;
}
Flutter 表单处理
目前看 Fluter 表单数据只能通过类似 React 受控组件的方式实现,Form 组件只是实现了 validator
reset
initialValue
这些能力
String _msg = '';
// ...
TextFormField(
minLines: 1,
maxLines: 3,
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter some text';
}
return null;
},
onChanged: (val) {
_msg = val;
},
),
安卓模拟器连接失败
模拟器中自带的谷歌浏览器基本没法使用,安装一个 via 浏览器进行测试
方案一:命令行启动,配置 dns-server(无效)
参考这个文档启动
cd C:\Users\admin\AppData\Local\Android\Sdk\emulator
.\emulator.exe -avd Pixel_XL_API_31_1 -dns-server 223.5.5.5
这样启动模拟 wifi 标志上的感叹号没有了
使用 via 访问 PC 上的 3000 端口: http://10.0.2.2:3000
网页可以打开,socket.io 连接也正常
访问 pc 本机地址: http://192.168.16.119:3000
连接也是正常的,但是 socket.io 还是连不上
方案二:配置 android:usesCleartextTraffic="true"
(无效)
参考文档修改 AndroidManifest.xml 文件
<application
android:label="flutter_app"
android:name="${applicationName}"
+ android:usesCleartextTraffic="true"
android:icon="@mipmap/ic_launcher">
修改重启后 socket.io 还是连不上
方案三:Socket.io 初始化参数(成功)
第一步,升级库版本
cupertino_icons: ^1.0.2
- socket_io_client: ^1.0.2
+ # socket_io_client: ^1.0.2
+ socket_io_client: ^2.0.0-beta.4-nullsafety.0
logger: ^1.1.0
第二步,配置参数 socket.io 参数
- _socket = IO.io("http://192.168.16.119:3001/");
+ _socket = IO.io("http://192.168.16.119:3001/",
+ IO.OptionBuilder().setTransports(['websocket']).build());
修改后重新启动可以连接成功,也能够成功发送消息
转载自:https://juejin.cn/post/7107159133488414756