WiFi 二维码是一种非常实用的二维码类型。用户扫描后可以直接连接 WiFi,不需要手动输入密码。这在咖啡厅、酒店、办公室等场景下特别方便。这篇文章介绍 WiFi 二维码生成页面的实现,包括 WiFi 信息输入、加密方式选择、隐藏网络设置等功能。

WiFi 二维码的格式

WiFi 二维码使用特定的格式编码 WiFi 信息:

WIFI:T:<加密类型>;S:<网络名称>;P:<密码>;H:<是否隐藏>;;

各字段说明:

T (Type):加密类型,可选值有 WPA、WEP、nopass(无密码)

S (SSID):WiFi 网络名称

P (Password):WiFi 密码

H (Hidden):是否是隐藏网络,true 或 false

例如:WIFI:T:WPA;S:MyWiFi;P:12345678;H:false;;

WifiQrView 的基础结构

先来看文件的导入和类定义:

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../data/models/qr_record.dart';
import 'generate_controller.dart';

class WifiQrView extends StatefulWidget {
  const WifiQrView({super.key});

  
  State<WifiQrView> createState() => _WifiQrViewState();
}

导入了必要的包。WifiQrView 使用 StatefulWidget,因为需要管理多个输入框和选项的状态。

状态类的定义

状态类中定义了多个控制器和状态变量:

class _WifiQrViewState extends State<WifiQrView> {
  final _ssidController = TextEditingController();
  final _passwordController = TextEditingController();
  String _encryption = 'WPA';
  bool _hidden = false;
  bool _showPassword = false;
  final _controller = Get.find<GenerateController>();

  
  void dispose() {
    _ssidController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

_ssidController 控制 WiFi 名称输入框,_passwordController 控制密码输入框。

_encryption 存储选择的加密类型,默认是 WPA。_hidden 表示是否是隐藏网络。_showPassword 控制密码是否明文显示。

dispose 中释放两个 TextEditingController,避免内存泄漏。

WiFi 字符串生成方法

生成 WiFi 格式字符串的方法:

  String _generateWifiString() {
    return 'WIFI:T:$_encryption;S:${_ssidController.text};P:${_passwordController.text};H:$_hidden;;';
  }

使用字符串插值拼接 WiFi 格式字符串。各字段按照标准格式排列,以分号分隔,最后以双分号结尾。

这个方法在点击生成按钮时调用,将用户输入的信息转换为二维码内容。

Scaffold 和 AppBar

build 方法返回页面结构:

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('WiFi二维码')),
      body: SingleChildScrollView(
        padding: EdgeInsets.all(16.w),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [

Scaffold 提供基础页面结构,AppBar 标题为"WiFi二维码"。body 使用 SingleChildScrollView 包裹,因为内容较多,需要支持滚动。

WiFi 名称输入

第一个输入项是 WiFi 名称:

            Text('WiFi名称 (SSID)', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500)),
            SizedBox(height: 8.h),
            TextField(
              controller: _ssidController,
              decoration: InputDecoration(
                hintText: '输入WiFi名称',
                prefixIcon: const Icon(Icons.wifi),
                border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
              ),
            ),
            SizedBox(height: 16.h),

标题使用 14.sp 字号和中等字重。括号中的 SSID 是技术术语,帮助了解技术的用户理解。

TextField 的 prefixIcon 使用 WiFi 图标,让用户一眼就知道这是输入 WiFi 名称的地方。

密码输入

密码输入框有显示/隐藏切换功能:

            Text('密码', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500)),
            SizedBox(height: 8.h),
            TextField(
              controller: _passwordController,
              obscureText: !_showPassword,
              decoration: InputDecoration(
                hintText: '输入WiFi密码',
                prefixIcon: const Icon(Icons.lock),
                suffixIcon: IconButton(
                  icon: Icon(_showPassword ? Icons.visibility_off : Icons.visibility),
                  onPressed: () => setState(() => _showPassword = !_showPassword),
                ),
                border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
              ),
            ),
            SizedBox(height: 16.h),

obscureText 根据 _showPassword 状态决定是否隐藏密码。suffixIcon 是一个切换按钮,点击后切换密码显示状态。

图标使用 visibility 和 visibility_off,符合用户的直觉:眼睛图标表示可以看到密码,划掉的眼睛表示密码被隐藏。

加密方式选择

使用 SegmentedButton 选择加密方式:

