wifi_plus 0.0.2
wifi_plus: ^0.0.2 copied to clipboard
A robust and feature-rich Flutter plugin to scan, connect, disconnect, and manage Wi-Fi networks on Windows desktop applications.
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:wifi_plus/wifi_plus.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
WifiConnection? _connection;
List<WifiNetwork> _networks = [];
List<WifiNetwork> _savedNetworks = [];
bool _loading = false;
String _statusMessage = 'Idle';
// Stream status variables
NetworkStatus? _networkStatus;
bool _captivePortalRequired = false;
int? _currentSignalStrength;
// Stream logs
final List<String> _streamLogs = [];
final ScrollController _logScrollController = ScrollController();
// Stream subscriptions
StreamSubscription<List<WifiNetwork>>? _networksSubscription;
StreamSubscription<WifiConnection?>? _connectionSubscription;
StreamSubscription<int>? _signalSubscription;
StreamSubscription<NetworkStatus>? _internetSubscription;
StreamSubscription<bool>? _captivePortalSubscription;
// Auto-scan state
bool _autoScanEnabled = true;
Timer? _autoScanTimer;
bool _backgroundScanning = false;
List<WifiNetwork> _deduplicateNetworks(List<WifiNetwork> networks) {
final seen = <String>{};
final unique = <WifiNetwork>[];
for (final net in networks) {
final key = net.bssid.isNotEmpty
? '${net.ssid}_${net.bssid}'
: '${net.ssid}_${net.frequency}_${net.channel}';
if (seen.add(key)) {
unique.add(net);
}
}
return unique;
}
void _startAutoScan() {
_autoScanTimer?.cancel();
_autoScanTimer = Timer.periodic(const Duration(seconds: 10), (timer) async {
if (_loading || _backgroundScanning || !mounted) return;
setState(() => _backgroundScanning = true);
try {
final nets = await WifiManager.scan();
final uniqueNets = _deduplicateNetworks(nets);
if (mounted) {
setState(() {
_networks = uniqueNets;
});
}
} catch (e) {
_addLog('Auto-scan error: $e');
} finally {
if (mounted) {
setState(() => _backgroundScanning = false);
}
}
});
}
void _stopAutoScan() {
_autoScanTimer?.cancel();
_autoScanTimer = null;
}
@override
void initState() {
super.initState();
_loadCurrentConnection();
_loadSavedNetworks();
_setupSubscriptions();
_startAutoScan();
}
@override
void dispose() {
_autoScanTimer?.cancel();
_networksSubscription?.cancel();
_connectionSubscription?.cancel();
_signalSubscription?.cancel();
_internetSubscription?.cancel();
_captivePortalSubscription?.cancel();
_logScrollController.dispose();
super.dispose();
}
void _addLog(String log) {
final timestamp = DateTime.now().toIso8601String().substring(11, 19);
setState(() {
_streamLogs.add('[$timestamp] $log');
});
// Auto scroll logs to bottom
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_logScrollController.hasClients) {
_logScrollController.jumpTo(_logScrollController.position.maxScrollExtent);
}
});
}
void _setupSubscriptions() {
_networksSubscription = WifiManager.onNetworksChanged.listen((nets) {
final uniqueNets = _deduplicateNetworks(nets);
_addLog('Networks updated: Found ${nets.length} networks (unique: ${uniqueNets.length})');
setState(() {
_networks = uniqueNets;
});
}, onError: (e) {
_addLog('Error on networks stream: $e');
});
_connectionSubscription = WifiManager.onConnectionChanged.listen((conn) {
_addLog(conn != null ? 'Connected to ${conn.ssid}' : 'Disconnected from Wi-Fi');
setState(() {
_connection = conn;
if (conn != null) {
_currentSignalStrength = conn.signalStrength;
} else {
_currentSignalStrength = null;
}
});
}, onError: (e) {
_addLog('Error on connection stream: $e');
});
_signalSubscription = WifiManager.onSignalChanged.listen((sig) {
_addLog('Signal strength changed: $sig%');
setState(() {
_currentSignalStrength = sig;
});
}, onError: (e) {
_addLog('Error on signal stream: $e');
});
_internetSubscription = WifiManager.onInternetChanged.listen((status) {
_addLog('Internet status changed: $status');
setState(() {
_networkStatus = status;
});
}, onError: (e) {
_addLog('Error on internet stream: $e');
});
_captivePortalSubscription = WifiManager.onCaptivePortalDetected.listen((detected) {
_addLog('Captive portal detected: $detected');
setState(() {
_captivePortalRequired = detected;
});
}, onError: (e) {
_addLog('Error on captive portal stream: $e');
});
}
Future<void> _loadCurrentConnection() async {
setState(() => _loading = true);
try {
final conn = await WifiManager.currentConnection();
setState(() {
_connection = conn;
_statusMessage = conn != null ? 'Connected to ${conn.ssid}' : 'Disconnected';
if (conn != null) {
_currentSignalStrength = conn.signalStrength;
}
});
} catch (e) {
setState(() => _statusMessage = 'Error loading connection: $e');
} finally {
setState(() => _loading = false);
}
}
Future<void> _loadSavedNetworks() async {
setState(() => _loading = true);
try {
final saved = await WifiManager.savedNetworks();
setState(() {
_savedNetworks = saved;
});
} catch (e) {
setState(() => _statusMessage = 'Error loading saved networks: $e');
} finally {
setState(() => _loading = false);
}
}
Future<void> _scanNetworks() async {
setState(() {
_loading = true;
_statusMessage = 'Scanning...';
});
try {
final nets = await WifiManager.scan();
final uniqueNets = _deduplicateNetworks(nets);
setState(() {
_networks = uniqueNets;
_statusMessage = 'Scan complete. Found ${nets.length} networks (unique: ${uniqueNets.length}).';
});
} catch (e) {
setState(() => _statusMessage = 'Error scanning: $e');
} finally {
setState(() => _loading = false);
}
}
Future<void> _disconnect() async {
setState(() {
_loading = true;
_statusMessage = 'Disconnecting...';
});
try {
await WifiManager.disconnect();
setState(() {
_statusMessage = 'Disconnected';
});
await _loadCurrentConnection();
} catch (e) {
setState(() => _statusMessage = 'Error disconnecting: $e');
} finally {
setState(() => _loading = false);
}
}
Future<void> _forget(String ssid) async {
setState(() {
_loading = true;
_statusMessage = 'Forgetting $ssid...';
});
try {
await WifiManager.forget(ssid);
setState(() {
_statusMessage = 'Forgot network: $ssid';
});
await _loadSavedNetworks();
} catch (e) {
setState(() => _statusMessage = 'Error forgetting network: $e');
} finally {
setState(() => _loading = false);
}
}
Future<void> _openCaptivePortal() async {
setState(() {
_loading = true;
_statusMessage = 'Opening captive portal...';
});
try {
await WifiManager.openCaptivePortal();
setState(() {
_statusMessage = 'Captive portal opened.';
});
} catch (e) {
setState(() => _statusMessage = 'Error opening captive portal: $e');
} finally {
setState(() => _loading = false);
}
}
Future<void> _connect(String ssid, String? password, String? username) async {
try {
setState(() {
_loading = true;
_statusMessage = 'Connecting to $ssid...';
});
await WifiManager.connect(ssid: ssid, password: password, username: username);
setState(() {
_statusMessage = 'Connected successfully to $ssid';
});
await _loadCurrentConnection();
await _loadSavedNetworks();
} catch (e) {
setState(() => _statusMessage = 'Connection failed: $e');
} finally {
setState(() => _loading = false);
}
}
void _showConnectDialog(BuildContext listContext, WifiNetwork network) {
final isEnterprise = network.security == WifiSecurity.wpaEnterprise ||
network.security == WifiSecurity.wpa2Enterprise ||
network.security == WifiSecurity.wpa3Enterprise;
final needsPassword = network.security != WifiSecurity.open && network.security != WifiSecurity.unknown;
final usernameController = TextEditingController();
final passwordController = TextEditingController();
showDialog(
context: listContext,
builder: (dialogContext) {
return AlertDialog(
title: Text('Connect to ${network.ssid}'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Security: ${network.security.name}'),
const SizedBox(height: 10),
if (isEnterprise)
TextField(
controller: usernameController,
decoration: const InputDecoration(
labelText: 'Username (Enterprise)',
border: OutlineInputBorder(),
),
),
if (isEnterprise) const SizedBox(height: 10),
if (needsPassword)
TextField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
),
if (!needsPassword)
const Text('This is an open network. No password is required.'),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
final password = needsPassword ? passwordController.text : null;
final username = isEnterprise ? usernameController.text : null;
Navigator.of(dialogContext).pop();
Future.microtask(() {
_connect(network.ssid, password, username);
});
},
child: const Text('Connect'),
),
],
);
},
);
}
Widget _buildConnectionDetails() {
return Card(
elevation: 4,
margin: const EdgeInsets.symmetric(vertical: 4),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Current Connection Details',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
if (_connection != null)
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent,
foregroundColor: Colors.white,
),
onPressed: _disconnect,
icon: const Icon(Icons.link_off),
label: const Text('Disconnect'),
),
],
),
const SizedBox(height: 8),
if (_connection != null) ...[
Wrap(
spacing: 16,
runSpacing: 8,
children: [
_buildDetailItem('SSID', _connection!.ssid),
_buildDetailItem('IP Address', _connection!.ipAddress ?? 'N/A'),
_buildDetailItem('MAC Address', _connection!.macAddress ?? 'N/A'),
_buildDetailItem('Gateway', _connection!.gateway ?? 'N/A'),
_buildDetailItem('DNS', _connection!.dns?.join(', ') ?? 'N/A'),
_buildDetailItem('Adapter', _connection!.adapterName),
_buildDetailItem('Security', _connection!.security.name),
_buildDetailItem(
'Signal Strength',
'${_currentSignalStrength ?? _connection!.signalStrength}% (${_connection!.signalQuality.name})',
),
_buildDetailItem('Speed (Rx/Tx)', '${_connection!.rxSpeed} / ${_connection!.txSpeed} Mbps'),
],
),
] else ...[
const Text('No active Wi-Fi connection detected.'),
]
],
),
),
);
}
Widget _buildDetailItem(String label, String value) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: const TextStyle(fontSize: 11, color: Colors.grey),
),
const SizedBox(height: 2),
Text(
value,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
),
],
);
}
Widget _buildStatusDashboard() {
final internet = _networkStatus?.internetAvailable ?? _connection?.internet ?? false;
final metered = _networkStatus?.isMetered ?? false;
final roaming = _networkStatus?.isRoaming ?? false;
final vpn = _networkStatus?.isVpnActive ?? false;
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
children: [
_buildStatusChip(
label: 'Internet: ${internet ? "Available" : "Offline"}',
isActive: internet,
activeColor: Colors.green,
inactiveColor: Colors.grey,
icon: internet ? Icons.cloud_done : Icons.cloud_off,
),
const SizedBox(width: 8),
_buildStatusChip(
label: 'VPN: ${vpn ? "Active" : "Inactive"}',
isActive: vpn,
activeColor: Colors.blue,
inactiveColor: Colors.grey,
icon: Icons.vpn_lock,
),
const SizedBox(width: 8),
_buildStatusChip(
label: 'Metered: ${metered ? "Yes" : "No"}',
isActive: metered,
activeColor: Colors.orange,
inactiveColor: Colors.grey,
icon: Icons.data_usage,
),
const SizedBox(width: 8),
_buildStatusChip(
label: 'Roaming: ${roaming ? "Yes" : "No"}',
isActive: roaming,
activeColor: Colors.purple,
inactiveColor: Colors.grey,
icon: Icons.cell_tower,
),
const SizedBox(width: 8),
_buildStatusChip(
label: _captivePortalRequired ? 'Portal Required (Click)' : 'Portal Not Required',
isActive: _captivePortalRequired,
activeColor: Colors.red,
inactiveColor: Colors.grey,
icon: Icons.door_sliding,
onTap: _captivePortalRequired ? _openCaptivePortal : null,
),
],
),
),
);
}
Widget _buildStatusChip({
required String label,
required bool isActive,
required Color activeColor,
required Color inactiveColor,
required IconData icon,
VoidCallback? onTap,
}) {
return ActionChip(
avatar: Icon(
icon,
color: Colors.white,
size: 16,
),
label: Text(
label,
style: const TextStyle(color: Colors.white, fontSize: 11),
),
backgroundColor: isActive ? activeColor : inactiveColor.withAlpha(80),
side: BorderSide.none,
onPressed: onTap ?? () {},
);
}
Widget _buildAvailableNetworksTab() {
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
'Scanned Networks (${_networks.length})',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
if (_backgroundScanning) ...[
const SizedBox(width: 8),
const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(strokeWidth: 2),
),
],
],
),
Row(
children: [
const Text('Auto-scan', style: TextStyle(fontSize: 12)),
Switch(
value: _autoScanEnabled,
onChanged: (val) {
setState(() {
_autoScanEnabled = val;
});
if (val) {
_startAutoScan();
} else {
_stopAutoScan();
}
},
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: _scanNetworks,
icon: const Icon(Icons.search),
label: const Text('Scan'),
),
],
),
],
),
),
Expanded(
child: _networks.isEmpty
? const Center(child: Text('No scanned networks. Press Scan to search.'))
: ListView.builder(
itemCount: _networks.length,
itemBuilder: (context, index) {
final net = _networks[index];
final isCurrent = net.connected || (_connection != null && _connection!.ssid == net.ssid);
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
shape: isCurrent
? RoundedRectangleBorder(
side: BorderSide(color: Colors.green.shade400, width: 2),
borderRadius: BorderRadius.circular(8),
)
: null,
color: isCurrent ? Colors.green.withAlpha(20) : null,
child: ListTile(
dense: true,
leading: Icon(
isCurrent ? Icons.wifi_lock : Icons.wifi,
color: isCurrent ? Colors.green : Colors.blue,
),
title: Row(
children: [
Expanded(
child: Text(
net.ssid.isEmpty ? '[Hidden Network]' : net.ssid,
style: TextStyle(
fontWeight: isCurrent ? FontWeight.bold : FontWeight.normal,
),
),
),
if (isCurrent) ...[
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.green.shade700,
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'CONNECTED',
style: TextStyle(fontSize: 9, fontWeight: FontWeight.bold, color: Colors.white),
),
),
],
],
),
subtitle: Text(
'Band: ${net.band.name} (${net.frequency} MHz) | Ch: ${net.channel} | Security: ${net.security.name}\nBSSID: ${net.bssid}',
),
isThreeLine: true,
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('${net.signalStrength}%'),
const SizedBox(width: 8),
if (!isCurrent)
IconButton(
icon: const Icon(Icons.login),
tooltip: 'Connect',
onPressed: () => _showConnectDialog(context, net),
),
],
),
),
);
},
),
),
],
);
}
Widget _buildSavedNetworksTab() {
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Saved Networks (${_savedNetworks.length})',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
ElevatedButton.icon(
onPressed: _loadSavedNetworks,
icon: const Icon(Icons.refresh),
label: const Text('Refresh'),
),
],
),
),
Expanded(
child: _savedNetworks.isEmpty
? const Center(child: Text('No saved network profiles found.'))
: ListView.builder(
itemCount: _savedNetworks.length,
itemBuilder: (context, index) {
final net = _savedNetworks[index];
return Card(
margin: const EdgeInsets.symmetric(vertical: 2),
child: ListTile(
dense: true,
leading: const Icon(Icons.bookmark_added, color: Colors.amber),
title: Text(net.ssid.isEmpty ? '[Hidden Network]' : net.ssid),
subtitle: Text('Security: ${net.security.name}'),
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.redAccent),
tooltip: 'Forget Profile',
onPressed: () => _forget(net.ssid),
),
),
);
},
),
),
],
);
}
Widget _buildStreamLogs() {
return Card(
elevation: 2,
color: Colors.black87,
margin: const EdgeInsets.only(top: 8),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
height: 120,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Live Platform Event Logs',
style: TextStyle(
fontFamily: 'Courier',
color: Colors.greenAccent,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: const Size(40, 20),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onPressed: () => setState(() => _streamLogs.clear()),
child: const Text(
'Clear',
style: TextStyle(color: Colors.redAccent, fontSize: 10),
),
),
],
),
const Divider(color: Colors.greenAccent, height: 8),
Expanded(
child: _streamLogs.isEmpty
? const Center(
child: Text(
'No events logged yet. Trigger changes (like scan/connect) to see events.',
style: TextStyle(fontFamily: 'Courier', color: Colors.grey, fontSize: 11),
),
)
: ListView.builder(
controller: _logScrollController,
itemCount: _streamLogs.length,
itemBuilder: (context, index) {
return Text(
_streamLogs[index],
style: const TextStyle(
fontFamily: 'Courier',
color: Colors.lightGreen,
fontSize: 11,
),
);
},
),
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark(useMaterial3: true),
home: DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: const Text('Wi-Fi Manager Plugin Example'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
tooltip: 'Refresh Connection',
onPressed: () {
_loadCurrentConnection();
_loadSavedNetworks();
},
),
IconButton(
icon: const Icon(Icons.door_sliding),
tooltip: 'Open Captive Portal',
onPressed: _openCaptivePortal,
),
],
),
body: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
if (_loading) const LinearProgressIndicator(),
const SizedBox(height: 5),
Container(
padding: const EdgeInsets.all(8),
width: double.infinity,
color: Colors.grey.withAlpha(30),
child: Text(
'Status: $_statusMessage',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 5),
_buildConnectionDetails(),
const SizedBox(height: 5),
_buildStatusDashboard(),
const SizedBox(height: 5),
const TabBar(
tabs: [
Tab(text: 'Available Networks', icon: Icon(Icons.wifi)),
Tab(text: 'Saved Profiles', icon: Icon(Icons.bookmark)),
],
),
Expanded(
child: TabBarView(
children: [
_buildAvailableNetworksTab(),
_buildSavedNetworksTab(),
],
),
),
_buildStreamLogs(),
],
),
),
),
),
);
}
}