adapter_websocket 0.0.7
adapter_websocket: ^0.0.7 copied to clipboard
WebSocket encapsulated with the adapter pattern enhances usability and testability, enabling rapid environment switching.
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:adapter_websocket/websocket_plugin.dart';
import 'package:flutter/services.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Enhanced WebSocket Plugin Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: WebSocketDemo(),
);
}
}
class WebSocketDemo extends StatefulWidget {
const WebSocketDemo({super.key});
@override
State<WebSocketDemo> createState() => _WebSocketDemoState();
}
class _WebSocketDemoState extends State<WebSocketDemo> {
WebSocketClient? _client;
final TextEditingController _messageController = TextEditingController();
final TextEditingController _urlController = TextEditingController(
text: 'ws://124.222.6.60:8800',
);
final List<String> _messages = [];
final List<String> _logs = [];
Map<String, dynamic> _stats = {};
@override
void initState() {
super.initState();
_initializeWebSocket();
}
// Future<HttpClient> createPinnedHttpClient({required String assetPath}) async {
// // Load the PEM from assets
// final ByteData certData = await rootBundle.load(assetPath);
// final Uint8List certBytes = certData.buffer.asUint8List();
//
// // Set up a SecurityContext with just that one cert
// final SecurityContext securityContext = SecurityContext(
// withTrustedRoots: false,
// );
// securityContext.setTrustedCertificatesBytes(certBytes);
//
// final HttpClient client = HttpClient(context: securityContext);
//
// // Optional: you can do additional runtime checks here if you like:
// client.badCertificateCallback =
// (X509Certificate cert, String host, int port) {
// // Return true only if this exact PEM matches
// final String incomingPem = cert.pem;
// final String pinnedPem = utf8.decode(certBytes);
// return incomingPem == pinnedPem;
// };
//
// return client;
// }
Future<void> _initializeWebSocket() async {
final config = WebSocketConfig(
url: _urlController.text,
autoReconnect: true,
maxReconnectAttempts: 5,
reconnectDelay: Duration(seconds: 2),
useExponentialBackoff: true,
maxReconnectDelay: Duration(minutes: 2),
enableLogging: true,
// httpClient: await createPinnedHttpClient(assetPath: 'ssl/test_cert.pem'),
// Enhanced heartbeat configuration
enableHeartbeat: true,
heartbeatInterval: Duration(seconds: 15),
heartbeatTimeout: Duration(seconds: 5),
heartbeatMessage: '{"type":"heartbeat"}',
expectedPongMessage: '{"type":"heartbeat_ack"}',
maxMissedHeartbeats: 3,
);
final adapter = WebSocketChannelAdapter(config);
_client = WebSocketClient(adapter);
// Listen to state changes
_client?.stateStream.listen((state) {
setState(() {
_logs.add('State: ${state.description}');
});
});
// Listen to messages
_client?.messageStream.listen((message) {
setState(() {
if (message.metadata?['isHeartbeat'] == true) {
_logs.add('Heartbeat: ${message.data}');
} else {
_messages.add('Received: ${message.data}');
}
});
});
// Listen to errors
_client?.errorStream.listen((error) {
setState(() {
_logs.add('Error: $error');
});
});
// Listen to logs
_client?.logStream.listen((log) {
setState(() {
_logs.add(log);
});
});
// Listen to statistics
_client?.statsStream.listen((stats) {
setState(() {
_stats = stats;
});
});
}
Future<void> _connect() async {
try {
await _client?.connect();
} catch (error) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Connection failed: $error')));
}
}
Future<void> _disconnect() async {
await _client?.disconnect();
}
Future<void> _forceReconnect() async {
await _client?.forceReconnect();
}
Future<void> _sendMessage() async {
if (_messageController.text.isNotEmpty) {
try {
await _client?.sendJson({
"type": "broadcast",
"content": _messageController.text,
"username": "User2",
});
setState(() {
_messages.add('Sent: ${_messageController.text}');
});
_messageController.clear();
} catch (error) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Send failed: $error')));
}
}
}
Future<void> _sendJsonMessage() async {
try {
final jsonMessage = {
"type": "broadcast",
"content": _messageController.text,
"username": "FlutterUser", // 保持字段名一致
};
await _client?.sendJson(jsonMessage);
setState(() {
_messages.add('Sent JSON: $jsonMessage');
});
} catch (error) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('JSON send failed: $error')));
}
}
void _clearMessages() {
setState(() {
_messages.clear();
});
}
void _clearLogs() {
setState(() {
_logs.clear();
});
}
@override
void dispose() {
_client?.dispose();
_messageController.dispose();
_urlController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Enhanced WebSocket Plugin Demo'),
backgroundColor: Colors.blue,
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
// Connection controls
Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: _urlController,
decoration: InputDecoration(
labelText: 'WebSocket URL',
border: OutlineInputBorder(),
),
onChanged: (value) {
_initializeWebSocket();
},
),
SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: (_client?.isConnected ==true)? null : _connect,
child: Text('Connect'),
),
),
SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: (_client?.isConnected==true) ? _disconnect : null,
child: Text('Disconnect'),
),
),
SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: _forceReconnect,
child: Text('Force Reconnect'),
),
),
],
),
SizedBox(height: 8),
Text(
'Status: ${_client?.currentState.description}',
style: TextStyle(
fontWeight: FontWeight.bold,
color: (_client?.isConnected ?? true)? Colors.green : Colors.red,
),
),
],
),
),
),
SizedBox(height: 16),
// Statistics
if (_stats.isNotEmpty)
Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Connection Statistics',
style: TextStyle(fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text(
'Heartbeat Active: ${_stats['heartbeat']?['isActive'] ?? false}',
),
Text(
'Missed Heartbeats: ${_stats['heartbeat']?['missedHeartbeats'] ?? 0}',
),
Text(
'Reconnect Attempts: ${_stats['reconnection']?['reconnectAttempts'] ?? 0}',
),
Text(
'Is Reconnecting: ${_stats['reconnection']?['isReconnecting'] ?? false}',
),
],
),
),
),
SizedBox(height: 16),
// Message sending
Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: _messageController,
decoration: InputDecoration(
labelText: 'Message',
border: OutlineInputBorder(),
),
onSubmitted: (_) => _sendMessage(),
),
SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: (_client?.isConnected ?? true)
? _sendMessage
: null,
child: Text('Send Text'),
),
),
SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: (_client?.isConnected??true)
? _sendJsonMessage
: null,
child: Text('Send JSON'),
),
),
],
),
],
),
),
),
SizedBox(height: 16),
// Messages and logs
Expanded(
child: Row(
children: [
// Messages
Expanded(
child: Card(
child: Column(
children: [
Padding(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Messages',
style: TextStyle(fontWeight: FontWeight.bold),
),
TextButton(
onPressed: _clearMessages,
child: Text('Clear'),
),
],
),
),
Expanded(
child: ListView.builder(
itemCount: _messages.length,
itemBuilder: (context, index) {
return Padding(
padding: EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 2.0,
),
child: Text(
_messages[index],
style: TextStyle(fontSize: 12),
),
);
},
),
),
],
),
),
),
SizedBox(width: 8),
// Logs
Expanded(
child: Card(
child: Column(
children: [
Padding(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Logs & Heartbeat',
style: TextStyle(fontWeight: FontWeight.bold),
),
TextButton(
onPressed: _clearLogs,
child: Text('Clear'),
),
],
),
),
Expanded(
child: ListView.builder(
itemCount: _logs.length,
itemBuilder: (context, index) {
final log = _logs[index];
final isHeartbeat =
log.contains('Heartbeat:') ||
log.contains('heartbeat');
return Padding(
padding: EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 2.0,
),
child: Text(
log,
style: TextStyle(
fontSize: 10,
color: isHeartbeat
? Colors.blue[600]
: Colors.grey[600],
),
),
);
},
),
),
],
),
),
),
],
),
),
],
),
),
);
}
}