fultter wifi_scan三方库鸿蒙版本部署与使用GIF效果展示
Flutter 社区地址: https://atomgit.com/CPF-Flutter/flutter_flutter
github三方库地址:https://github.com/flutternetwork/WiFiFlutter/tree/master/packages/wifi_scan
pub地址:https://pub.dev/packages/wifi_scan
适配后地址:https://atomgit.com/oh-flutter/wifi_scan
wifi_scan库概述
wifi_scan 原本是 flutternetwork/WiFiFlutter 提供的 Flutter WiFi 扫描插件,官方版本支持 Android、iOS 两个平台。通过社区的努力,现阶段已经支持鸿蒙方向。
主要功能:用于扫描附近可见 WiFi 接入点的 Flutter 插件。
WiFi 扫描:通过 WiFiScan.instance.startScan() 一行调用,由鸿蒙原生 wifiManager.startScan() 实现。扫描完成后可通过 getScannedResults() 获取扫描结果。
扫描结果监听:通过 onScannedResultsAvailable 流监听扫描结果变化,当新的扫描结果可用时自动推送更新。
权限检查:提供 canStartScan() 和 canGetScannedResults() 方法检查是否可以执行扫描操作,支持自动请求权限。

需要clone到国内,这样操作效率高一些。


适配基础环境
Flutter版本:3.44.9
HarmonyOS:6.1.0(API 23)

演示GIF视频
通过实际真机录制的。

演示的鸿蒙系统版本

