老规矩效果图:
第一步:引入
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);
}
}
}
完工!!!!!!!!!!!!!!!!