Flutter for OpenHarmony 二维码扫描App实战 - WiFi二维码生成实现
override控制 WiFi 名称输入框,控制密码输入框。存储选择的加密类型,默认是 WPA。_hidden表示是否是隐藏网络。控制密码是否明文显示。dispose 中释放两个 TextEditingController,避免内存泄漏。
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
更多推荐

所有评论(0)