Flutter连接websocket、实现在线聊天功能

老规矩效果图:

第一步:引入

web_socket_channel: ^2.4.0

第二步:封装 websocket.dart 单例

Dart 复制代码
import 'dart:async';
import 'dart:convert';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:web_socket_channel/io.dart';

class WebSocketManager {
  late WebSocketChannel _channel;
  final String _serverUrl; //ws连接路径
  final String _accessToken; //登录携带的token
  bool _isConnected = false; //连接状态
  bool _isManuallyDisconnected = false; //是否为主动断开
  late Timer _heartbeatTimer; //心跳定时器
  late Timer _reconnectTimer; //重新连接定时器
  Duration _reconnectInterval = Duration(seconds: 5); //重新连接间隔时间
  StreamController<String> _messageController = StreamController<String>();

  Stream<String> get messageStream => _messageController.stream; //监听的消息

  //初始化
  WebSocketManager(this._serverUrl, this._accessToken) {
    print('初始化');
    _heartbeatTimer = Timer(Duration(seconds: 0), () {});
    _startConnection();
  }

  //建立连接
  void _startConnection() async {
    try {
      _channel = WebSocketChannel.connect(Uri.parse(_serverUrl));
      print('建立连接');
      _isConnected = true;
      _channel.stream.listen(
        (data) {
          _isConnected = true;
          print('已连接$data');
          final jsonObj = jsonDecode(data); // 将消息对象转换为 JSON 字符串
          if (jsonObj['cmd'] == 0) {
            _startHeartbeat(); //开始心跳
          } else if (jsonObj['cmd'] == 1) {
            _resetHeartbeat(); // 重新开启心跳定时
          } else {
            _onMessageReceived(data);// 其他消息转发出去
          }
        },
        onError: (error) {
          // 处理连接错误
          print('连接错误: $error');
          _onError(error);
        },
        onDone: _onDone,
      );
      _sendInitialData(); // 连接成功后发送登录信息();
    } catch (e) {
      // 连接错误处理
      print('连接异常错误: $e');
      _onError(e);
    }
  }

  //断开连接
  void disconnect() {
    print('断开连接');
    _isConnected = false;
    _isManuallyDisconnected = true;
    _stopHeartbeat();
    _messageController.close();
    _channel.sink.close();
  }

  //开始心跳
  void _startHeartbeat() {
    _heartbeatTimer = Timer.periodic(Duration(seconds: 20), (_) {
      sendHeartbeat();
    });
  }

  //停止心跳
  void _stopHeartbeat() {
    _heartbeatTimer.cancel();
  }

  //重置心跳
  void _resetHeartbeat() {
    _stopHeartbeat();
    _startHeartbeat(); //开始心跳
  }

  // 发送心跳消息到服务器
  void sendHeartbeat() {
    if (_isConnected) {
      final message = {"cmd": 1, "data": {}};
      final jsonString = jsonEncode(message); // 将消息对象转换为 JSON 字符串
      _channel.sink.add(jsonString); // 发送心跳
      print('连接成功发送心跳消息到服务器$message');
    }
  }

  // 登录
  void _sendInitialData() async {
    try {
      final message = {
        "cmd": 0,
        "data": {"accessToken": _accessToken}
      };
      final jsonString = jsonEncode(message); // 将消息对象转换为 JSON 字符串
      _channel.sink.add(jsonString); // 发送 JSON 字符串
      print('连接成功-发送登录信息$message');
    } catch (e) {
      // 连接错误处理
      print('连接异常错误: $e');
      _onError(e);
    }
  }

  //发送信息
  void sendMessage(dynamic message) {
    final data = {
      "cmd":3,
      "data":message
    };
    final jsonString = jsonEncode(data); // 将消息对象转换为 JSON 字符串
    _channel.sink.add(jsonString); // 发送 JSON 字符串
  }

  // 处理接收到的消息
  void _onMessageReceived(dynamic message) {
    print(
        '处理接收到的消息Received===========================================: $message');
    _messageController.add(message);
  }

  //异常
  void _onError(dynamic error) {
    // 处理错误
    print('Error: $error');
    _isConnected = false;
    _stopHeartbeat();
    if (!_isManuallyDisconnected) {
      // 如果不是主动断开连接,则尝试重连
      _reconnect();
    }
  }

  //关闭
  void _onDone() {
    print('WebSocket 连接已关闭');
    _isConnected = false;
    _stopHeartbeat();
    if (!_isManuallyDisconnected) {
      // 如果不是主动断开连接,则尝试重连
      _reconnect();
    }
  }

