老规矩结果图:
第一步:引入
- 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; //登录携带的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编写静态页面
- //在线聊天
- 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全局挂载关照
- 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);
- }
- }
- }
复制代码 完工!!!!!!!!!!!!!!!!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |