fultter wifi_scan三方库鸿蒙版本部署与使用GIF效果展示
Flutter 社区地址: https://atomgit.com/CPF-Flutter/flutter_flutter
github三方库地址:https://github.com/flutternetwork/WiFiFlutter/tree/master/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() 方法检查是否可以执行扫描操作,支持自动请求权限。
基础环境
Flutter版本:3.44.9
HarmonyOS:6.1.0(API 23)

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

演示的鸿蒙系统版本

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)