  // 重连
  void _reconnect() {
    // 避免频繁重连,启动重连定时器
    _reconnectTimer = Timer(_reconnectInterval, () {
      _isConnected = false;
      _channel.sink.close(); // 关闭之前的连接
      print('重连====================$_serverUrl===$_accessToken');
      _startConnection();
    });
  }
}

第三步:chat.dart编写静态页面

Dart 复制代码
//在线聊天
import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:zhzt_estate/library/websocket/websocket.dart';

import 'package:zhzt_estate/home/house_detail_page.dart';
import '../library/network/network.dart';
import '../mine/models/userinfo.dart';
import 'models/chat.dart';

class Message {
  final String type;
  final String sender;
  final String? text;
  final Map? cardInfo;

  Message({required this.sender, this.text, required this.type, this.cardInfo});
}

//文字信息==============================================================================
class Bubble extends StatelessWidget {
  final Message message;
  final bool isMe;

  Bubble({required this.message, required this.isMe});

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
      children: [
        Visibility(
          visible: !isMe,
          child: const Icon(
            Icons.paid,
            size: 30,
          ),
        ),
        Container(
          margin: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
          padding: const EdgeInsets.all(10.0),
          decoration: BoxDecoration(
            color: isMe ? Colors.blue : Colors.grey[300],
            borderRadius: BorderRadius.circular(12.0),
          ),
          child: Text(
            message.text ?? '',
            style: TextStyle(color: isMe ? Colors.white : Colors.black),
          ),
        ),
        Visibility(
          visible: isMe,
          child: const Icon(
            Icons.pages,
            size: 30,
          ),
        )
      ],
    );
  }
}

//卡片================================================================================
class Card extends StatelessWidget {
  final Message message;
  final bool isMe;

  Card({required this.message, required this.isMe});

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
      children: [
        Visibility(
          visible: !isMe,
          child: const Icon(
            Icons.paid,
            size: 30,
          ),
        ),
        SizedBox(child: _CardPage(cardInfo: message.cardInfo ?? {})),
        Visibility(
          visible: isMe,
          child: const Icon(
            Icons.pages,
            size: 30,
          ),
        )
      ],
    );
  }
}

class _CardPage extends StatelessWidget {
  late Map cardInfo;

  _CardPage({required this.cardInfo});

  @override
  Widget build(BuildContext context) {
    return Container(
        width: MediaQuery.of(context).size.width * 0.8,
        margin: EdgeInsets.only(top: 5),
        padding: EdgeInsets.all(5),
        decoration: BoxDecoration(
            color: Colors.white, borderRadius: BorderRadius.circular(12.0)),
        child: Row(
          children: [
            GestureDetector(
                onTap: () {
                  // Add your click event handling code here
                  // 去详情页
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                      // fullscreenDialog: true,
                      builder: (context) => MyHomeDetailPage(
                          houseId: cardInfo['id'], type: cardInfo['type']),
                    ),
                  );
                },
                child: Container(
                  width: 100,
                  height: 84,
                  margin: const EdgeInsets.all(8),
                  decoration: BoxDecoration(
                    color: Colors.blueAccent,
                    // image: DecorationImage(
                    //   image: NetworkImage(
                    //       kFileRootUrl + (cardInfo['styleImgPath'] ?? '')),
                    //   fit: BoxFit.fill,
                    //   repeat: ImageRepeat.noRepeat,
                    // ),
                    borderRadius: BorderRadius.circular(10),
                  ),
                )),
            GestureDetector(
                onTap: () {
                  // Add your click event handling code here
                  // 去详情页
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                      // fullscreenDialog: true,
                      builder: (context) => MyHomeDetailPage(
                          houseId: cardInfo['id'], type: cardInfo['type']),
                    ),
                  );
                },
                child: Container(
                  alignment: Alignment.topLeft,
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.start,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        crossAxisAlignment: CrossAxisAlignment.center,
                        children: [
                          Text(
                            cardInfo['name'],
                            style: const TextStyle(fontSize: 18),
                          ),
                        ],
                      ),
                      Row(
                        children: [
                          Text(cardInfo['zoneName'] ?? ''),
                          const Text(' | '),
                          Text('${"mianji".tr} '),
                          Text(cardInfo['area']),
                        ],
                      ),
                      Container(
                        alignment: Alignment.centerLeft,
                        child: Text(
                          '${cardInfo['price'] ?? ''}/㎡',
                          style: const TextStyle(
                              color: Colors.orange, fontSize: 16),
                        ),
                      ),
                    ],
                  ),
                )), //小标题
          ],
        ));
  }
}

//主页
class CommunicatePage extends StatefulWidget {
  const CommunicatePage({super.key});

  @override
  State<CommunicatePage> createState() => _CommunicatePageState();
}