适配过程
一、背景与目标
wifi_scan 是 flutternetwork/WiFiFlutter 提供的 Flutter WiFi 扫描插件,官方版本(0.4.1+2)仅支持 Android 和 iOS 两个平台。鸿蒙化目标:
- Dart 层 API 完全不变——业务方迁移时零改动,仅更换依赖来源;
- 通道协议完全不变——MethodChannel / EventChannel 的通道名、方法名、参数、返回值、错误码语义与原平台对齐;
- 鸿蒙端能力对齐——扫描、获取缓存结果、扫描结果流监听、前置能力检查(can*)全部可用;
- 平台语义差异显式处理——HarmonyOS 权限模型与 Android 不同,需要对 can-code、字段缺失、BSSID 随机化等差异做明确的映射与文档说明。
二、基础环境
| 项 | 版本 |
|---|---|
| Flutter | 3.41.10-ohos-1.0.0(CPF-Flutter/flutter_flutter 鸿蒙化分支) |
| DevEco Studio | 26.0.0 |
| OpenHarmony SDK | 5.1.0(18),runtimeOS: HarmonyOS |
| 真机 | ALN-AL00,const.ohos.fullname 返回 OpenHarmony-7.0.0.105(API 26) |
| 上游插件版本 | wifi_scan 0.4.1+2 |
三、原库分析
3.1 Dart 层结构
Dart 层代码全部位于 lib/,是纯 Dart 实现,跨平台通用,适配过程中一行未改:
lib/
├── wifi_scan.dart # 入口类 WiFiScan(单例),通道定义
└── src/
├── accesspoint.dart # WiFiAccessPoint 数据结构 + WiFiStandards / WiFiChannelWidth 枚举
└── can.dart # CanStartScan / CanGetScannedResults 枚举(can-code 0~5 反序列化)
WiFiScan 对外暴露 5 个成员:
| 成员 | 通道 | 类型 | 说明 |
|---|---|---|---|
canStartScan({askPermissions}) | MethodChannel wifi_scan → canStartScan | Future<CanStartScan> | 检查是否可以启动扫描 |
startScan() | MethodChannel wifi_scan → startScan | Future<bool> | 触发一次扫描 |
canGetScannedResults({askPermissions}) | MethodChannel wifi_scan → canGetScannedResults | Future<CanGetScannedResults> | 检查是否可以获取结果 |
getScannedResults() | MethodChannel wifi_scan → getScannedResults | Future<List<WiFiAccessPoint>> | 获取最近一次扫描缓存 |
onScannedResultsAvailable | EventChannel wifi_scan/onScannedResultsAvailable | Stream<List<WiFiAccessPoint>> | 新结果可用时推送 |
3.2 通道协议(鸿蒙端必须严格遵守的契约)
MethodChannel 方法:
canStartScan/canGetScannedResults:入参{"askPermissions": bool},返回 int(can-code 0~5);startScan:无入参,返回 bool(是否成功触发扫描);getScannedResults:无入参,返回List<Map<String, dynamic>>。
can-code 枚举(Dart 侧反序列化规则,见 lib/src/can.dart):
| code | CanStartScan / CanGetScannedResults | 语义 |
|---|---|---|
| 0 | notSupported | 平台不支持该功能 |
| 1 | yes | 可以调用 |
| 2 | noLocationPermissionRequired | 缺少权限,可申请 |
| 3 | noLocationPermissionDenied | 权限被拒 |
| 4 | noLocationPermissionUpgradeAccuracy | 需升级定位精度 |
| 5 | noLocationServiceDisabled | 定位服务未开启 |
注意:Dart 侧对超出 0~5 的 code 会抛
UnsupportedError,鸿蒙端只能返回 0~5 范围内的值。
扫描结果 wire 格式(WiFiAccessPoint._fromMap 解析的字段):
| wire key | Dart 字段 | 类型 |
|---|---|---|
ssid | ssid | String |
bssid | bssid | String |
capabilities | capabilities | String |
frequency | frequency | int (MHz) |
level | level (RSSI) | int (dBm) |
timestamp | timestamp | int?(启动以来微秒) |
standard | standard | int?(1=legacy, 4=n, 5=ac, 6=ax, 7=ad) |
centerFrequency0 | centerFrequency0 | int? |
centerFrequency1 | centerFrequency1 | int? |
channelWidth | channelWidth | int?(0=20MHz, 1=40, 2=80, 3=160, 4=80+80) |
isPasspoint | isPasspoint | bool? |
operatorFriendlyName | operatorFriendlyName | String? |
venueName | venueName | String? |
is80211mcResponder | is80211mcResponder | bool? |
3.3 Android 实现对照
android/src/main/kotlin/dev/flutternetwork/wifi/wifi_scan/WifiScanPlugin.kt(340 行)基于 WifiManager 实现,关键行为:
- 扫描依赖 定位权限(
ACCESS_FINE_LOCATION),can-code 2~5 全部围绕定位权限/定位服务状态设计; startScan()通过WIFI_STATE_ENABLED判断开关状态;getScanResults()返回系统缓存的ScanResult列表;- 通过
BroadcastReceiver(SCAN_RESULTS_AVAILABLE_ACTION)驱动 EventChannel 推送。
鸿蒙端需要在不改变 Dart 契约的前提下,用 HarmonyOS 的等价能力复现这套行为。
四、适配方案设计
4.1 总体思路
┌────────────────────────────────────────────────────────────┐
│ Dart 层(不变) │
│ WiFiScan ── MethodChannel('wifi_scan') │
│ └── EventChannel('wifi_scan/onScannedResultsAvailable')
├────────────────────────────────────────────────────────────┤
│ 鸿蒙端(新增) │
│ WifiScanPlugin.ets (ArkTS) │
│ implements FlutterPlugin + MethodCallHandler + StreamHandler
│ └── @kit.ConnectivityKit 的 wifiManager │
│ └── @kit.AbilityKit 的 abilityAccessCtrl / bundleManager │
└────────────────────────────────────────────────────────────┘
决策点:
- 采用 Flutter 插件 v2 规范下的 ohos 平台声明:在
pubspec.yaml的flutter.plugin.platforms中新增ohos段,由 Flutter 工具(鸿蒙化版本)自动生成 HAP 工程骨架并管理 HAR 依赖; - 插件以 HAR 模块形式交付:
ohos/目录是一个标准 Stage 模型 HAR 模块(module.json5中type: "har"),打包后以wifi_scan.har被宿主工程引用; - Dart 层零改动:所有平台差异收敛在 ArkTS 实现内部,通过 can-code 重映射和 null 字段体现。
4.2 接口语义映射(核心设计)
| Dart API | HarmonyOS 实现 | 语义差异处理 |
|---|---|---|
canStartScan / canGetScannedResults | computeCanCode() | 鸿蒙无定位权限概念,can-code 3/4/5 无对应场景:STA 能力不支持 → 0;缺 WiFi 权限 → 2;正常 → 1 |
startScan | wifiManager.startScan() | 先查 SET_WIFI_INFO 权限,再查 isWifiActive()(WLAN 未开返回 false,与 Android 行为一致) |
getScannedResults | wifiManager.getScanInfoList() | 查 GET_WIFI_INFO 权限;WLAN 关闭/无缓存返回空列表(对齐 Android 行为) |
onScannedResultsAvailable | wifiManager.on('wifiScanStateChange') | 监听值 1(扫描完成)时推送全量缓存;onListen 时先推一次当前缓存快照(兼容 Android"新订阅立即收到一次数据"的行为) |
can-code 重映射规则(鸿蒙端只返回 0/1/2):
0 (notSupported):wifiManager.isFeatureSupported(0x0001)(0x0001 = infrastructure/STA 模式)返回 false,或查询异常;1 (yes):GET_WIFI_INFO与SET_WIFI_INFO均已授予(两者为system_grant权限,声明后安装即授予);2 (noLocationPermissionRequired):上述权限缺失时的兜底(最接近"需要权限"的语义),askPermissions参数在鸿蒙端无实际作用(无运行时弹窗)。
扫描结果字段映射(toWireMap):
| wire key | 来源(wifiManager.WifiScanInfo) | 说明 |
|---|---|---|
ssid / bssid / capabilities / frequency / timestamp | 同名字段直接透传 | |
level | info.rssi | 鸿蒙字段名为 rssi,wire 上保持 level |
centerFrequency0 / centerFrequency1 | 同名字段直接透传 | |
channelWidth | info.channelWidth 在 0~4 范围内直接透传,否则 null | 与 Dart 枚举下标一致 |
standard | 无对应字段 → null | Dart 侧反序列化为 WiFiStandards.unkown |
isPasspoint / operatorFriendlyName / venueName / is80211mcResponder | 无对应字段 → null | WifiScanInfo 不暴露 Passpoint 信息 |
BSSID 隐私差异:HarmonyOS 上若未持有受限权限 ohos.permission.GET_WIFI_PEERS_MAC,系统返回的是随机化 BSSID。适配决定不声明该受限权限(申请门槛高、面向系统/特权应用),在文档中显式告知使用者。
五、鸿蒙插件工程搭建
5.1 目录结构
通过
flutter create . --template=plugin --platforms=ohos
命令自动构建骨架,例如:
在包根目录新增ohos/,为标准 HAR 模块布局:
ohos/
├── build-profile.json5 # Stage 模型 HAR 构建配置(apiType: stageMode)
├── hvigorfile.ts # 构建脚本(harTasks 内置插件)
├── oh-package.json5 # 包元信息(name: wifi_scan, main: index.ets)
├── index.ets # HAR 入口:export default WifiScanPlugin
├── .gitignore # 忽略 oh_modules / build / local.properties 等
├── BuildProfile.ets # 工具生成的版本号文件(已 gitignore)
├── oh-package-lock.json5 # 锁文件(生成物,引用 flutter_ohos 引擎 HAR)
└── src/main/
├── module.json5 # 模块声明(type: har)+ 权限申请
└── ets/components/plugin/
└── WifiScanPlugin.ets # 插件核心实现(ArkTS,约 278 行)
其中 index.ets 作为 HAR 的统一导出入口:
import WifiScanPlugin from './src/main/ets/components/plugin/WifiScanPlugin';
export default WifiScanPlugin;
build-profile.json5:
{
"apiType": "stageMode",
"buildOption": {},
"targets": [{ "name": "default" }]
}
5.2 模块声明与权限
ohos/src/main/module.json5:
{
"module": {
"name": "wifi_scan",
"type": "har",
"deviceTypes": ["default", "tablet"],
"requestPermissions": [
{ "name": "ohos.permission.GET_WIFI_INFO" },
{ "name": "ohos.permission.SET_WIFI_INFO" }
]
}
}
权限说明:
| 权限 | 级别 | 授予方式 | 用途 |
|---|---|---|---|
ohos.permission.GET_WIFI_INFO | system_grant | 安装时自动授予 | 读取 WiFi 信息(扫描结果列表) |
ohos.permission.SET_WIFI_INFO | system_grant | 安装时自动授予 | 修改 WiFi 配置(触发扫描) |
两者均为普通系统授予权限,无运行时弹窗,因此 Dart 层的 askPermissions 参数在鸿蒙端不做实际申请,仅在 computeCanCode 中校验授予状态。
5.3 pubspec.yaml 平台声明
在 flutter.plugin.platforms 中新增 ohos 段(与 Android 段保持相同的 package/pluginClass 命名):
flutter:
plugin:
platforms:
android:
package: dev.flutternetwork.wifi.wifi_scan
pluginClass: WifiScanPlugin
ios:
pluginClass: WifiScanPlugin
ohos:
package: dev.flutternetwork.wifi.wifi_scan
pluginClass: WifiScanPlugin
该声明使鸿蒙化 Flutter 工具链在 flutter build hap / flutter run -d ohos 时能识别本插件的 ohos 平台实现,并自动完成:
- 生成/更新示例工程的
GeneratedPluginRegistrant.ets; - 将
ohos/目录以 HAR 形式注入依赖(@ohos/flutter_ohos等引擎 HAR 由oh-package-lock.json5锁定到本地 Flutter 引擎缓存路径)。
六、ArkTS 插件实现(WifiScanPlugin.ets)
6.1 类结构
export default class WifiScanPlugin
implements FlutterPlugin, MethodCallHandler, StreamHandler {
private static readonly CHANNEL_NAME: string = "wifi_scan";
private static readonly EVENT_CHANNEL_NAME: string = "wifi_scan/onScannedResultsAvailable";
private methodChannel: MethodChannel | null = null;
private eventChannel: EventChannel | null = null;
private eventSink: EventSink | null = null;
private scanStateCallback: ((value: number) => void) | null = null;
}
一个类同时承担三个角色,依赖 @ohos/flutter_ohos 提供的插件接口:
FlutterPlugin:onAttachedToEngine/onDetachedFromEngine生命周期;MethodCallHandler:onMethodCall分发 4 个方法;StreamHandler:onListen/onCancel管理 EventChannel 流。
6.2 通道注册与生命周期
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.methodChannel = new MethodChannel(binding.getBinaryMessenger(), WifiScanPlugin.CHANNEL_NAME);
this.methodChannel.setMethodCallHandler(this);
this.eventChannel = new EventChannel(binding.getBinaryMessenger(), WifiScanPlugin.EVENT_CHANNEL_NAME);
this.eventChannel.setStreamHandler(this);
}
onDetachedFromEngine(binding: FlutterPluginBinding): void {
this.unregisterScanListener(); // 防泄漏:先摘系统监听
if (this.eventSink !== null) {
this.eventSink.endOfStream(); // 通知 Dart 侧流结束
}
// 逐置空 methodChannel / eventChannel / eventSink
}
生命周期处理要点:
- 通道名与 Dart 侧严格一致(
wifi_scan、wifi_scan/onScannedResultsAvailable); - 引擎解绑时必须先反注册
wifiScanStateChange系统监听再置空 sink,否则系统回调持有插件实例造成泄漏,且回调内访问已置空字段会抛异常。
6.3 方法分发
onMethodCall(call: MethodCall, result: MethodResult): void {
switch (call.method) {
case "canStartScan": this.handleCanStartScan(call, result); break;
case "startScan": this.handleStartScan(result); break;
case "canGetScannedResults": this.handleCanGetScannedResults(call, result); break;
case "getScannedResults": this.handleGetScannedResults(result); break;
default: result.notImplemented(); break;
}
}
未知方法返回 notImplemented,与 Flutter 插件通用契约一致。
6.4 启动扫描 startScan
private handleStartScan(result: MethodResult): void {
try {
if (!this.hasPermission('ohos.permission.SET_WIFI_INFO' as Permissions)) {
result.error("WifiScanPlugin.Security", "SET_WIFI_INFO permission is not granted", null);
return;
}
if (!wifiManager.isWifiActive()) {
result.success(false); // WLAN 未开启:返回 false,与 Android 行为一致
return;
}
wifiManager.startScan();
result.success(true);
} catch (error) {
const err = error as BusinessError;
if (err.code === 201) {
result.error("WifiScanPlugin.Security", err.message, null); // 权限类错误 → 错误码
} else {
result.success(false); // 扫描限频、WLAN 关闭中等 → 触发失败
}
}
}
错误处理策略:
- 201(权限类 BusinessError):按
WifiScanPlugin.Security错误码回传,Dart 侧以PlatformException感知; - 限频/状态类异常:不抛错,返回
false——与 Android 端"扫描触发失败返回 false"的语义一致,业务侧只需处理 bool 结果; - 鸿蒙对扫描有系统级限频(throttling),频繁调用会被系统拒绝,这里统一收敛为
false。
6.5 获取扫描结果 getScannedResults
private handleGetScannedResults(result: MethodResult): void {
try {
if (!this.hasPermission('ohos.permission.GET_WIFI_INFO' as Permissions)) {
result.error("WifiScanPlugin.Security", "GET_WIFI_INFO permission is not granted", null);
return;
}
result.success(this.buildScannedResults());
} catch (error) {
const err = error as BusinessError;
if (err.code === 201) {
result.error("WifiScanPlugin.Security", err.message, null);
} else {
result.error("WifiScanPlugin", err.message, null);
}
}
}
buildScannedResults() 内部再兜一层权限与异常检查,WLAN 关闭或扫描缓存为空时返回空列表(对齐 Android getScanResults() 在无线环境下返回空集合的行为,而非报错):
private buildScannedResults(): Object[] {
const result: Object[] = [];
try {
if (!this.hasPermission('ohos.permission.GET_WIFI_INFO' as Permissions)) {
return result;
}
const scanInfos: wifiManager.WifiScanInfo[] = wifiManager.getScanInfoList();
for (let i = 0; i < scanInfos.length; i++) {
result.push(this.toWireMap(scanInfos[i]));
}
} catch (error) {
// WLAN off / scan cache empty => 空列表
}
return result;
}
6.6 EventChannel 流监听
onListen(args: Object, events: EventSink): void {
this.eventSink = events;
this.registerScanListener();
// Android 兼容行为:新订阅立即收到当前缓存快照,后续扫描持续推送
this.pushScannedResults();
}
onCancel(args: Object): void {
this.unregisterScanListener();
if (this.eventSink !== null) {
this.eventSink.endOfStream();
}
this.eventSink = null;
}
系统监听注册/反注册(防重入 + 防泄漏):
private registerScanListener(): void {
if (this.scanStateCallback !== null) {
return; // 已注册则跳过,避免重复注册
}
this.scanStateCallback = (value: number): void => {
if (value === 1) { // 1 = 扫描完成
this.pushScannedResults();
}
};
try {
wifiManager.on('wifiScanStateChange', this.scanStateCallback);
} catch (error) {
this.scanStateCallback = null;
}
}
private unregisterScanListener(): void {
if (this.scanStateCallback === null) {
return;
}
try {
wifiManager.off('wifiScanStateChange', this.scanStateCallback);
} catch (error) {
}
this.scanStateCallback = null;
}
与 Android 用 SCAN_RESULTS_AVAILABLE_ACTION 广播驱动推送等价;鸿蒙的 wifiScanStateChange 状态值 1 表示一轮扫描完成,此时 getScanInfoList() 中已是最新缓存,直接全量推送(Dart 侧 Stream<List<WiFiAccessPoint>> 的语义就是每次推送全量列表,与 Android 端行为一致)。
6.7 权限检查工具方法
private hasPermission(permission: Permissions): boolean {
try {
const atManager = abilityAccessCtrl.createAtManager();
const bundleInfo = bundleManager.getBundleInfoForSelfSync(
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
const tokenId: number = bundleInfo.appInfo.accessTokenId;
const status = atManager.verifyAccessTokenSync(tokenId, permission);
return status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
} catch (error) {
return false;
}
}
采用同步校验(verifyAccessTokenSync):can* 接口需要立即返回 int 结果,不引入异步授权流程;异常一律按"未授权"处理。
6.8 can-code 计算
private computeCanCode(): number {
try {
if (!this.isStaSupported()) {
return 0;
}
} catch (error) {
return 0;
}
if (!this.hasPermission('ohos.permission.GET_WIFI_INFO' as Permissions)
|| !this.hasPermission('ohos.permission.SET_WIFI_INFO' as Permissions)) {
return 2;
}
return 1;
}
private isStaSupported(): boolean {
// 0x0001 = infrastructure mode 能力位
return wifiManager.isFeatureSupported(0x0001);
}
canStartScan 与 canGetScannedResults 共用该逻辑(鸿蒙端两项能力依赖同一套权限与硬件能力)。
6.9 结果数据映射 toWireMap
private toWireMap(info: wifiManager.WifiScanInfo): Map<string, Object | null> {
const map = new Map<string, Object | null>();
map.set('ssid', info.ssid);
map.set('bssid', info.bssid);
map.set('capabilities', info.capabilities);
map.set('frequency', info.frequency);
map.set('level', info.rssi); // 鸿蒙字段名 rssi → wire 字段 level
map.set('timestamp', info.timestamp);
map.set('standard', null); // WifiScanInfo 无 WiFi 代际字段
map.set('centerFrequency0', info.centerFrequency0);
map.set('centerFrequency1', info.centerFrequency1);
map.set('channelWidth', this.toChannelWidthCode(info.channelWidth));
map.set('isPasspoint', null); // Passpoint 信息不对外暴露
map.set('operatorFriendlyName', null);
map.set('venueName', null);
map.set('is80211mcResponder', null);
return map;
}
private toChannelWidthCode(channelWidth: number): Object | null {
if (channelWidth >= 0 && channelWidth <= 4) {
return channelWidth; // 与 Dart 侧 WiFiChannelWidth 枚举下标对齐
}
return null;
}
使用 Map<string, Object | null> 保证 wire 上 key 与 Dart 侧 _fromMap 期望的 key 一一对应;不可用字段显式置 null 而非省略,Dart 侧可选项反序列化为 null,枚举项落到 unkown 分支,行为与 Android 端字段缺失时一致。
七、示例工程(example/ohos)
7.1 工程创建
基于鸿蒙化 Flutter 工具链为示例工程生成 example/ohos/ 目录(Stage 模型 HAP 工程),核心结构:
example/ohos/
├── AppScope/app.json5 # 应用级配置
├── build-profile.json5 # 签名配置 + products(compatibleSdkVersion: 6.1.0(23))
├── hvigor/ + hvigorfile.ts + hvigorconfig.ts
├── oh-package.json5
└── entry/ # 主入口模块
├── oh-package.json5
└── src/main/
├── module.json5 # entry 模块声明(含权限申请)
├── ets/
│ ├── entryability/EntryAbility.ets # 插件注册入口
│ ├── pages/Index.ets
│ └── plugins/GeneratedPluginRegistrant.ets # 工具生成
└── resources/...
7.2 权限声明
entry/src/main/module.json5 中声明 WiFi 权限(另加 INTERNET 供示例应用其他用途):
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" },
{ "name": "ohos.permission.GET_WIFI_INFO" },
{ "name": "ohos.permission.SET_WIFI_INFO" },
]
7.3 插件注册
flutter build hap / flutter run 时工具链根据 pubspec.yaml 的 ohos 平台声明自动生成 GeneratedPluginRegistrant.ets:
import { FlutterEngine, Log } from '@ohos/flutter_ohos';
import WifiScanPlugin from 'wifi_scan';
export class GeneratedPluginRegistrant {
static registerWith(flutterEngine: FlutterEngine) {
try {
flutterEngine.getPlugins()?.add(new WifiScanPlugin());
} catch (e) {
Log.e(TAG, "Tried to register plugins with FlutterEngine failed.");
Log.e(TAG, "Received exception while registering", e);
}
}
}
EntryAbility.ets 在引擎配置阶段调用注册:
export default class EntryAbility extends FlutterAbility {
configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine)
}
}
插件实例随引擎生命周期创建/销毁,onAttachedToEngine / onDetachedFromEngine 由引擎回调驱动。
7.4 Dart 侧示例代码
example/lib/main.dart 未做平台分支(纯 Dart,跨平台共用),覆盖全部 5 个 API:
// 检查并启动扫描
final can = await WiFiScan.instance.canStartScan();
if (can == CanStartScan.yes) {
final result = await WiFiScan.instance.startScan();
}
// 拉取缓存结果
final results = await WiFiScan.instance.getScannedResults();
// 订阅扫描结果流
subscription = WiFiScan.instance.onScannedResultsAvailable
.listen((result) => setState(() => accessPoints = result));
界面上 SCAN / GET / STREAM 三个入口分别验证"触发扫描"、“拉取缓存”、"流式监听"三条链路,接入点详情页展示全部 wire 字段(含鸿蒙端返回 null 的 Passpoint 等字段),便于逐项核对映射。
八、构建与验证
8.1 Dart 层单元测试
test/wifi_scan_test.dart 为纯 Dart 层测试(mock MethodChannel),与平台实现无关,适配后原样复用:
flutter test
验证点:can-code 0~5 的反序列化、非法 code 抛 UnsupportedError、startScan 透传 bool、getScannedResults 对 wire map(含全 null 可选项)的解析——这正是鸿蒙端 toWireMap 输出的数据形态,单测通过说明 Dart 层能正确消费鸿蒙端回传的数据结构。
8.2 鸿蒙端静态检查
- ArkTS 代码遵循严格模式:所有成员显式类型标注、
try/catch兜底、BusinessError按code分类处理; - DevEco Studio 中打开
example/ohos工程执行 Build,确认ohos/HAR 参与编译且无编译错误。
8.3 真机功能验证(HarmonyOS 7.0,API 26)
| 用例 | 步骤 | 预期 | 结果 |
|---|---|---|---|
| 能力检查 | canStartScan() / canGetScannedResults() | 返回 yes(code 1) | 通过 |
| WLAN 关闭时启动扫描 | 关闭 WLAN → startScan() | 返回 false,无异常 | 通过 |
| 触发扫描 | WLAN 开启 → startScan() | 返回 true | 通过 |
| 拉取结果 | getScannedResults() | 返回 AP 列表(ssid/bssid/信号强度/频段等) | 通过 |
| 流监听 | 订阅 onScannedResultsAvailable → 触发扫描 | 订阅即收到一次快照;扫描完成(state=1)后收到新数据 | 通过 |
| 字段核对 | 查看 AP 详情 | standard/isPasspoint 等为 null,level(rssi)、frequency、channelWidth 正常 | 通过 |
| 重复订阅/取消 | 多次开/关 STREAM 开关 | 监听正确注册/反注册,无泄漏、无重复推送 | 通过 |
| 页面反复进出 | 切换页面触发 Ability 生命周期 | 引擎解绑时监听摘除,无崩溃 | 通过 |
| BSSID 观察 | 对比同一 AP 多次扫描 BSSID | 无 GET_WIFI_PEERS_MAC 权限时 BSSID 为随机化值(符合鸿蒙安全策略) | 符合预期 |
构建命令:
flutter build hap --release
安装到真机运行,hilog 中 WifiScanPlugin 日志无异常记录。
API 说明
| API | 描述 | 参数 | 返回值 | OpenHarmony支持 |
|---|---|---|---|---|
canStartScan() | 检查是否可以启动扫描 | askPermissions: bool | Future<CanStartScan> | 是 |
startScan() | 启动WiFi扫描 | 无 | Future<bool> | 是 |
canGetScannedResults() | 检查是否可以获取扫描结果 | askPermissions: bool | Future<CanGetScannedResults> | 是 |
getScannedResults() | 获取扫描结果 | 无 | Future<List<WiFiAccessPoint>> | 是 |
onScannedResultsAvailable | 扫描结果可用时的流 | 无 | Stream<List<WiFiAccessPoint>> | 是 |
具体工作流程
左侧 · 主动扫描(拉模式)
应用主动发起,按以下三步顺序执行:
-
权限预判:调用 canStartScan() 检查扫描条件
- 鸿蒙侧通过 verifyAccessTokenSync 校验 GET_WIFI_INFO、SET_WIFI_INFO 权限
- 同时校验 STA 能力是否支持、WLAN 服务是否可用
- 权限不通过时返回对应错误码,不进入下一步
-
启动扫描:调用 startScan() 触发硬件扫描
- 经 MethodChannel(通道名 wifi_scan)转发到鸿蒙原生层
- 原生端 handleStartScan 检查 WiFi 状态后调用 wifiManager.startScan()
- WLAN 硬件扫描周边接入点,结果写入系统缓存
-
拉取结果:调用 getScannedResults() 获取扫描数据
- 可独立调用,无需每次都先触发扫描,直接读取系统缓存
- 原生端 handleGetScannedResults 调用 getScanInfoList() 获取原始数据
- 经 toWireMap() 转换为 Dart 可识别的 WiFiAccessPoint 列表返回
- 返回字段包含 ssid、bssid、level、frequency、capabilities、channelWidth 等
右侧 · 流式监听(推模式)
应用订阅后由系统主动推送,生命周期分为三个阶段:
-
建立订阅:监听 onScannedResultsAvailable 流
- Dart 层通过 StreamSubscription 订阅,EventChannel(通道名 wifi_scan/onScannedResultsAvailable)建立广播流
- 原生端 onListen 回调中执行 registerScanListener() 注册系统扫描状态回调
- 同时调用 pushScannedResults() 立即推送一次当前缓存结果,避免首屏空白
-
持续推送:扫描完成后自动下发数据
- 触发来源不限:本应用、系统服务、其他 App 触发的扫描均可被监听
- 扫描完成后原生端回调执行 eventSink.success(results) 向下游推送
- Dart 层 Stream.listen 回调触发,执行 setState(() => accessPoints = results) 更新状态
- 可搭配 StreamBuilder 实现 UI 响应式自动刷新 WiFi 列表
-
资源释放:页面销毁时解绑监听
- dispose 中必须调用 subscription.cancel() 取消 Dart 层订阅
- 触发原生端 onCancel 回调,执行 unregisterScanListener() 解绑系统监听
- 同时 eventSink.endOfStream() 关闭流,防止内存泄漏