            Text('加密方式', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500)),
            SizedBox(height: 8.h),
            SegmentedButton<String>(
              segments: const [
                ButtonSegment(value: 'WPA', label: Text('WPA/WPA2')),
                ButtonSegment(value: 'WEP', label: Text('WEP')),
                ButtonSegment(value: 'nopass', label: Text('无密码')),
              ],
              selected: {_encryption},
              onSelectionChanged: (value) => setState(() => _encryption = value.first),
            ),
            SizedBox(height: 16.h),

SegmentedButton 是 Material 3 的分段按钮组件,适合在几个选项中选择一个。

三个选项分别是 WPA/WPA2(最常用)、WEP(老旧的加密方式)和无密码。selected 是一个 Set,包含当前选中的值。onSelectionChanged 在选择变化时更新状态。

隐藏网络开关

使用 SwitchListTile 设置是否是隐藏网络:

            SwitchListTile(
              title: const Text('隐藏网络'),
              subtitle: const Text('WiFi名称不广播'),
              value: _hidden,
              onChanged: (value) => setState(() => _hidden = value),
            ),
          ],
        ),
      ),

SwitchListTile 是带开关的列表项,适合布尔值设置。title 是主标题,subtitle 是说明文字,解释什么是隐藏网络。

隐藏网络是指不广播 SSID 的 WiFi,需要手动输入名称才能连接。这个选项对于安全性要求较高的网络很有用。

底部生成按钮

页面底部是生成按钮:

      bottomNavigationBar: SafeArea(
        child: Padding(
          padding: EdgeInsets.all(16.w),
          child: ElevatedButton(
            onPressed: () => _controller.generateQr(_generateWifiString(), QrType.wifi),
            style: ElevatedButton.styleFrom(
              minimumSize: Size(double.infinity, 48.h),
              backgroundColor: Theme.of(context).primaryColor,
              foregroundColor: Colors.white,
            ),
            child: const Text('生成二维码'),
          ),
        ),
      ),
    );
  }
}

点击按钮时调用 _generateWifiString() 生成 WiFi 格式字符串,然后调用控制器的 generateQr 方法生成二维码。

输入验证

在生成前验证输入:

void _handleGenerate() {
  final ssid = _ssidController.text.trim();
  final password = _passwordController.text;
  
  if (ssid.isEmpty) {
    Get.snackbar('提示', '请输入WiFi名称', snackPosition: SnackPosition.BOTTOM);
    return;
  }
  
  if (_encryption != 'nopass' && password.isEmpty) {
    Get.snackbar('提示', '请输入WiFi密码', snackPosition: SnackPosition.BOTTOM);
    return;
  }
  
  if (_encryption == 'WPA' && password.length < 8) {
    Get.snackbar('提示', 'WPA密码至少需要8位', snackPosition: SnackPosition.BOTTOM);
    return;
  }
  
  _controller.generateQr(_generateWifiString(), QrType.wifi);
}

验证逻辑包括:WiFi 名称不能为空、有密码的加密方式需要输入密码、WPA 密码至少 8 位。

这些验证确保生成的二维码是有效的,用户扫描后可以正常连接。

加密方式联动

选择"无密码"时禁用密码输入框:

TextField(
  controller: _passwordController,
  enabled: _encryption != 'nopass',
  obscureText: !_showPassword,
  decoration: InputDecoration(
    hintText: _encryption == 'nopass' ? '无需密码' : '输入WiFi密码',
    prefixIcon: const Icon(Icons.lock),
    suffixIcon: _encryption != 'nopass'
        ? IconButton(
            icon: Icon(_showPassword ? Icons.visibility_off : Icons.visibility),
            onPressed: () => setState(() => _showPassword = !_showPassword),
          )
        : null,
    border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
  ),
),