class _CommunicatePageState extends State<CommunicatePage> {
//变量 start==========================================================
  final TextEditingController _ContentController =
      TextEditingController(text: '');

  /// 输入框焦点
  FocusNode focusNode = FocusNode();
  final List<Message> messages = [
    Message(
        sender: "ta",
        cardInfo: {
          "id": "4",
          "code": "fxhsud",
          "title": "test1",
          "name": "test1",
          "zoneName": null,
          "area": "90",
          "roomType": "2室1厅1卫",
          "directions": ["2"],
          "price": "200.00",
          "type": 2,
          "status": 2,
          "seeCount": null,
          "floorNum": "24/30",
          "styleImgPath":
              "",
          "time": "2022-03-26"
        },
        type: "card"),
    Message(sender: "me", text: "hi!", type: "text"),
    Message(sender: "me", text: "你是?!", type: "text"),
    Message(sender: "ta", text: "hello!", type: "text")
  ];
  var isEmojiShow = false;
  final List unicodeArr = [
    '\u{1F600}',
    '\u{1F601}',
    '\u{1F602}',
    '\u{1F603}',
    '\u{1F604}',
    '\u{1F60A}',
    '\u{1F60B}',
    '\u{1F60C}',
    '\u{1F60D}',
    '\u{2764}',
    '\u{1F44A}',
    '\u{1F44B}',
    '\u{1F44C}',
    '\u{1F44D}'
  ];

  // 创建 Websocket 实例
  final websocket = WebSocketManager(kWsRootUrl, UserInfo.instance.token ?? '');
  initFunc() {
    if (UserInfo.instance.token != null) {
      websocket.messageStream.listen((message) {
        print('接收数据---------------------$message');
        setMsg(message);//接收消息渲染
      });
    }
  }

  //接收消息渲染
  setMsg(data){
    final jsonObj = jsonDecode(data);
    setState(() {
      messages.add(Message(
        sender: 'ta',
        text: data,
        type: 'text',
      ));
    });
  }

  //发送消息
  sendMsg(data){
    websocket.sendMessage({
      "content": data,
      "type": 0,
      "recvId": 6
    });
  }

  pullPrivateOfflineMessage(minId) {
    Network.get('$kRootUrl/message/private/pullOfflineMessage',
        headers: {'Content-Type': 'application/json'},
        queryParameters: {"minId": minId}).then((res) {
      if (res == null) {
        return;
      }
    });
  }

//变量 end==========================================================

  @override
  void initState() {
    initFunc();
    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
    websocket.disconnect();
    _ContentController.dispose();
    focusNode.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
        backgroundColor: Color(0xFFebebeb),
        resizeToAvoidBottomInset: true,
        appBar: AppBar(
          title: Text('张三'),
        ),
        body: Stack(alignment: Alignment.bottomCenter, children: [
          ListView.builder(
            itemCount: messages.length,
            itemBuilder: (BuildContext context, int index) {
              return messages[index].type == 'text'
                  ? Bubble(
                      message: messages[index],
                      isMe: messages[index].sender == 'me',
                    )
                  : Card(
                      message: messages[index],
                      isMe: messages[index].sender == 'me',
                    );
            },
          ),
          Positioned(
              bottom: 0,
              child: SingleChildScrollView(
                  reverse: true, // 反向滚动以确保 Positioned 在键盘上方
                  child: Column(children: [
                    Container(
                      width: MediaQuery.of(context).size.width,
                      height: 50,
                      decoration: const BoxDecoration(
                          color: Color.fromRGBO(240, 240, 240, 1)),
                      child: Row(
                        mainAxisAlignment: MainAxisAlignment.spaceAround,
                        children: [
                          const Icon(
                            Icons.contactless_outlined,
                            size: 35,
                          ),
                          SizedBox(
                              width: MediaQuery.of(context).size.width *
                                  0.6, // 添加固定宽度
                              child: TextField(
                                textAlignVertical: TextAlignVertical.center,
                                controller: _ContentController,
                                decoration: const InputDecoration(
                                  contentPadding: EdgeInsets.all(5),
                                  isCollapsed: true,
                                  filled: true,
                                  fillColor: Colors.white,
                                  // 设置背景色
                                  border: OutlineInputBorder(
                                    borderRadius: BorderRadius.all(
                                        Radius.circular(10)), // 设置圆角半径
                                    borderSide: BorderSide.none, // 去掉边框
                                  ),
                                ),
                                focusNode: focusNode,
                                onTap: () => {
                                  setState(() {
                                    isEmojiShow = false;
                                  })
                                },
                                onTapOutside: (e) => {focusNode.unfocus()},
                                onEditingComplete: () {
                                  FocusScope.of(context)
                                      .requestFocus(focusNode);
                                },
                              )),
                          GestureDetector(
                              onTap: () => {
                                    setState(() {
                                      isEmojiShow =
                                          !isEmojiShow; // 数据加载完毕,重置标志位
                                    })
                                  },
                              child: const Icon(
                                Icons.sentiment_satisfied_alt_outlined,
                                size: 35,
                              )),
                          Visibility(
                              visible: _ContentController.text=='',
                              child:
                          GestureDetector(
                              onTap: () {

                              },
                              child: const Icon(
                                Icons.add_circle_outline,
                                size: 35,
                              ))
                          ),
                          Visibility(
                              visible: _ContentController.text!='',
                              child:
                              GestureDetector(
                                  onTap: () {
                                    sendMsg(_ContentController.text);
                                  },
                                  child: const Icon(
                                    Icons.send,
                                    color: Colors.blueAccent,
                                    size: 35,
                                  ))
                          )
                        ],
                      ),
                    ),
                    Visibility(
                        visible: isEmojiShow,
                        child: Container(
                            width: MediaQuery.of(context).size.width,
                            height: 200,
                            decoration:
                                const BoxDecoration(color: Colors.white),
                            child: SingleChildScrollView(
                              scrollDirection: Axis.vertical,
                              child: Wrap(
                                children: unicodeArr.map((emoji) {
                                  return Container(
                                    padding: const EdgeInsets.all(8.0),
                                    width: MediaQuery.of(context).size.width /
                                        4, // 设置每个子项的宽度为屏幕宽度的三分之一
                                    height: 60,
                                    child: GestureDetector(
                                      onTap: () {
                                        setState(() {
                                          messages.add(Message(
                                            sender: 'me',
                                            text: emoji,
                                            type: 'text',
                                          ));
                                        });
                                      },
                                      child: Text(
                                        emoji,
                                        style: TextStyle(fontSize: 30),
                                      ),
                                    ),
                                  );
                                }).toList(),
                              ),
                            )))
                  ])))
        ]));
  }
}