核心代码
下面挑四个关键的代码块来说明。
Dart层入口
这一段在 lib/wifi_scan.dart 里。
class WiFiScan {
static const MethodChannel _channel = MethodChannel('wifi_scan');
static const EventChannel _eventChannel = EventChannel('wifi_scan/onScannedResultsAvailable');
static WiFiScan? _instance;
static WiFiScan get instance => _instance ??= WiFiScan._();
Future<CanStartScan> canStartScan({bool askPermissions = false}) async {
final int code = await _channel.invokeMethod('canStartScan', <String, dynamic>{
'askPermissions': askPermissions,
});
return CanStartScan.values[code];
}
Future<bool> startScan() async {
return await _channel.invokeMethod('startScan');
}
Future<CanGetScannedResults> canGetScannedResults({bool askPermissions = false}) async {
final int code = await _channel.invokeMethod('canGetScannedResults', <String, dynamic>{
'askPermissions': askPermissions,
});
return CanGetScannedResults.values[code];
}
Future<List<WiFiAccessPoint>> getScannedResults() async {
final List<dynamic> results = await _channel.invokeMethod('getScannedResults');
return results.map((e) => WiFiAccessPoint.fromMap(e)).toList();
}
Stream<List<WiFiAccessPoint>> get onScannedResultsAvailable {
return _eventChannel.receiveBroadcastStream().map((event) {
final List<dynamic> results = event;
return results.map((e) => WiFiAccessPoint.fromMap(e)).toList();
});
}
}
MethodChannel 的通道名固定为 wifi_scan,EventChannel 为 wifi_scan/onScannedResultsAvailable,和各平台原生端保持一致。这一层是纯 Dart 代码,跨平台通用。
鸿蒙端 MethodCallHandlerImpl
文件在 ohos/src/main/ets/components/plugin/WifiScanPlugin.ets。
import {
FlutterPlugin,
FlutterPluginBinding,
MethodCall,
MethodCallHandler,
MethodChannel,
EventChannel,
EventSink,
StreamHandler,
} from '@ohos/flutter_ohos';
import { wifiManager } from '@kit.ConnectivityKit';
import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';
export default class WifiScanPlugin implements FlutterPlugin, MethodCallHandler, StreamHandler {
private static readonly CHANNEL_NAME: string = "wifi_scan";
private static readonly EVENT_CHANNEL_NAME: string = "wifi_scan/onScannedResultsAvailable";
private methodChannel: MethodChannel | null = null;
private eventChannel: EventChannel | null = null;
private eventSink: EventSink | null = null;
private scanStateCallback: ((value: number) => void) | null = null;
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.methodChannel = new MethodChannel(binding.getBinaryMessenger(), WifiScanPlugin.CHANNEL_NAME);
this.methodChannel.setMethodCallHandler(this);
this.eventChannel = new EventChannel(binding.getBinaryMessenger(), WifiScanPlugin.EVENT_CHANNEL_NAME);
this.eventChannel.setStreamHandler(this);
}
onDetachedFromEngine(binding: FlutterPluginBinding): void {
this.unregisterScanListener();
if (this.eventSink !== null) {
this.eventSink.endOfStream();
}
if (this.methodChannel !== null) {
this.methodChannel.setMethodCallHandler(null);
}
this.methodChannel = null;
if (this.eventChannel !== null) {
this.eventChannel.setStreamHandler(null);
}
this.eventChannel = null;
this.eventSink = null;
}
onMethodCall(call: MethodCall, result: MethodResult): void {
switch (call.method) {
case "canStartScan":
this.handleCanStartScan(call, result);
break;
case "startScan":
this.handleStartScan(result);
break;
case "canGetScannedResults":
this.handleCanGetScannedResults(call, result);
break;
case "getScannedResults":
this.handleGetScannedResults(result);
break;
default:
result.notImplemented();
break;
}
}
onListen(args: Object, events: EventSink): void {
this.eventSink = events;
this.registerScanListener();
this.pushScannedResults();
}
onCancel(args: Object): void {
this.unregisterScanListener();
if (this.eventSink !== null) {
this.eventSink.endOfStream();
}
this.eventSink = null;
}
}
这是鸿蒙适配的核心,实现了鸿蒙的 FlutterPlugin 接口。onAttachedToEngine 时创建 MethodChannel 和 EventChannel,onDetachedFromEngine 时解除注册,避免内存泄漏。
鸿蒙端扫描实现
文件在 ohos/src/main/ets/components/plugin/WifiScanPlugin.ets。
private handleStartScan(result: MethodResult): void {
try {
if (!this.hasPermission('ohos.permission.SET_WIFI_INFO' as Permissions)) {
result.error("WifiScanPlugin.Security", "SET_WIFI_INFO permission is not granted", null);
return;
}
if (!wifiManager.isWifiActive()) {
result.success(false);
return;
}
wifiManager.startScan();
result.success(true);
} catch (error) {
const err = error as BusinessError;
if (err.code === 201) {
result.error("WifiScanPlugin.Security", err.message, null);
} else {
result.success(false);
}
}
}
private handleGetScannedResults(result: MethodResult): void {
try {
if (!this.hasPermission('ohos.permission.GET_WIFI_INFO' as Permissions)) {
result.error("WifiScanPlugin.Security", "GET_WIFI_INFO permission is not granted", null);
return;
}
result.success(this.buildScannedResults());
} catch (error) {
const err = error as BusinessError;
if (err.code === 201) {
result.error("WifiScanPlugin.Security", err.message, null);
} else {
result.error("WifiScanPlugin", err.message, null);
}
}
}
private buildScannedResults(): Object[] {
const result: Object[] = [];
try {
if (!this.hasPermission('ohos.permission.GET_WIFI_INFO' as Permissions)) {
return result;
}
const scanInfos: wifiManager.WifiScanInfo[] = wifiManager.getScanInfoList();
for (let i = 0; i < scanInfos.length; i++) {
const info: wifiManager.WifiScanInfo = scanInfos[i];
result.push(this.toWireMap(info));
}
} catch (error) {
// WLAN off / scan cache empty => empty list, mirroring Android scanResults
}
return result;
}
private toWireMap(info: wifiManager.WifiScanInfo): Map<string, Object | null> {
const map = new Map<string, Object | null>();
map.set('ssid', info.ssid);
map.set('bssid', info.bssid);
map.set('capabilities', info.capabilities);
map.set('frequency', info.frequency);
map.set('level', info.rssi);
map.set('timestamp', info.timestamp);
map.set('standard', null);
map.set('centerFrequency0', info.centerFrequency0);
map.set('centerFrequency1', info.centerFrequency1);
map.set('channelWidth', this.toChannelWidthCode(info.channelWidth));
map.set('isPasspoint', null);
map.set('operatorFriendlyName', null);
map.set('venueName', null);
map.set('is80211mcResponder', null);
return map;
}
把 Dart 发来的 startScan 和 getScannedResults 方法映射到鸿蒙 ArkUI 的 wifiManager API。扫描结果通过 getScanInfoList() 获取,然后转换成 Dart 端能识别的 Map 格式。
权限检查实现
文件在 ohos/src/main/ets/components/plugin/WifiScanPlugin.ets。
private computeCanCode(): number {
try {
if (!this.isStaSupported()) {
return 0;
}
} catch (error) {
return 0;
}
if (!this.hasPermission('ohos.permission.GET_WIFI_INFO' as Permissions)
|| !this.hasPermission('ohos.permission.SET_WIFI_INFO' as Permissions)) {
return 2;
}
return 1;
}
private isStaSupported(): boolean {
return wifiManager.isFeatureSupported(0x0001);
}
private hasPermission(permission: Permissions): boolean {
try {
const atManager = abilityAccessCtrl.createAtManager();
const bundleInfo = bundleManager.getBundleInfoForSelfSync(
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
const tokenId: number = bundleInfo.appInfo.accessTokenId;
const status = atManager.verifyAccessTokenSync(tokenId, permission);
return status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
} catch (error) {
return false;
}
}
Can-codes 在 HarmonyOS 上的映射规则:
- 0 (notSupported):STA/scan 能力不支持或 WLAN 服务缺失
- 1 (yes):GET/SET_WIFI_INFO 是 system_grant 权限,安装时已授予
- 2 (noLocationPermissionRequired):缺少 WiFi 权限时返回
使用示例
引入依赖的时候鸿蒙必须用 git 分支,不能直接写版本号。
dependencies:
flutter:
sdk: flutter
wifi_scan:
git:
url: "https://atomgit.com/oh-flutter/wifi_scan.git"
ref: "0.4.1+2-ohos-1.0.0-beta.1"
在鸿蒙工程的 module.json5 中需要声明以下权限:
{
"requestPermissions": [
{
"name": "ohos.permission.GET_WIFI_INFO"
},
{
"name": "ohos.permission.SET_WIFI_INFO"
}
]
}
基础 WiFi 扫描调用示例:
import 'package:wifi_scan/wifi_scan.dart';
void _startScan() async {
// 检查平台扫描支持情况
final can = await WiFiScan.instance.canStartScan(askPermissions: true);
switch(can) {
case CanStartScan.yes:
// 启动扫描
final isScanning = await WiFiScan.instance.startScan();
if (isScanning) {
print('扫描已启动');
}
break;
case CanStartScan.noLocationPermissionRequired:
print('需要位置权限');
break;
case CanStartScan.notSupported:
print('不支持扫描');
break;
}
}
void _getScannedResults() async {
final can = await WiFiScan.instance.canGetScannedResults(askPermissions: true);
switch(can) {
case CanGetScannedResults.yes:
final accessPoints = await WiFiScan.instance.getScannedResults();
for (var ap in accessPoints) {
print('SSID: ${ap.ssid}, BSSID: ${ap.bssid}, Level: ${ap.level}');
}
break;
// ... 处理其他情况
}
}
监听扫描结果变化:
List<WiFiAccessPoint> accessPoints = [];
StreamSubscription<List<WiFiAccessPoint>>? subscription;
void _startListening() {
subscription = WiFiScan.instance.onScannedResultsAvailable.listen((results) {
setState(() {
accessPoints = results;
});
});
}
void dispose() {
subscription?.cancel();
super.dispose();
}
WiFiAccessPoint 数据结构
| 字段名 | 类型 | 描述 | OpenHarmony支持 |
|---|---|---|---|
ssid | String | WiFi网络的SSID(网络名称) | 是 |
bssid | String | WiFi接入点的BSSID(MAC地址) | 是(注意:无GET_WIFI_PEERS_MAC权限时可能为随机值) |
capabilities | String | 网络的安全能力描述 | 是 |
frequency | int | 频率(MHz) | 是 |
level | int | 信号强度(dBm) | 是 |
timestamp | int | 扫描时间戳(微秒) | 是 |
standard | int? | WiFi标准(802.11a/b/g/n/ac/ax等) | 否(返回null) |
centerFrequency0 | int? | 中心频率0(用于80+80/160MHz) | 是 |
centerFrequency1 | int? | 中心频率1(用于80+80/160MHz) | 是 |
channelWidth | int? | 信道宽度(20/40/80/160MHz) | 是 |
isPasspoint | bool? | 是否为Passpoint网络 | 否(返回null) |
operatorFriendlyName | String? | 运营商友好名称 | 否(返回null) |
venueName | String? | 场地名称 | 否(返回null) |
is80211mcResponder | bool? | 是否支持802.11mc RTT响应 | 否(返回null) |
CanStartScan 枚举值
| 值 | 描述 | OpenHarmony映射 |
|---|---|---|
yes | 可以启动扫描 | 已授予WiFi权限且STA能力支持 |
noLocationPermissionRequired | 需要位置权限 | 缺少WiFi权限(system_grant权限未授予) |
notSupported | 不支持扫描 | STA能力不支持或WLAN服务缺失 |
CanGetScannedResults 枚举值
| 值 | 描述 | OpenHarmony映射 |
|---|---|---|
yes | 可以获取扫描结果 | 已授予WiFi权限且STA能力支持 |
noLocationPermissionRequired | 需要位置权限 | 缺少WiFi权限(system_grant权限未授予) |
notSupported | 不支持获取扫描结果 | STA能力不支持或WLAN服务缺失 |
使用说明

启动扫描
调用 startScan() 触发完整的 WiFi 扫描。如果扫描成功启动,该方法返回 true。
调用 getScannedResults() 获取最新可用的扫描结果。返回一个 WiFiAccessPoint 对象列表,包含 SSID、BSSID、信号强度、频率等信息。
当新的扫描结果可用时,onScannedResultsAvailable 流会发出新的数据。
OpenHarmony 平台差异说明
- 如果没有受限制的
ohos.permission.GET_WIFI_PEERS_MAC权限,系统会返回随机化的 BSSID。 WifiScanInfo不包含 Passpoint、运营商、场地、802.11mc 字段,因此对应字段返回null。GET_WIFI_INFO和SET_WIFI_INFO为system_grant权限,在安装时授予,无需运行时弹窗授权。
wifi_scan 插件优势总结
wifi_scan 是 Flutter 生态下用于扫描周边 WiFi 热点的开源插件,归属 WiFiFlutter 工具套件,现已完成鸿蒙(OpenHarmony/HarmonyOS)平台适配,核心优势如下:
1. 跨平台支持
- 原生支持 Android、iOS、鸿蒙三端;Android 最低兼容 SDK16,iOS 最低兼容 9.0,鸿蒙基于 ConnectivityKit wifiManager 原生接口实现扫描能力。
- Android 封装系统
WifiManager原生扫描 API,能力完整;iOS 做兼容桩实现,适配苹果平台 API 限制;鸿蒙复用系统原生扫描接口,Dart API 与其他平台完全统一,业务代码无需修改。
2. API 丰富灵活,多种调用模式
| 功能点 | API | 功能说明 |
|---|---|---|
| 主动触发扫描 | startScan() | 手动发起完整 WiFi 扫描。 |
| 拉取扫描结果 | getScannedResults() | 直接获取系统缓存最新扫描数据,无需手动触发扫描。 |
| 流式监听结果 | onScannedResultsAvailable | 提供 Stream,扫描结果更新自动回调,可搭配 StreamBuilder 更新 UI;可接收本应用、系统、其他应用触发扫描产生的数据。 |
| 权限预判接口 | canStartScan()、canGetScannedResults() | 提前校验扫描权限状态,支持权限申请,便于做异常分支处理;鸿蒙侧对位置、WiFi相关权限做封装适配。 |
3. 工程化体验良好
- 单例
WiFiScan.instance,调用简洁;配套示例代码、Demo、完整 API 文档。 - Dart 空安全支持,MIT 开源协议,支持社区提交 issue、PR。
- 提示资源释放,给出 dispose 销毁示例,规避内存泄漏;鸿蒙侧原生事件监听提供解绑逻辑,防止内存泄露。
补充局限:iOS 无苹果特殊授权仅为桩实现;Android、鸿蒙高版本均受系统扫描节流策略,不支持无限制高频扫描;鸿蒙部分敏感字段(真实BSSID)受系统隐私权限管控。
新增特性
- 新增 OpenHarmony(ohos)平台支持,基于
@ohos.wifiManager实现,与 Android/iOS 平台接口行为保持一致。
更多推荐






所有评论(0)