enabled 根据加密方式决定是否可编辑。hintText 也根据加密方式显示不同的提示。无密码时不显示显示/隐藏按钮。

获取当前 WiFi 信息

提供获取当前连接的 WiFi 信息的功能:

import 'package:network_info_plus/network_info_plus.dart';

Future<void> _getCurrentWifi() async {
  final info = NetworkInfo();
  final wifiName = await info.getWifiName();
  
  if (wifiName != null) {
    // 去除引号
    final ssid = wifiName.replaceAll('"', '');
    _ssidController.text = ssid;
    Get.snackbar('成功', '已获取当前WiFi名称', snackPosition: SnackPosition.BOTTOM);
  } else {
    Get.snackbar('提示', '无法获取WiFi名称,请手动输入', snackPosition: SnackPosition.BOTTOM);
  }
}

使用 network_info_plus 插件获取当前连接的 WiFi 名称。注意需要位置权限才能获取 WiFi 名称。

获取 WiFi 按钮

在 WiFi 名称输入框旁边添加获取按钮:

Row(
  children: [
    Expanded(
      child: TextField(
        controller: _ssidController,
        decoration: InputDecoration(
          hintText: '输入WiFi名称',
          prefixIcon: const Icon(Icons.wifi),
          border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
        ),
      ),
    ),
    SizedBox(width: 8.w),
    IconButton(
      icon: const Icon(Icons.wifi_find),
      onPressed: _getCurrentWifi,
      tooltip: '获取当前WiFi',
    ),
  ],
),

使用 Row 让输入框和按钮水平排列。IconButton 使用 wifi_find 图标,tooltip 提供悬停提示。

WiFi 信息预览

显示将要生成的 WiFi 信息预览:

Widget _buildPreview() {
  if (_ssidController.text.isEmpty) {
    return const SizedBox.shrink();
  }
  
  return Container(
    margin: EdgeInsets.only(top: 16.h),
    padding: EdgeInsets.all(12.w),
    decoration: BoxDecoration(
      color: Colors.blue.withOpacity(0.1),
      borderRadius: BorderRadius.circular(8.r),
      border: Border.all(color: Colors.blue.withOpacity(0.3)),
    ),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          children: [
            const Icon(Icons.wifi, color: Colors.blue),
            SizedBox(width: 8.w),
            Text(
              _ssidController.text,
              style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600),
            ),
          ],
        ),
        SizedBox(height: 8.h),
        Text('加密方式: ${_getEncryptionLabel()}', style: TextStyle(fontSize: 13.sp)),
        if (_encryption != 'nopass')
          Text('密码: ${_showPassword ? _passwordController.text : '••••••••'}',
              style: TextStyle(fontSize: 13.sp)),
        if (_hidden)
          Text('隐藏网络: 是', style: TextStyle(fontSize: 13.sp)),
      ],
    ),
  );
}

String _getEncryptionLabel() {
  switch (_encryption) {
    case 'WPA':
      return 'WPA/WPA2';
    case 'WEP':
      return 'WEP';
    case 'nopass':
      return '无密码';
    default:
      return _encryption;
  }
}

预览区域显示 WiFi 名称、加密方式、密码(根据显示状态)和是否隐藏。使用蓝色主题,与 WiFi 图标颜色一致。

密码强度提示

显示密码强度提示:

Widget _buildPasswordStrength() {
  final password = _passwordController.text;
  
  if (password.isEmpty || _encryption == 'nopass') {
    return const SizedBox.shrink();
  }
  
  int strength = 0;
  if (password.length >= 8) strength++;
  if (password.length >= 12) strength++;
  if (RegExp(r'[A-Z]').hasMatch(password)) strength++;
  if (RegExp(r'[0-9]').hasMatch(password)) strength++;
  if (RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password)) strength++;
  
  String label;
  Color color;
  
  if (strength <= 2) {
    label = '弱';
    color = Colors.red;
  } else if (strength <= 3) {
    label = '中';
    color = Colors.orange;
  } else {
    label = '强';
    color = Colors.green;
  }
  
  return Padding(
    padding: EdgeInsets.only(top: 8.h),
    child: Row(
      children: [
        Text('密码强度: ', style: TextStyle(fontSize: 12.sp, color: Colors.grey)),
        Container(
          padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.h),
          decoration: BoxDecoration(
            color: color.withOpacity(0.1),
            borderRadius: BorderRadius.circular(4.r),
          ),
          child: Text(label, style: TextStyle(fontSize: 12.sp, color: color)),
        ),
      ],
    ),
  );
}

