Flutter毗连websocket、实现在线谈天功能

干翻全岛蛙蛙  金牌会员 | 2024-9-17 15:19:29 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 664|帖子 664|积分 1992

老规矩结果图:

第一步:引入
  1. web_socket_channel: ^2.4.0
复制代码
第二步:封装 websocket.dart 单例
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'package:web_socket_channel/web_socket_channel.dart';
  4. import 'package:web_socket_channel/io.dart';
  5. class WebSocketManager {
  6.   late WebSocketChannel _channel;
  7.   final String _serverUrl; //ws连接路径
  8.   final String _accessToken; //登录携带的token
  9.   bool _isConnected = false; //连接状态
  10.   bool _isManuallyDisconnected = false; //是否为主动断开
  11.   late Timer _heartbeatTimer; //心跳定时器
  12.   late Timer _reconnectTimer; //重新连接定时器
  13.   Duration _reconnectInterval = Duration(seconds: 5); //重新连接间隔时间
  14.   StreamController<String> _messageController = StreamController<String>();
  15.   Stream<String> get messageStream => _messageController.stream; //监听的消息
  16.   //初始化
  17.   WebSocketManager(this._serverUrl, this._accessToken) {
  18.     print('初始化');
  19.     _heartbeatTimer = Timer(Duration(seconds: 0), () {});
  20.     _startConnection();
  21.   }
  22.   //建立连接
  23.   void _startConnection() async {
  24.     try {
  25.       _channel = WebSocketChannel.connect(Uri.parse(_serverUrl));
  26.       print('建立连接');
  27.       _isConnected = true;
  28.       _channel.stream.listen(
  29.         (data) {
  30.           _isConnected = true;
  31.           print('已连接$data');
  32.           final jsonObj = jsonDecode(data); // 将消息对象转换为 JSON 字符串
  33.           if (jsonObj['cmd'] == 0) {
  34.             _startHeartbeat(); //开始心跳
  35.           } else if (jsonObj['cmd'] == 1) {
  36.             _resetHeartbeat(); // 重新开启心跳定时
  37.           } else {
  38.             _onMessageReceived(data);// 其他消息转发出去
  39.           }
  40.         },
  41.         onError: (error) {
  42.           // 处理连接错误
  43.           print('连接错误: $error');
  44.           _onError(error);
  45.         },
  46.         onDone: _onDone,
  47.       );
  48.       _sendInitialData(); // 连接成功后发送登录信息();
  49.     } catch (e) {
  50.       // 连接错误处理
  51.       print('连接异常错误: $e');
  52.       _onError(e);
  53.     }
  54.   }
  55.   //断开连接
  56.   void disconnect() {
  57.     print('断开连接');
  58.     _isConnected = false;
  59.     _isManuallyDisconnected = true;
  60.     _stopHeartbeat();
  61.     _messageController.close();
  62.     _channel.sink.close();
  63.   }
  64.   //开始心跳
  65.   void _startHeartbeat() {
  66.     _heartbeatTimer = Timer.periodic(Duration(seconds: 20), (_) {
  67.       sendHeartbeat();
  68.     });
  69.   }
  70.   //停止心跳
  71.   void _stopHeartbeat() {
  72.     _heartbeatTimer.cancel();
  73.   }
  74.   //重置心跳
  75.   void _resetHeartbeat() {
  76.     _stopHeartbeat();
  77.     _startHeartbeat(); //开始心跳
  78.   }
  79.   // 发送心跳消息到服务器
  80.   void sendHeartbeat() {
  81.     if (_isConnected) {
  82.       final message = {"cmd": 1, "data": {}};
  83.       final jsonString = jsonEncode(message); // 将消息对象转换为 JSON 字符串
  84.       _channel.sink.add(jsonString); // 发送心跳
  85.       print('连接成功发送心跳消息到服务器$message');
  86.     }
  87.   }
  88.   // 登录
  89.   void _sendInitialData() async {
  90.     try {
  91.       final message = {
  92.         "cmd": 0,
  93.         "data": {"accessToken": _accessToken}
  94.       };
  95.       final jsonString = jsonEncode(message); // 将消息对象转换为 JSON 字符串
  96.       _channel.sink.add(jsonString); // 发送 JSON 字符串
  97.       print('连接成功-发送登录信息$message');
  98.     } catch (e) {
  99.       // 连接错误处理
  100.       print('连接异常错误: $e');
  101.       _onError(e);
  102.     }
  103.   }
  104.   //发送信息
  105.   void sendMessage(dynamic message) {
  106.     final data = {
  107.       "cmd":3,
  108.       "data":message
  109.     };
  110.     final jsonString = jsonEncode(data); // 将消息对象转换为 JSON 字符串
  111.     _channel.sink.add(jsonString); // 发送 JSON 字符串
  112.   }
  113.   // 处理接收到的消息
  114.   void _onMessageReceived(dynamic message) {
  115.     print(
  116.         '处理接收到的消息Received===========================================: $message');
  117.     _messageController.add(message);
  118.   }
  119.   //异常
  120.   void _onError(dynamic error) {
  121.     // 处理错误
  122.     print('Error: $error');
  123.     _isConnected = false;
  124.     _stopHeartbeat();
  125.     if (!_isManuallyDisconnected) {
  126.       // 如果不是主动断开连接,则尝试重连
  127.       _reconnect();
  128.     }
  129.   }
  130.   //关闭
  131.   void _onDone() {
  132.     print('WebSocket 连接已关闭');
  133.     _isConnected = false;
  134.     _stopHeartbeat();
  135.     if (!_isManuallyDisconnected) {
  136.       // 如果不是主动断开连接,则尝试重连
  137.       _reconnect();
  138.     }
  139.   }
  140.   // 重连
  141.   void _reconnect() {
  142.     // 避免频繁重连,启动重连定时器
  143.     _reconnectTimer = Timer(_reconnectInterval, () {
  144.       _isConnected = false;
  145.       _channel.sink.close(); // 关闭之前的连接
  146.       print('重连====================$_serverUrl===$_accessToken');
  147.       _startConnection();
  148.     });
  149.   }
  150. }
