前言
- WebSocket很常用,在很多语言都有支持,例如Java、JavaScript、Rust、C++、Go等,那么Dart也是有支持的,在Flutter中使用
web_socket_channel
即可使用WebSocket
- Flutter的跨平台功能很强大,本篇使用Flutter的WebSocket来实现安卓、iOS、Web的3个平台的应用编写
效果展示
依赖
- Toast库可以换其他的,我选择
fluttertoast
是因为它是使用平台API来实现的,在Android平台上调用的的是Toast,可以跨页面、跨应用显示,如果是纯Flutter实现,是不可以跨应用的
# WebSocket支持库
web_socket_channel: ^2.4.0
# Toast,支持Android、iOS、Web
fluttertoast: ^8.0.9
工具类
- Toast工具类(toast_util.dart)
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
/// Toast 工具类
class ToastUtil {
static toast(String msg) {
Fluttertoast.showToast(
msg: msg,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: 1,
backgroundColor: Colors.black,
textColor: Colors.white,
fontSize: 16.0);
}
}
- WebSocket工具类(web_socket_manager.dart)
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:web_socket_channel/html.dart';
import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
/// 连接状态枚举
enum ConnectStatusEnum {
//已连接
connect,
//连接中
connecting,
//已关闭
close,
//关闭中
closing
}
/// 接收到消息后的回调
typedef ListenMessageCallback = void Function(String msg);
/// 错误回调
typedef ErrorCallback = void Function(Exception error);
/// WebSocket管理类
class WebSocketManager {
/// 连接状态,默认为关闭
ConnectStatusEnum _connectStatus = ConnectStatusEnum.close;
/// WebSocket通道
WebSocketChannel? _webSocketChannel;
/// WebSocket通道的流
Stream<dynamic>? _webSocketChannelStream;
/// WebSocket状态的流控制器
final StreamController<ConnectStatusEnum> _socketStatusController =
StreamController<ConnectStatusEnum>();
/// 连接状态的流
Stream<ConnectStatusEnum>? _socketStatusStream;
/// 获取WebSocket消息的流
Stream<dynamic> getWebSocketChannelStream() {
//只赋值一次
_webSocketChannelStream ??= _webSocketChannel!.stream.asBroadcastStream();
return _webSocketChannelStream!;
}
/// 获取连接状态的流
Stream<ConnectStatusEnum> getSocketStatusStream() {
//只赋值一次
_socketStatusStream ??= _socketStatusController.stream.asBroadcastStream();
return _socketStatusStream!;
}
/// 发起连接,Url实例:"ws://echo.websocket.org";
Future<bool> connect(String url) async {
if (_connectStatus == ConnectStatusEnum.connect) {
//已连接,不需要处理
return true;
} else if (_connectStatus == ConnectStatusEnum.close) {
//未连接,发起连接
_connectStatus = ConnectStatusEnum.connecting;
_socketStatusController.add(ConnectStatusEnum.connecting);
var connectUrl = Uri.parse(url);
//Web端需要使用该Channel,否则报错
if (kIsWeb) {
_webSocketChannel = HtmlWebSocketChannel.connect(connectUrl);
} else {
_webSocketChannel = IOWebSocketChannel.connect(connectUrl);
}
_connectStatus = ConnectStatusEnum.connect;
_socketStatusController.add(ConnectStatusEnum.connect);
return true;
} else {
return false;
}
}
/// 关闭连接
Future disconnect() async {
if (_connectStatus == ConnectStatusEnum.connect) {
_connectStatus = ConnectStatusEnum.closing;
if (!_socketStatusController.isClosed) {
_socketStatusController.add(ConnectStatusEnum.closing);
}
await _webSocketChannel?.sink.close(3000, "主动关闭");
_connectStatus = ConnectStatusEnum.close;
if (!_socketStatusController.isClosed) {
_socketStatusController.add(ConnectStatusEnum.close);
}
}
}
/// 重连
void reconnect(String url) async {
await disconnect();
await connect(url);
}
/// 监听消息
void listen(ListenMessageCallback messageCallback, {ErrorCallback? onError}) {
getWebSocketChannelStream().listen((message) {
messageCallback.call(message);
}, onError: (error) {
//连接异常
_connectStatus = ConnectStatusEnum.close;
_socketStatusController.add(ConnectStatusEnum.close);
if (onError != null) {
onError.call(error);
}
});
}
/// 发送消息
bool sendMsg(String text) {
if (_connectStatus == ConnectStatusEnum.connect) {
_webSocketChannel?.sink.add(text);
return true;
}
return false;
}
/// 获取当前连接状态
ConnectStatusEnum getCurrentStatus() {
if (_connectStatus == ConnectStatusEnum.connect) {
return ConnectStatusEnum.connect;
} else if (_connectStatus == ConnectStatusEnum.connecting) {
return ConnectStatusEnum.connecting;
} else if (_connectStatus == ConnectStatusEnum.close) {
return ConnectStatusEnum.close;
} else if (_connectStatus == ConnectStatusEnum.closing) {
return ConnectStatusEnum.closing;
}
return ConnectStatusEnum.closing;
}
/// 销毁通道
void dispose() {
//断开连接
disconnect();
//关闭连接状态的流
_socketStatusController.close();
}
}
页面
应用入口(main.dart)
import 'package:flutter/material.dart';
import 'home_page.dart';
void main() {
runApp(const MyApp());
}
/// 主页面
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter WebSocket',
theme: ThemeData(
//主题色
primarySwatch: Colors.blue,
//colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
//是否使用Material3风格
useMaterial3: false,
),
home: const HomePage(title: 'Flutter WebSocket'),
);
}
}
首页(home_page.dart)
import 'package:flutter/material.dart';
import 'package:flutter_web_socket/util/toast_util.dart';
import 'package:flutter_web_socket/web_socket_page.dart';
/// 首页
class HomePage extends StatefulWidget {
const HomePage({super.key, required this.title});
/// 页面标题
final String title;
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
/// TextField操作控制器
final TextEditingController _editingController =
TextEditingController(text: "ws://127.0.0.1:9001/ws");
/// 跳转到WebSocket页面
void _goWebSocketPage(BuildContext context, String url) {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return WebSocketPage(
url: url,
);
}));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.primary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
horizontal: 15.0
),
child: TextField(
controller: _editingController,
decoration: InputDecoration(
//左侧图标
icon: const Icon(Icons.person),
//提示文字
hintText: "请输入连接地址:",
//边框
border: const OutlineInputBorder(),
//右侧按钮
suffixIcon: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
//清除输入框内容
_editingController.clear();
},
),
),
),
),
ElevatedButton(
child: const Text("WebSocket测试"),
onPressed: () {
//连接地址
var url = _editingController.text;
if (url.isEmpty) {
ToastUtil.toast("请输入连接地址");
return;
}
// 跳转到WebSocket页面
_goWebSocketPage(context, url);
},
),
],
),
),
);
}
}
聊天页面(web_socket_page.dart)
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_web_socket/util/toast_util.dart';
import 'package:flutter_web_socket/util/web_socket_manager.dart';
/// WebSocket页面
class WebSocketPage extends StatefulWidget {
/// 连接地址
final String _url;
const WebSocketPage({super.key, required String url}) : _url = url;
@override
State<StatefulWidget> createState() {
return WebSocketPageState();
}
}
class WebSocketPageState extends State<WebSocketPage> {
/// WebSocket管理类
final WebSocketManager _webSocketManager = WebSocketManager();
/// TextField操作控制器
final TextEditingController _editingController = TextEditingController();
/// ListView的滚动控制器
final ScrollController _scrollController = ScrollController();
/// 消息列表
final List<String> _msgList = [];
@override
void initState() {
super.initState();
_webSocketManager.connect(widget._url).then((isConnect) {
//连接成功,监听消息
if (isConnect) {
_webSocketManager.listen((msg) {
//添加消息到列表中
_addMsg2List("服务器:$msg");
}, onError: (error) {
ToastUtil.toast("连接异常:${error.toString()}");
});
}
});
}
@override
void dispose() {
//断开连接,销毁流对象
_webSocketManager.dispose();
super.dispose();
}
/// 添加消息到消息列表中
void _addMsg2List(String msg) {
setState(() {
_msgList.add(msg);
});
//延迟500毫秒,再滚动到地址
Future.delayed(const Duration(milliseconds: 500), () {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
});
}
/// 发送消息
void _sendMsg(String msg) {
if (msg.isEmpty) {
ToastUtil.toast("请输入要发送的消息内容");
return;
}
var isSendSuccess = _webSocketManager.sendMsg(msg);
//发送成功
if (isSendSuccess) {
//添加消息到列表中
_addMsg2List("我:$msg");
//清除输入框的内容
_editingController.clear();
} else {
ToastUtil.toast("发送失败");
}
}
/// 构建连接状态控件
Widget _buildConnectStatusWidget() {
return StreamBuilder<ConnectStatusEnum>(
builder: (context, snapshot) {
if (snapshot.data == ConnectStatusEnum.connect) {
return StreamBuilder(
builder: (context, newSnapshot) {
//WebSocket发生错误,那么重连
if (newSnapshot.hasError) {
_webSocketManager.reconnect(widget._url);
}
return const Text(
"连接状态:已连接",
);
},
stream: _webSocketManager.getWebSocketChannelStream(),
);
} else if (snapshot.data == ConnectStatusEnum.connecting) {
//连接中
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CupertinoActivityIndicator(
animating: true,
radius: 10,
),
Container(
margin: const EdgeInsets.only(left: 5.0),
child: const Text("连接中..."),
)
],
);
} else if (snapshot.data == ConnectStatusEnum.close) {
return const Text(
"连接状态:已关闭",
);
} else if (snapshot.data == ConnectStatusEnum.closing) {
return const Text(
"连接状态:关闭中",
);
}
return const Text(
"未连接",
);
},
initialData: ConnectStatusEnum.close,
stream: _webSocketManager.getSocketStatusStream(),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () {
Navigator.pop(context, null);
},
icon: const Icon(Icons.arrow_back)),
title: const Text("WebSocket测试"),
),
body: Column(
children: [
//连接状态
Column(
children: [
Container(
height: 0.2,
color: Colors.grey,
),
Padding(
padding: const EdgeInsets.all(15.0),
child: _buildConnectStatusWidget(),
),
Container(
height: 0.2,
color: Colors.grey,
)
],
),
Expanded(
flex: 1,
child: ListView.builder(
controller: _scrollController,
itemCount: _msgList.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text(_msgList[index]));
}),
),
Center(
child: Column(
children: [
Container(
height: 0.3,
color: Colors.grey,
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 15.0, horizontal: 18.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
flex: 1,
child: TextField(
controller: _editingController,
decoration: const InputDecoration(
//提示文字
hintText: "请输入要发送的消息",
//边框
border: OutlineInputBorder(),
),
),
),
Container(
margin: const EdgeInsets.only(left: 5.0),
child: ElevatedButton(
child: const Text("发送"),
onPressed: () {
var msg = _editingController.text;
_sendMsg(msg);
},
),
)
],
),
),
],
),
)
],
),
);
}
}
权限配置
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<!-- 省略其他配置 -->
</manifest>