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: 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 发来的 startScangetScannedResults 方法映射到鸿蒙 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_INFOSET_WIFI_INFOsystem_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

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