复制代码
第三步:chat.dart编写静态页面
  1. //在线聊天
  2. import 'dart:convert';
  3. import 'package:flutter/cupertino.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:get/get.dart';
  6. import 'package:zhzt_estate/library/websocket/websocket.dart';
  7. import 'package:zhzt_estate/home/house_detail_page.dart';
  8. import '../library/network/network.dart';
  9. import '../mine/models/userinfo.dart';
  10. import 'models/chat.dart';
  11. class Message {
  12.   final String type;
  13.   final String sender;
  14.   final String? text;
  15.   final Map? cardInfo;
  16.   Message({required this.sender, this.text, required this.type, this.cardInfo});
  17. }
  18. //文字信息==============================================================================
  19. class Bubble extends StatelessWidget {
  20.   final Message message;
  21.   final bool isMe;
  22.   Bubble({required this.message, required this.isMe});
  23.   @override
  24.   Widget build(BuildContext context) {
  25.     return Row(
  26.       mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
  27.       children: [
  28.         Visibility(
  29.           visible: !isMe,
  30.           child: const Icon(
  31.             Icons.paid,
  32.             size: 30,
  33.           ),
  34.         ),
  35.         Container(
  36.           margin: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
  37.           padding: const EdgeInsets.all(10.0),
  38.           decoration: BoxDecoration(
  39.             color: isMe ? Colors.blue : Colors.grey[300],
  40.             borderRadius: BorderRadius.circular(12.0),
  41.           ),
  42.           child: Text(
  43.             message.text ?? '',
  44.             style: TextStyle(color: isMe ? Colors.white : Colors.black),
  45.           ),
  46.         ),
  47.         Visibility(
  48.           visible: isMe,
  49.           child: const Icon(
  50.             Icons.pages,
  51.             size: 30,
  52.           ),
  53.         )
  54.       ],
  55.     );
  56.   }
  57. }
  58. //卡片================================================================================
  59. class Card extends StatelessWidget {
  60.   final Message message;
  61.   final bool isMe;
  62.   Card({required this.message, required this.isMe});
  63.   @override
  64.   Widget build(BuildContext context) {
  65.     return Row(
  66.       mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
  67.       children: [
  68.         Visibility(
  69.           visible: !isMe,
  70.           child: const Icon(
  71.             Icons.paid,
  72.             size: 30,
  73.           ),
  74.         ),
  75.         SizedBox(child: _CardPage(cardInfo: message.cardInfo ?? {})),
  76.         Visibility(
  77.           visible: isMe,
  78.           child: const Icon(
  79.             Icons.pages,
  80.             size: 30,
  81.           ),
  82.         )
  83.       ],
  84.     );
  85.   }
  86. }
  87. class _CardPage extends StatelessWidget {
  88.   late Map cardInfo;
  89.   _CardPage({required this.cardInfo});
  90.   @override
  91.   Widget build(BuildContext context) {
  92.     return Container(
  93.         width: MediaQuery.of(context).size.width * 0.8,
  94.         margin: EdgeInsets.only(top: 5),
  95.         padding: EdgeInsets.all(5),
  96.         decoration: BoxDecoration(
  97.             color: Colors.white, borderRadius: BorderRadius.circular(12.0)),
  98.         child: Row(
  99.           children: [
  100.             GestureDetector(
  101.                 onTap: () {
  102.                   // Add your click event handling code here
  103.                   // 去详情页
  104.                   Navigator.push(
  105.                     context,
  106.                     MaterialPageRoute(
  107.                       // fullscreenDialog: true,
  108.                       builder: (context) => MyHomeDetailPage(
  109.                           houseId: cardInfo['id'], type: cardInfo['type']),
  110.                     ),
  111.                   );
  112.                 },
  113.                 child: Container(
  114.                   width: 100,
  115.                   height: 84,
  116.                   margin: const EdgeInsets.all(8),
  117.                   decoration: BoxDecoration(
  118.                     color: Colors.blueAccent,
  119.                     // image: DecorationImage(
  120.                     //   image: NetworkImage(
  121.                     //       kFileRootUrl + (cardInfo['styleImgPath'] ?? '')),
  122.                     //   fit: BoxFit.fill,
  123.                     //   repeat: ImageRepeat.noRepeat,
  124.                     // ),
  125.                     borderRadius: BorderRadius.circular(10),
  126.                   ),
  127.                 )),
  128.             GestureDetector(
  129.                 onTap: () {
  130.                   // Add your click event handling code here
  131.                   // 去详情页
  132.                   Navigator.push(
  133.                     context,
  134.                     MaterialPageRoute(
  135.                       // fullscreenDialog: true,
  136.                       builder: (context) => MyHomeDetailPage(
  137.                           houseId: cardInfo['id'], type: cardInfo['type']),
  138.                     ),
  139.                   );
  140.                 },
  141.                 child: Container(
  142.                   alignment: Alignment.topLeft,
  143.                   child: Column(
  144.                     mainAxisAlignment: MainAxisAlignment.start,
  145.                     crossAxisAlignment: CrossAxisAlignment.start,
  146.                     children: [
  147.                       Row(
  148.                         crossAxisAlignment: CrossAxisAlignment.center,
  149.                         children: [
  150.                           Text(
  151.                             cardInfo['name'],
  152.                             style: const TextStyle(fontSize: 18),
  153.                           ),
  154.                         ],
  155.                       ),
  156.                       Row(
  157.                         children: [
  158.                           Text(cardInfo['zoneName'] ?? ''),
  159.                           const Text(' | '),
  160.                           Text('${"mianji".tr} '),
  161.                           Text(cardInfo['area']),
  162.                         ],
  163.                       ),
  164.                       Container(
  165.                         alignment: Alignment.centerLeft,
  166.                         child: Text(
  167.                           '${cardInfo['price'] ?? ''}/㎡',
  168.                           style: const TextStyle(
  169.                               color: Colors.orange, fontSize: 16),
  170.                         ),
  171.                       ),
  172.                     ],
  173.                   ),
  174.                 )), //小标题
  175.           ],
  176.         ));
  177.   }
  178. }
  179. //主页
  180. class CommunicatePage extends StatefulWidget {
  181.   const CommunicatePage({super.key});
  182.   @override
  183.   State<CommunicatePage> createState() => _CommunicatePageState();
  184. }
  185. class _CommunicatePageState extends State<CommunicatePage> {
  186. //变量 start==========================================================
  187.   final TextEditingController _ContentController =
  188.       TextEditingController(text: '');
  189.   /// 输入框焦点
  190.   FocusNode focusNode = FocusNode();
  191.   final List<Message> messages = [
  192.     Message(
  193.         sender: "ta",
  194.         cardInfo: {
  195.           "id": "4",
  196.           "code": "fxhsud",
  197.           "title": "test1",
  198.           "name": "test1",
  199.           "zoneName": null,
  200.           "area": "90",
  201.           "roomType": "2室1厅1卫",
  202.           "directions": ["2"],
  203.           "price": "200.00",
  204.           "type": 2,
  205.           "status": 2,
  206.           "seeCount": null,
  207.           "floorNum": "24/30",
  208.           "styleImgPath":
  209.               "",
  210.           "time": "2022-03-26"
  211.         },
  212.         type: "card"),
  213.     Message(sender: "me", text: "hi!", type: "text"),
  214.     Message(sender: "me", text: "你是?!", type: "text"),
  215.     Message(sender: "ta", text: "hello!", type: "text")
  216.   ];
  217.   var isEmojiShow = false;
  218.   final List unicodeArr = [
  219.     '\u{1F600}',
  220.     '\u{1F601}',
  221.     '\u{1F602}',
  222.     '\u{1F603}',
  223.     '\u{1F604}',
  224.     '\u{1F60A}',
  225.     '\u{1F60B}',
  226.     '\u{1F60C}',
  227.     '\u{1F60D}',
  228.     '\u{2764}',
  229.     '\u{1F44A}',
  230.     '\u{1F44B}',
  231.     '\u{1F44C}',
  232.     '\u{1F44D}'
  233.   ];
  234.   // 创建 Websocket 实例
  235.   final websocket = WebSocketManager(kWsRootUrl, UserInfo.instance.token ?? '');
  236.   initFunc() {
  237.     if (UserInfo.instance.token != null) {
  238.       websocket.messageStream.listen((message) {
  239.         print('接收数据---------------------$message');
  240.         setMsg(message);//接收消息渲染
  241.       });
  242.     }
  243.   }
  244.   //接收消息渲染
  245.   setMsg(data){
  246.     final jsonObj = jsonDecode(data);
  247.     setState(() {
  248.       messages.add(Message(
  249.         sender: 'ta',
  250.         text: data,
  251.         type: 'text',
  252.       ));
  253.     });
  254.   }
  255.   //发送消息
  256.   sendMsg(data){
  257.     websocket.sendMessage({
  258.       "content": data,
  259.       "type": 0,
  260.       "recvId": 6
  261.     });
  262.   }
  263.   pullPrivateOfflineMessage(minId) {
  264.     Network.get('$kRootUrl/message/private/pullOfflineMessage',
  265.         headers: {'Content-Type': 'application/json'},
  266.         queryParameters: {"minId": minId}).then((res) {
  267.       if (res == null) {
  268.         return;
  269.       }
  270.     });
  271.   }
  272. //变量 end==========================================================
  273.   @override
  274.   void initState() {
  275.     initFunc();
  276.     super.initState();
  277.   }
  278.   @override
  279.   void dispose() {
  280.     super.dispose();
  281.     websocket.disconnect();
  282.     _ContentController.dispose();
  283.     focusNode.dispose();
  284.   }
  285.   @override
  286.   Widget build(BuildContext context) {
  287.     // TODO: implement build
  288.     return Scaffold(
  289.         backgroundColor: Color(0xFFebebeb),
  290.         resizeToAvoidBottomInset: true,
  291.         appBar: AppBar(
  292.           title: Text('张三'),
  293.         ),
  294.         body: Stack(alignment: Alignment.bottomCenter, children: [
  295.           ListView.builder(
  296.             itemCount: messages.length,
  297.             itemBuilder: (BuildContext context, int index) {
  298.               return messages[index].type == 'text'
  299.                   ? Bubble(
  300.                       message: messages[index],
  301.                       isMe: messages[index].sender == 'me',
  302.                     )
  303.                   : Card(
  304.                       message: messages[index],
  305.                       isMe: messages[index].sender == 'me',
  306.                     );
  307.             },
  308.           ),
  309.           Positioned(
  310.               bottom: 0,
  311.               child: SingleChildScrollView(
  312.                   reverse: true, // 反向滚动以确保 Positioned 在键盘上方
  313.                   child: Column(children: [
  314.                     Container(
  315.                       width: MediaQuery.of(context).size.width,
  316.                       height: 50,
  317.                       decoration: const BoxDecoration(
  318.                           color: Color.fromRGBO(240, 240, 240, 1)),
  319.                       child: Row(
  320.                         mainAxisAlignment: MainAxisAlignment.spaceAround,
  321.                         children: [
  322.                           const Icon(
  323.                             Icons.contactless_outlined,
  324.                             size: 35,
  325.                           ),
  326.                           SizedBox(
  327.                               width: MediaQuery.of(context).size.width *
  328.                                   0.6, // 添加固定宽度
  329.                               child: TextField(
  330.                                 textAlignVertical: TextAlignVertical.center,
  331.                                 controller: _ContentController,
  332.                                 decoration: const InputDecoration(
  333.                                   contentPadding: EdgeInsets.all(5),
  334.                                   isCollapsed: true,
  335.                                   filled: true,
  336.                                   fillColor: Colors.white,
  337.                                   // 设置背景色
  338.                                   border: OutlineInputBorder(
  339.                                     borderRadius: BorderRadius.all(
  340.                                         Radius.circular(10)), // 设置圆角半径
  341.                                     borderSide: BorderSide.none, // 去掉边框
  342.                                   ),
  343.                                 ),
  344.                                 focusNode: focusNode,
  345.                                 onTap: () => {
  346.                                   setState(() {
  347.                                     isEmojiShow = false;
  348.                                   })
  349.                                 },
  350.                                 onTapOutside: (e) => {focusNode.unfocus()},
  351.                                 onEditingComplete: () {
  352.                                   FocusScope.of(context)
  353.                                       .requestFocus(focusNode);
  354.                                 },
  355.                               )),
  356.                           GestureDetector(
  357.                               onTap: () => {
  358.                                     setState(() {
  359.                                       isEmojiShow =
  360.                                           !isEmojiShow; // 数据加载完毕,重置标志位
  361.                                     })
  362.                                   },
  363.                               child: const Icon(
  364.                                 Icons.sentiment_satisfied_alt_outlined,
  365.                                 size: 35,
  366.                               )),
  367.                           Visibility(
  368.                               visible: _ContentController.text=='',
  369.                               child:
  370.                           GestureDetector(
  371.                               onTap: () {
  372.                               },
  373.                               child: const Icon(
  374.                                 Icons.add_circle_outline,
  375.                                 size: 35,
  376.                               ))
  377.                           ),
  378.                           Visibility(
  379.                               visible: _ContentController.text!='',
  380.                               child:
  381.                               GestureDetector(
  382.                                   onTap: () {
  383.                                     sendMsg(_ContentController.text);
  384.                                   },
  385.                                   child: const Icon(
  386.                                     Icons.send,
  387.                                     color: Colors.blueAccent,
  388.                                     size: 35,
  389.                                   ))
  390.                           )
  391.                         ],
  392.                       ),
  393.                     ),
  394.                     Visibility(
  395.                         visible: isEmojiShow,
  396.                         child: Container(
  397.                             width: MediaQuery.of(context).size.width,
  398.                             height: 200,
  399.                             decoration:
  400.                                 const BoxDecoration(color: Colors.white),
  401.                             child: SingleChildScrollView(
  402.                               scrollDirection: Axis.vertical,
  403.                               child: Wrap(
  404.                                 children: unicodeArr.map((emoji) {
  405.                                   return Container(
  406.                                     padding: const EdgeInsets.all(8.0),
  407.                                     width: MediaQuery.of(context).size.width /
  408.                                         4, // 设置每个子项的宽度为屏幕宽度的三分之一
  409.                                     height: 60,
  410.                                     child: GestureDetector(
  411.                                       onTap: () {
  412.                                         setState(() {
  413.                                           messages.add(Message(
  414.                                             sender: 'me',
  415.                                             text: emoji,
  416.                                             type: 'text',
  417.                                           ));
  418.                                         });
  419.                                       },
  420.                                       child: Text(
  421.                                         emoji,
  422.                                         style: TextStyle(fontSize: 30),
  423.                                       ),
  424.                                     ),
  425.                                   );
  426.                                 }).toList(),
  427.                               ),
  428.                             )))
  429.                   ])))
  430.         ]));
  431.   }
  432. }
