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

ops/2024/10/22 14:25:43/

老规矩效果图:

第一步:引入

web_socket_channel: ^2.4.0

第二步:封装 websocket.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; //登录携带的tokenbool _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编写静态页面

//在线聊天
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});@overrideWidget 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});@overrideWidget 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});@overrideWidget 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});@overrideState<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==========================================================@overridevoid initState() {initFunc();super.initState();}@overridevoid dispose() {super.dispose();websocket.disconnect();_ContentController.dispose();focusNode.dispose();}@overrideWidget build(BuildContext context) {// TODO: implement buildreturn 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全局挂载通知

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

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


http://www.ppmy.cn/ops/39834.html

相关文章

golang中变量交换的2种方式

变量的交换在我们对数据进行排序时是必须要用到的技术&#xff0c; 今天就给大家介绍2种go语言中交换变量的方式。 1. 使用传统方式定义一个中间变量来进行变量的交换 // 文件名 demo1.go package demo01import "fmt"func ExampleDemo02() {//常规方式进行变量交换…

域基础-NTLM协议

简介 NTLM(New Technology LAN Manager)协议是微软用于Windows身份验证的主要协议之一。继SMB、LM协议之后微软提出了NTLM协议&#xff0c;这一协议安全性更高&#xff0c;不仅可以用于工作组中的机器身份验证&#xff0c;又可以用于域环境身份验证&#xff0c;还可以为SMB、H…

2024最新从0部署Django项目(nginx+uwsgi+mysql)

云服务器 我这里用的是腾讯云免费试用的2H4Gcentos服务器&#xff08;后升级为2H8G&#xff0c;保险一点提高内存&#xff09; 因为网上很多关于django部属的教程都是宝塔啊&#xff0c;python版本控制器啊这种的&#xff0c;我也误打误撞安装了宝塔面板&#xff0c;但这里我…

https://是怎么实现的?

默认的网站建设好后都是http访问模式&#xff0c;这种模式对于纯内容类型的网站来说&#xff0c;没有什么问题&#xff0c;但如果受到中间网络劫持会让网站轻易的跳转钓鱼网站&#xff0c;为避免这种情况下发生&#xff0c;所以传统的网站改为https协议&#xff0c;这种协议自己…

Python-VBA函数之旅-str函数

目录 一、str函数的常见应用场景 二、str函数使用注意事项 三、如何用好str函数&#xff1f; 1、str函数&#xff1a; 1-1、Python&#xff1a; 1-2、VBA&#xff1a; 2、推荐阅读&#xff1a; 个人主页&#xff1a; https://myelsa1024.blog.csdn.net/ 一、str函数的常…

Docker介绍及使用

Docker简介 Docker 是一种用于开发、部署和运行应用程序的开源平台。它使用容器化技术&#xff0c;将应用程序及其所有依赖项打包到一个轻型的可移植单元中&#xff0c;称为容器。容器可以快速启动、停止和移动&#xff0c;并且可以在各种环境中运行&#xff0c;包括本地计算机…

网课:第三章递归与分治思想---小q的数列

题目描述 小q最近迷上了各种好玩的数列&#xff0c;这天&#xff0c;他发现了一个有趣的数列&#xff0c;其递推公式如下&#xff1a; f[0]0 f[1]1; f[i]f[i/2]f[i%2];(i>2) 现在&#xff0c;他想考考你&#xff0c;问&#xff1a;给你一个n&#xff0c;代表数列的第n项&am…

Mac安装Photoshop2024 For Macv25.7.0 ps2024中文激活版

资源介绍 支持&#xff1a;mac系统/M/INTEL芯 Adobe Photoshop for mac是由Adobe专业为mac系统开发和发行的图像处理软件。Photoshop主要处理以像素所构成的数字图像。使用其众多的编修与绘图工具&#xff0c;可以有效地进行图片编辑和创造工作。PS有很多功能&#xff0c;在图…