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 两个平台。鸿蒙化目标:

  1. Dart 层 API 完全不变——业务方迁移时零改动,仅更换依赖来源;
  2. 通道协议完全不变——MethodChannel / EventChannel 的通道名、方法名、参数、返回值、错误码语义与原平台对齐;
  3. 鸿蒙端能力对齐——扫描、获取缓存结果、扫描结果流监听、前置能力检查(can*)全部可用;
  4. 平台语义差异显式处理——HarmonyOS 权限模型与 Android 不同,需要对 can-code、字段缺失、BSSID 随机化等差异做明确的映射与文档说明。

二、基础环境

项版本
Flutter3.41.10-ohos-1.0.0(CPF-Flutter/flutter_flutter 鸿蒙化分支)
DevEco Studio26.0.0
OpenHarmony SDK5.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 → canStartScanFuture<CanStartScan>检查是否可以启动扫描
startScan()MethodChannel wifi_scan → startScanFuture<bool>触发一次扫描
canGetScannedResults({askPermissions})MethodChannel wifi_scan → canGetScannedResultsFuture<CanGetScannedResults>检查是否可以获取结果
getScannedResults()MethodChannel wifi_scan → getScannedResultsFuture<List<WiFiAccessPoint>>获取最近一次扫描缓存
onScannedResultsAvailableEventChannel wifi_scan/onScannedResultsAvailableStream<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):

codeCanStartScan / CanGetScannedResults语义
0notSupported平台不支持该功能
1yes可以调用
2noLocationPermissionRequired缺少权限,可申请
3noLocationPermissionDenied权限被拒
4noLocationPermissionUpgradeAccuracy需升级定位精度
5noLocationServiceDisabled定位服务未开启

注意:Dart 侧对超出 0~5 的 code 会抛 UnsupportedError,鸿蒙端只能返回 0~5 范围内的值。

扫描结果 wire 格式(WiFiAccessPoint._fromMap 解析的字段):

wire keyDart 字段类型
ssidssidString
bssidbssidString
capabilitiescapabilitiesString
frequencyfrequencyint (MHz)
levellevel (RSSI)int (dBm)
timestamptimestampint?(启动以来微秒)
standardstandardint?(1=legacy, 4=n, 5=ac, 6=ax, 7=ad)
centerFrequency0centerFrequency0int?
centerFrequency1centerFrequency1int?
channelWidthchannelWidthint?(0=20MHz, 1=40, 2=80, 3=160, 4=80+80)
isPasspointisPasspointbool?
operatorFriendlyNameoperatorFriendlyNameString?
venueNamevenueNameString?
is80211mcResponderis80211mcResponderbool?

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  │
└────────────────────────────────────────────────────────────┘

决策点:

  1. 采用 Flutter 插件 v2 规范下的 ohos 平台声明:在 pubspec.yaml 的 flutter.plugin.platforms 中新增 ohos 段,由 Flutter 工具(鸿蒙化版本)自动生成 HAP 工程骨架并管理 HAR 依赖;
  2. 插件以 HAR 模块形式交付:ohos/ 目录是一个标准 Stage 模型 HAR 模块(module.json5 中 type: "har"),打包后以 wifi_scan.har 被宿主工程引用;
  3. Dart 层零改动:所有平台差异收敛在 ArkTS 实现内部,通过 can-code 重映射和 null 字段体现。

4.2 接口语义映射(核心设计)

Dart APIHarmonyOS 实现语义差异处理
canStartScan / canGetScannedResultscomputeCanCode()鸿蒙无定位权限概念,can-code 3/4/5 无对应场景:STA 能力不支持 → 0;缺 WiFi 权限 → 2;正常 → 1
startScanwifiManager.startScan()先查 SET_WIFI_INFO 权限,再查 isWifiActive()(WLAN 未开返回 false,与 Android 行为一致)
getScannedResultswifiManager.getScanInfoList()查 GET_WIFI_INFO 权限;WLAN 关闭/无缓存返回空列表(对齐 Android 行为)
onScannedResultsAvailablewifiManager.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同名字段直接透传
levelinfo.rssi鸿蒙字段名为 rssi,wire 上保持 level
centerFrequency0 / centerFrequency1同名字段直接透传
channelWidthinfo.channelWidth 在 0~4 范围内直接透传,否则 null与 Dart 枚举下标一致
standard无对应字段 → nullDart 侧反序列化为 WiFiStandards.unkown
isPasspoint / operatorFriendlyName / venueName / is80211mcResponder无对应字段 → nullWifiScanInfo 不暴露 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_INFOsystem_grant安装时自动授予读取 WiFi 信息(扫描结果列表)
ohos.permission.SET_WIFI_INFOsystem_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: boolFuture<CanStartScan>是
startScan()启动WiFi扫描无Future<bool>是
canGetScannedResults()检查是否可以获取扫描结果askPermissions: boolFuture<CanGetScannedResults>是
getScannedResults()获取扫描结果无Future<List<WiFiAccessPoint>>是
onScannedResultsAvailable扫描结果可用时的流无Stream<List<WiFiAccessPoint>>是