复制代码
第四步:创建会话模子Getx全局挂载关照
  1. import 'package:get/get.dart';
  2. import 'package:get/get_state_manager/src/simple/get_controllers.dart';
  3. import 'package:shared_preferences/shared_preferences.dart';
  4. import 'dart:convert';
  5. const String kChatInfoLocalKey = 'chatInfo_key';
  6. class ChatInfo extends GetxController {
  7.   factory ChatInfo() => _getInstance();
  8.   static ChatInfo get instance => _getInstance();
  9.   static ChatInfo? _instance;
  10.   ChatInfo._internal();
  11.   static ChatInfo _getInstance() {
  12.     _instance ??= ChatInfo._internal();
  13.     return _instance!;
  14.   }
  15.   String? get privateMsgMaxId => _privateMsgMaxId;
  16.   String _privateMsgMaxId ="0";
  17.   refreshWithMap(Map<String, dynamic> json) {
  18.     _privateMsgMaxId = json['privateMsgMaxId'];
  19.     update();
  20.   }
  21.   clearData() {
  22.     _privateMsgMaxId = "0";
  23.     update();
  24.   }
  25.   setPrivateMsgMaxId(String e) {
  26.     _privateMsgMaxId = e;
  27.     update();
  28.   }
  29.   static readLocalData() async {
  30.     SharedPreferences prefs = await SharedPreferences.getInstance();
  31.     //读取数据
  32.     String? jsonStr = prefs.getString(kChatInfoLocalKey);
  33.     if (jsonStr != null) {
  34.       Map<String, dynamic> chatInfo = json.decode(jsonStr);
  35.       ChatInfo.instance.refreshWithMap(chatInfo);
  36.     }
  37.   }
  38. }
复制代码
完工!!!!!!!!!!!!!!!!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

干翻全岛蛙蛙

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表