第四步:创建会话模型Getx全局挂载通知

Dart 复制代码
import 'package:get/get.dart';
import 'package:get/get_state_manager/src/simple/get_controllers.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

const String kChatInfoLocalKey = 'chatInfo_key';

class ChatInfo extends GetxController {
  factory ChatInfo() => _getInstance();

  static ChatInfo get instance => _getInstance();

  static ChatInfo? _instance;
  ChatInfo._internal();
  static ChatInfo _getInstance() {
    _instance ??= ChatInfo._internal();
    return _instance!;
  }
  String? get privateMsgMaxId => _privateMsgMaxId;

  String _privateMsgMaxId ="0";

  refreshWithMap(Map<String, dynamic> json) {
    _privateMsgMaxId = json['privateMsgMaxId'];
    update();
  }

  clearData() {
    _privateMsgMaxId = "0";
    update();
  }

  setPrivateMsgMaxId(String e) {
    _privateMsgMaxId = e;
    update();
  }

  static readLocalData() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    //读取数据
    String? jsonStr = prefs.getString(kChatInfoLocalKey);
    if (jsonStr != null) {
      Map<String, dynamic> chatInfo = json.decode(jsonStr);
      ChatInfo.instance.refreshWithMap(chatInfo);
    }
  }
}

完工!!!!!!!!!!!!!!!!

相关推荐
记得多喝水o4 分钟前
华三M-LAG场景下,部分MAC内的流量泛洪导致端口流量打满
网络
群联云防护小杜5 分钟前
服务器被攻击怎么办
运维·服务器·网络·网络协议·安全·web安全
华为云开发者联盟10 分钟前
混合云网络过于复杂?ENS给你全局一张网的极致体验
网络·ens·混合云·华为云stack
__zhangheng29 分钟前
Mac 查询IP配置,网络代理
linux·服务器·网络·git
IT 古月方源1 小时前
ospf 的 状态机详解
运维·网络·tcp/ip·智能路由器
dog2501 小时前
UDP 比 TCP 更快吗?
网络·tcp/ip·udp
一勺汤1 小时前
YOLO11改进-模块-引入星型运算Star Blocks
网络·yolo·目标检测·改进·魔改·yolov11·yolov11改进
ChennyWJS1 小时前
03.HTTPS的实现原理-HTTPS的工作流程
网络·网络协议·http·https
ChennyWJS1 小时前
01.HTTPS的实现原理-HTTPS的概念
网络协议·http·https
Hacker_Oldv2 小时前
网络安全攻防学习平台 - 基础关
网络·学习·web安全