wifi_plus 0.0.2 copy "wifi_plus: ^0.0.2" to clipboard
wifi_plus: ^0.0.2 copied to clipboard

PlatformWindows

A robust and feature-rich Flutter plugin to scan, connect, disconnect, and manage Wi-Fi networks on Windows desktop applications.

example/lib/main.dart

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(),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
0
likes
160
points
46
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A robust and feature-rich Flutter plugin to scan, connect, disconnect, and manage Wi-Fi networks on Windows desktop applications.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on wifi_plus

Packages that implement wifi_plus