具体工作流程

左侧 · 主动扫描(拉模式)

应用主动发起,按以下三步顺序执行:

  1. 权限预判:调用 canStartScan() 检查扫描条件

    • 鸿蒙侧通过 verifyAccessTokenSync 校验 GET_WIFI_INFO、SET_WIFI_INFO 权限
    • 同时校验 STA 能力是否支持、WLAN 服务是否可用
    • 权限不通过时返回对应错误码,不进入下一步
  2. 启动扫描:调用 startScan() 触发硬件扫描

    • 经 MethodChannel(通道名 wifi_scan)转发到鸿蒙原生层
    • 原生端 handleStartScan 检查 WiFi 状态后调用 wifiManager.startScan()
    • WLAN 硬件扫描周边接入点,结果写入系统缓存
  3. 拉取结果:调用 getScannedResults() 获取扫描数据

    • 可独立调用,无需每次都先触发扫描,直接读取系统缓存
    • 原生端 handleGetScannedResults 调用 getScanInfoList() 获取原始数据
    • 经 toWireMap() 转换为 Dart 可识别的 WiFiAccessPoint 列表返回
    • 返回字段包含 ssid、bssid、level、frequency、capabilities、channelWidth 等

右侧 · 流式监听(推模式)

应用订阅后由系统主动推送,生命周期分为三个阶段:

  1. 建立订阅:监听 onScannedResultsAvailable 流

    • Dart 层通过 StreamSubscription 订阅,EventChannel(通道名 wifi_scan/onScannedResultsAvailable)建立广播流
    • 原生端 onListen 回调中执行 registerScanListener() 注册系统扫描状态回调
    • 同时调用 pushScannedResults() 立即推送一次当前缓存结果,避免首屏空白
  2. 持续推送:扫描完成后自动下发数据

    • 触发来源不限:本应用、系统服务、其他 App 触发的扫描均可被监听
    • 扫描完成后原生端回调执行 eventSink.success(results) 向下游推送
    • Dart 层 Stream.listen 回调触发,执行 setState(() => accessPoints = results) 更新状态
    • 可搭配 StreamBuilder 实现 UI 响应式自动刷新 WiFi 列表
  3. 资源释放:页面销毁时解绑监听

    • 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支持
ssidStringWiFi网络的SSID(网络名称)是
bssidStringWiFi接入点的BSSID(MAC地址)是(注意:无GET_WIFI_PEERS_MAC权限时可能为随机值)
capabilitiesString网络的安全能力描述是
frequencyint频率(MHz)是
levelint信号强度(dBm)是
timestampint扫描时间戳(微秒)是
standardint?WiFi标准(802.11a/b/g/n/ac/ax等)否(返回null)
centerFrequency0int?中心频率0(用于80+80/160MHz)是
centerFrequency1int?中心频率1(用于80+80/160MHz)是
channelWidthint?信道宽度(20/40/80/160MHz)是
isPasspointbool?是否为Passpoint网络否(返回null)
operatorFriendlyNameString?运营商友好名称否(返回null)
venueNameString?场地名称否(返回null)
is80211mcResponderbool?是否支持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. 工程化体验良好

  1. 单例 WiFiScan.instance,调用简洁;配套示例代码、Demo、完整 API 文档。
  2. Dart 空安全支持,MIT 开源协议,支持社区提交 issue、PR。
  3. 提示资源释放,给出 dispose 销毁示例,规避内存泄漏;鸿蒙侧原生事件监听提供解绑逻辑,防止内存泄露。

补充局限:iOS 无苹果特殊授权仅为桩实现;Android、鸿蒙高版本均受系统扫描节流策略,不支持无限制高频扫描;鸿蒙部分敏感字段(真实BSSID)受系统隐私权限管控。

新增特性

  • 新增 OpenHarmony(ohos)平台支持,基于 @ohos.wifiManager 实现,与 Android/iOS 平台接口行为保持一致。
Logo

一站式 AI 云服务平台

更多推荐