前言
- WebSocket很常用,在很多語(yǔ)言都有支持,例如Java、JavaScript、Rust、C++、Go等,那么Dart也是有支持的,在Flutter中使用
web_socket_channel即可使用WebSocket - Flutter的跨平臺(tái)功能很強(qiáng)大,本篇使用Flutter的WebSocket來(lái)實(shí)現(xiàn)安卓、iOS、Web的3個(gè)平臺(tái)的應(yīng)用編寫
效果展示

首頁(yè).png

聊天頁(yè)面.png
依賴
- Toast庫(kù)可以換其他的,我選擇
fluttertoast是因?yàn)樗鞘褂闷脚_(tái)API來(lái)實(shí)現(xiàn)的,在Android平臺(tái)上調(diào)用的的是Toast,可以跨頁(yè)面、跨應(yīng)用顯示,如果是純Flutter實(shí)現(xiàn),是不可以跨應(yīng)用的
# WebSocket支持庫(kù)
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';
/// 連接狀態(tài)枚舉
enum ConnectStatusEnum {
//已連接
connect,
//連接中
connecting,
//已關(guān)閉
close,
//關(guān)閉中
closing
}
/// 接收到消息后的回調(diào)
typedef ListenMessageCallback = void Function(String msg);
/// 錯(cuò)誤回調(diào)
typedef ErrorCallback = void Function(Exception error);
/// WebSocket管理類
class WebSocketManager {
/// 連接狀態(tài),默認(rèn)為關(guān)閉
ConnectStatusEnum _connectStatus = ConnectStatusEnum.close;
/// WebSocket通道
WebSocketChannel? _webSocketChannel;
/// WebSocket通道的流
Stream<dynamic>? _webSocketChannelStream;
/// WebSocket狀態(tài)的流控制器
final StreamController<ConnectStatusEnum> _socketStatusController =
StreamController<ConnectStatusEnum>();
/// 連接狀態(tài)的流
Stream<ConnectStatusEnum>? _socketStatusStream;
/// 獲取WebSocket消息的流
Stream<dynamic> getWebSocketChannelStream() {
//只賦值一次
_webSocketChannelStream ??= _webSocketChannel!.stream.asBroadcastStream();
return _webSocketChannelStream!;
}
/// 獲取連接狀態(tài)的流
Stream<ConnectStatusEnum> getSocketStatusStream() {
//只賦值一次
_socketStatusStream ??= _socketStatusController.stream.asBroadcastStream();
return _socketStatusStream!;
}
/// 發(fā)起連接,Url實(shí)例:"ws://echo.websocket.org";
Future<bool> connect(String url) async {
if (_connectStatus == ConnectStatusEnum.connect) {
//已連接,不需要處理
return true;
} else if (_connectStatus == ConnectStatusEnum.close) {
//未連接,發(fā)起連接
_connectStatus = ConnectStatusEnum.connecting;
_socketStatusController.add(ConnectStatusEnum.connecting);
var connectUrl = Uri.parse(url);
//Web端需要使用該Channel,否則報(bào)錯(cuò)
if (kIsWeb) {
_webSocketChannel = HtmlWebSocketChannel.connect(connectUrl);
} else {
_webSocketChannel = IOWebSocketChannel.connect(connectUrl);
}
_connectStatus = ConnectStatusEnum.connect;
_socketStatusController.add(ConnectStatusEnum.connect);
return true;
} else {
return false;
}
}
/// 關(guān)閉連接
Future disconnect() async {
if (_connectStatus == ConnectStatusEnum.connect) {
_connectStatus = ConnectStatusEnum.closing;
if (!_socketStatusController.isClosed) {
_socketStatusController.add(ConnectStatusEnum.closing);
}
await _webSocketChannel?.sink.close(3000, "主動(dòng)關(guān)閉");
_connectStatus = ConnectStatusEnum.close;
if (!_socketStatusController.isClosed) {
_socketStatusController.add(ConnectStatusEnum.close);
}
}
}
/// 重連
void reconnect(String url) async {
await disconnect();
await connect(url);
}
/// 監(jiān)聽消息
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);
}
});
}
/// 發(fā)送消息
bool sendMsg(String text) {
if (_connectStatus == ConnectStatusEnum.connect) {
_webSocketChannel?.sink.add(text);
return true;
}
return false;
}
/// 獲取當(dāng)前連接狀態(tài)
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();
//關(guān)閉連接狀態(tài)的流
_socketStatusController.close();
}
}
頁(yè)面
應(yīng)用入口(main.dart)
import 'package:flutter/material.dart';
import 'home_page.dart';
void main() {
runApp(const MyApp());
}
/// 主頁(yè)面
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風(fēng)格
useMaterial3: false,
),
home: const HomePage(title: 'Flutter WebSocket'),
);
}
}
首頁(yè)(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';
/// 首頁(yè)
class HomePage extends StatefulWidget {
const HomePage({super.key, required this.title});
/// 頁(yè)面標(biāo)題
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");
/// 跳轉(zhuǎn)到WebSocket頁(yè)面
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(
//左側(cè)圖標(biāo)
icon: const Icon(Icons.person),
//提示文字
hintText: "請(qǐng)輸入連接地址:",
//邊框
border: const OutlineInputBorder(),
//右側(cè)按鈕
suffixIcon: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
//清除輸入框內(nèi)容
_editingController.clear();
},
),
),
),
),
ElevatedButton(
child: const Text("WebSocket測(cè)試"),
onPressed: () {
//連接地址
var url = _editingController.text;
if (url.isEmpty) {
ToastUtil.toast("請(qǐng)輸入連接地址");
return;
}
// 跳轉(zhuǎn)到WebSocket頁(yè)面
_goWebSocketPage(context, url);
},
),
],
),
),
);
}
}
聊天頁(yè)面(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頁(yè)面
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的滾動(dòng)控制器
final ScrollController _scrollController = ScrollController();
/// 消息列表
final List<String> _msgList = [];
@override
void initState() {
super.initState();
_webSocketManager.connect(widget._url).then((isConnect) {
//連接成功,監(jiān)聽消息
if (isConnect) {
_webSocketManager.listen((msg) {
//添加消息到列表中
_addMsg2List("服務(wù)器:$msg");
}, onError: (error) {
ToastUtil.toast("連接異常:${error.toString()}");
});
}
});
}
@override
void dispose() {
//斷開連接,銷毀流對(duì)象
_webSocketManager.dispose();
super.dispose();
}
/// 添加消息到消息列表中
void _addMsg2List(String msg) {
setState(() {
_msgList.add(msg);
});
//延遲500毫秒,再滾動(dòng)到地址
Future.delayed(const Duration(milliseconds: 500), () {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
});
}
/// 發(fā)送消息
void _sendMsg(String msg) {
if (msg.isEmpty) {
ToastUtil.toast("請(qǐng)輸入要發(fā)送的消息內(nèi)容");
return;
}
var isSendSuccess = _webSocketManager.sendMsg(msg);
//發(fā)送成功
if (isSendSuccess) {
//添加消息到列表中
_addMsg2List("我:$msg");
//清除輸入框的內(nèi)容
_editingController.clear();
} else {
ToastUtil.toast("發(fā)送失敗");
}
}
/// 構(gòu)建連接狀態(tài)控件
Widget _buildConnectStatusWidget() {
return StreamBuilder<ConnectStatusEnum>(
builder: (context, snapshot) {
if (snapshot.data == ConnectStatusEnum.connect) {
return StreamBuilder(
builder: (context, newSnapshot) {
//WebSocket發(fā)生錯(cuò)誤,那么重連
if (newSnapshot.hasError) {
_webSocketManager.reconnect(widget._url);
}
return const Text(
"連接狀態(tài):已連接",
);
},
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(
"連接狀態(tài):已關(guān)閉",
);
} else if (snapshot.data == ConnectStatusEnum.closing) {
return const Text(
"連接狀態(tài):關(guān)閉中",
);
}
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測(cè)試"),
),
body: Column(
children: [
//連接狀態(tài)
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: "請(qǐng)輸入要發(fā)送的消息",
//邊框
border: OutlineInputBorder(),
),
),
),
Container(
margin: const EdgeInsets.only(left: 5.0),
child: ElevatedButton(
child: const Text("發(fā)送"),
onPressed: () {
var msg = _editingController.text;
_sendMsg(msg);
},
),
)
],
),
),
],
),
)
],
),
);
}
}
權(quán)限配置
- Android端的網(wǎng)絡(luò)權(quán)限
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<!-- 省略其他配置 -->
</manifest>