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);
    }
  }
}

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

相关推荐
惜.己27 分钟前
appium中urllib3.exceptions.LocationValueError: No host specified. 的错误解决办法
网络·appium
吉凶以情迁39 分钟前
window服务相关问题探索 go语言服务开发探索调试
linux·服务器·开发语言·网络·golang
专注VB编程开发20年1 小时前
UDP受限广播地址255.255.255.255的通信机制详解
网络·udp·智能路由器
189228048612 小时前
NX947NX955美光固态闪存NX962NX966
大数据·服务器·网络·人工智能·科技
Sadsvit3 小时前
Linux 进程管理与计划任务
linux·服务器·网络
一碗白开水一3 小时前
【模型细节】FPN经典网络模型 (Feature Pyramid Networks)详解及其变形优化
网络·人工智能·pytorch·深度学习·计算机视觉
什么都想学的阿超4 小时前
【网络与爬虫 38】Apify全栈指南:从0到1构建企业级自动化爬虫平台
网络·爬虫·自动化
D-海漠5 小时前
安全光幕Muting功能程序逻辑设计
服务器·网络·人工智能
都给我6 小时前
可计算存储(Computational Storage)与DPU(Data Processing Unit)的技术特点对比及实际应用场景分析
运维·服务器·网络·云计算
阿蒙Amon6 小时前
详解Python标准库之互联网数据处理
网络·数据库·python