根据密码长度、是否包含大写字母、数字、特殊字符计算强度分数,显示弱/中/强三个等级。

生成随机密码

提供生成随机密码的功能:

import 'dart:math';

void _generateRandomPassword() {
  const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#\$%^&*';
  final random = Random.secure();
  final password = List.generate(12, (_) => chars[random.nextInt(chars.length)]).join();
  
  _passwordController.text = password;
  setState(() => _showPassword = true);
  
  Get.snackbar('成功', '已生成随机密码', snackPosition: SnackPosition.BOTTOM);
}

使用 Random.secure() 生成安全的随机数。密码包含大小写字母、数字和特殊字符,长度 12 位。生成后自动显示密码,方便用户查看。

复制 WiFi 信息

提供复制 WiFi 信息的功能:

void _copyWifiInfo() {
  final info = '''
WiFi名称: ${_ssidController.text}
密码: ${_passwordController.text}
加密方式: ${_getEncryptionLabel()}
''';
  
  Clipboard.setData(ClipboardData(text: info));
  Get.snackbar('成功', '已复制WiFi信息', snackPosition: SnackPosition.BOTTOM);
}

将 WiFi 信息格式化后复制到剪贴板,方便用户通过其他方式分享。

保存为模板

保存常用的 WiFi 配置为模板:

void _saveAsTemplate() async {
  final prefs = await SharedPreferences.getInstance();
  final templates = prefs.getStringList('wifi_templates') ?? [];
  
  final template = jsonEncode({
    'ssid': _ssidController.text,
    'password': _passwordController.text,
    'encryption': _encryption,
    'hidden': _hidden,
  });
  
  if (!templates.contains(template)) {
    templates.add(template);
    await prefs.setStringList('wifi_templates', templates);
    Get.snackbar('成功', '已保存为模板', snackPosition: SnackPosition.BOTTOM);
  } else {
    Get.snackbar('提示', '模板已存在', snackPosition: SnackPosition.BOTTOM);
  }
}

将 WiFi 配置序列化为 JSON 保存到 SharedPreferences。下次可以快速加载模板。

加载模板

显示已保存的模板列表:

Widget _buildTemplates() {
  return FutureBuilder<List<Map<String, dynamic>>>(
    future: _loadTemplates(),
    builder: (context, snapshot) {
      if (!snapshot.hasData || snapshot.data!.isEmpty) {
        return const SizedBox.shrink();
      }
      
      return Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(height: 16.h),
          Text('已保存的模板', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500)),
          SizedBox(height: 8.h),
          ...snapshot.data!.map((template) => ListTile(
            dense: true,
            leading: const Icon(Icons.wifi),
            title: Text(template['ssid']),
            subtitle: Text(template['encryption']),
            onTap: () => _loadTemplate(template),
          )),
        ],
      );
    },
  );
}

void _loadTemplate(Map<String, dynamic> template) {
  _ssidController.text = template['ssid'];
  _passwordController.text = template['password'];
  setState(() {
    _encryption = template['encryption'];
    _hidden = template['hidden'];
  });
}

使用 FutureBuilder 异步加载模板列表。点击模板可以快速填充所有字段。

小结

WiFi 二维码生成页面需要处理多个输入字段和选项,包括 WiFi 名称、密码、加密方式和隐藏网络设置。页面使用 StatefulWidget 管理这些状态。

WiFi 字符串格式是标准的,需要按照规范拼接。输入验证确保生成的二维码是有效的。密码显示/隐藏、获取当前 WiFi、生成随机密码等功能提升了用户体验。

WiFi 二维码在日常生活中非常实用,一个好用的生成页面可以让分享 WiFi 变得更加方便。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