Flutter for OpenHarmony 实战:network_info_plus 网络扫描与隐私合规深度适配

前言
做 IoT(物联网)配网、局域网文件互传(类似华为分享)、或简单的 WiFi 测速 App 时,我们需要获取当前连接的 WiFi SSID (名称) 、BSSID (Mac 地址) 以及本机的 IP 地址。
但在 HarmonyOS NEXT 这个极其看重隐私合规的系统中,SSID 已经不再是一个简单的字符串,它被视为用户的物理轨迹隐私 。如果应用在未授权情况下频繁扫描网络,将面临应用商店下架的风险。network_info_plus 插件为我们封装了跨平台的调用逻辑,但在鸿蒙上落地,你还需要处理一些特有的权限"潜规则"。
一、 深度视角:WiFi 信息为何与位置挂钩?
1.1 "WiFi 列表 == 地理位置"
鸿蒙系统(以及 Android 12+)遵循一套安全原则:由于全球绝大部分 WiFi AP 的地理位置已被云端数据库索引,通过获取 BSSID (Mac 地址),应用可以推算出误差在 50 米内的位置。
因此,在鸿蒙上获取网络详情,本质上是对 ohos.permission.LOCATION 的挑战。
1.2 鸿蒙 API 12+ 的隐私变更
- SSID 屏蔽 :若未开启系统 GPS 开关且未授予精确位置权限,API 将返回
<unknown ssid>。 - IP 格式:鸿蒙底层同时支持 IPv4 和 IPv6,插件会自动选择当前活跃的本地 IP。

二、 工程实战:合规的"全家桶"方案
2.1 快速安装与依赖配置
在鸿蒙 NEXT (API 12+) 生态中,通用插件直接在真机运行往往需要对应的 _ohos 适配包来建立底层的 MethodChannel 桥接。
命令行安装:
bash
# 1. 安装跨平台主插件
flutter pub add network_info_plus
# 2. ⚡️ 关键:安装鸿蒙真机适配包
flutter pub add network_info_plus_ohos
# 3. 安装鸿蒙专项权限管理插件
flutter pub add permission_handler_ohos
pubspec.yaml 最终配置参考:
安装完成后,你的 dependencies 列表应当包含以下三项,这是确保网络扫描功能在鸿蒙真机上"跑得通"的铁三角:
yaml
dependencies:
# ... 其他依赖
network_info_plus: ^7.0.0 # 提供跨平台一致的 API 接口
network_info_plus_ohos: any # 💡 必选:建立鸿蒙真机底层桥接
permission_handler_ohos: any # 💡 必选:处理鸿蒙 user_grant 权限弹窗

