Flutter Socket.io 聊天室开发爬坑日记

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

上次的文章中使用 socket.io 和 React 实现了一个简易版本的聊天室,文章连接, 今天主要记录下开发配套 Flutter 客户端时踩的一些坑,直接开发 Flutter 应用还是比较吃力。

Demo

源码地址:github.com/goblin-labo…

demo 截图:

image.png

Flutter 连接 Socket.io

文档地址

pub.dev/packages/so…

项目文件增加库

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…

扫描二维码关注公众号,回复: 14253349 查看本文章

目前 Dart 版本的 socket_io_client 正式版本是 1.0.2

方案一:将 socket_io_client 版本升级到最新的测试版

  • 修改 pubspec.yaml 文件中的版本
  • 重新获取依赖包
  • 重新启动就可以连接成功
flutter clean
flutter pub get
复制代码

方案二:Server 端兼容 EIO3

Server 端初始化的时候配置 allowEIO3 选项兼容

socket.io/docs/v3/ser…

  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(无效)

参考这个文档启动

www.jianshu.com/p/cb738ad17…

 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"(无效)

reactnative.dev/docs/networ…

参考文档修改 AndroidManifest.xml 文件

   <application
        android:label="flutter_app"
        android:name="${applicationName}"
+       android:usesCleartextTraffic="true"
        android:icon="@mipmap/ic_launcher">
复制代码

修改重启后 socket.io 还是连不上

方案三:Socket.io 初始化参数(成功)

github.com/rikulo/sock…

解决方案:github.com/rikulo/sock…

第一步,升级库版本

    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());
复制代码

修改后重新启动可以连接成功,也能够成功发送消息

猜你喜欢

转载自juejin.im/post/7107159133488414756