2.2 权限组合配置 (module.json5)
在鸿蒙中,地理位置属于 user_grant 权限。除了声明权限名,你必须 提供 reason(理由)和 usedScene(使用场景),否则编译会报错。
json5
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" },
{ "name": "ohos.permission.GET_WIFI_INFO" },
{
"name": "ohos.permission.LOCATION",
"reason": "$string:location_reason", // 💡 必须在 string.json 中定义
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.APPROXIMATELY_LOCATION",
"reason": "$string:location_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]

2.3 优雅的权限检查代码 (专项适配版)
由于通用插件在鸿蒙 NEXT 早期版本可能存在行为不一致,推荐使用专项适配插件 permission_handler_ohos:
dart
import 'package:permission_handler_ohos/permission_handler_ohos.dart';
Future<void> initNetworkScan() async {
// 1. 直接申请鸿蒙系统定位权限字符串
final status = await PermissionHandlerOhos.requestPermission("ohos.permission.LOCATION");
if (status == PermissionStatusOhos.granted) {
// 2. 只有位置权限也被授予,SSID 才不会返回 <unknown>
final info = NetworkInfo();
final ssid = await info.getWifiName();
print('当前连接的 WiFi: $ssid');
} else {
// 🔔 告知用户:不授权位置权限,我们就没法帮你完成配网哦
}
}
三、 高级应用场景:智能投屏子网校验
在构建局域网投屏功能时,第一步是判断手机与电视是否在同一个网段。
3.1 同子网判断逻辑
dart
Future<bool> isSameSubnet(String deviceIp) async {
final info = NetworkInfo();
final myIp = await info.getWifiIP(); // e.g., 192.168.3.15
if (myIp == null) return false;
final myPrefix = myIp.substring(0, myIp.lastIndexOf('.'));
final devicePrefix = deviceIp.substring(0, deviceIp.lastIndexOf('.'));
return myPrefix == devicePrefix; // 判断 C 段是否一致
}
3.2 监听网络状态变更
配合 connectivity_plus 使用,每当用户切换 WiFi,自动刷新 NetworkInfo:
dart
Connectivity().onConnectivityChanged.listen((result) {
if (result == ConnectivityResult.wifi) {
refreshWifiDetails();
}
});
四、 鸿蒙环境下的避坑指南 (FAQ)
4.1 为什么一直返回 <unknown ssid> 或 null?
自查清单:
- 是否在真机运行?
- 核心痛点:鸿蒙模拟器(Simulator)是通过宿主电脑网卡虚拟出来的网络,不具备真实的 WiFi 链路层属性。
- 现象 :即便权限全部授予,
getWifiName()依然会返回null。 - 对策 :开发此类功能必须使用真机。若需在模拟器演示,建议在代码中判断获取结果,若为空则填入"模拟数据"以保证 UI 完整性。
- 系统 GPS 开关开了吗? 即使给了应用权限,如果下拉控制中心的"位置信息"总开关是关的,SSID 依然被屏蔽。
- API 20 的特殊性:针对 API 20 以后的 SDK,建议显式设置请求频率,避免被系统判定为"恶意扫描"。
4.2 IPv6 地址解析
挑战 :getWifiIP 有时会返回一段长长的十六进制地址。
方案 :在业务逻辑中,对返回结果进行正则校验,优先过滤出符合 ^([0-9]{1,3}\.){3}[0-9]{1,3}$ 格式的 IPv4 地址。
五、 完整示例代码
以下代码演示了如何在鸿蒙应用中获取并展示当前连接的 WiFi 详细信息:
dart
import 'package:flutter/material.dart';
import 'package:network_info_plus/network_info_plus.dart';
import 'package:permission_handler_ohos/permission_handler_ohos.dart';
class NetworkInfoPlusPage extends StatefulWidget {
const NetworkInfoPlusPage({super.key});
@override
State<NetworkInfoPlusPage> createState() => _NetworkInfoPlusPageState();
}
class _NetworkInfoPlusPageState extends State<NetworkInfoPlusPage> {
final NetworkInfo _networkInfo = NetworkInfo();
String _wifiName = "正在获取...";
String _wifiIP = "正在获取...";
@override
void initState() {
super.initState();
_loadNetworkInfo();
}
Future<void> _loadNetworkInfo() async {
// 💡 专项适配:使用 permission_handler_ohos 申请鸿蒙原生权限
final status = await PermissionHandlerOhos.requestPermission("ohos.permission.LOCATION");
if (status == PermissionStatusOhos.granted) {
final wifiName = await _networkInfo.getWifiName();
final wifiIP = await _networkInfo.getWifiIP();
setState(() {
// 💡 模拟器适配:如果获取不到真实数据,则展示模拟数据供演示
if (wifiName == null && wifiIP == null) {
_wifiName = "Harmony_Guest_WiFi (模拟数据)";
_wifiIP = "192.168.3.15";
} else {
_wifiName = wifiName ?? "未连接或 SSID 被隐藏";
_wifiIP = wifiIP ?? "未知";
}
});
} else {
setState(() {
_wifiName = "位置权限未授予 (${status.name})";
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('鸿蒙网络信息实战')),
body: Center(
child: Card(
margin: const EdgeInsets.all(20),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.wifi, size: 60, color: Colors.blue),
const SizedBox(height: 20),
Text('当前 WiFi: $_wifiName', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
Text('本地 IP 地址: $_wifiIP'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _loadNetworkInfo,
child: const Text('刷新网络状态'),
),
],
),
),
),
),
);
}
}

六、 总结
network_info_plus 虽然只是一个小小的插件,但它牵动着鸿蒙最核心的隐私机制。对于 IoT 和工具类应用开发者,处理好 "权限引导 -> 隐私合规 -> 数据解析" 这一闭环,是打造极致用户体验的基石。
🌐 欢迎加入开源鸿蒙跨平台社区 :开源鸿蒙跨平台开发者社区