基础环境

Flutter 社区地址: https://atomgit.com/CPF-Flutter/flutter_flutter
三方库地址:https://atomgit.com/oh-flutter/wifi_scan
适配后地址:https://atomgit.com/oh-flutter/wifi_scan
鸿蒙版本:7.0

在这里插入图片描述

演示GIF

在这里插入图片描述

演示的鸿蒙系统版本

在这里插入图片描述

库的概述

wifi_scan 原本是 flutternetwork/WiFiFlutter 提供的 Flutter WiFi 扫描插件,官方版本支持 Android、iOS 两个平台。通过社区的努力,现阶段已经支持鸿蒙方向。

主要功能

用于扫描附近可见 WiFi 接入点的 Flutter 插件。

WiFi 扫描

通过 WiFiScan.instance.startScan() 一行调用,由鸿蒙原生 wifiManager.startScan() 实现。扫描完成后可通过 getScannedResults() 获取扫描结果。

扫描结果监听

通过 onScannedResultsAvailable 流监听扫描结果变化,当新的扫描结果可用时自动推送更新。

权限检查

提供 canStartScan()canGetScannedResults() 方法检查是否可以执行扫描操作,支持自动请求权限。

API 说明

API描述参数返回值OpenHarmony支持
canStartScan()检查是否可以启动扫描askPermissions: boolFuture<CanStartScan>
startScan()启动WiFi扫描Future<bool>
canGetScannedResults()检查是否可以获取扫描结果askPermissions: boolFuture<CanGetScannedResults>
getScannedResults()获取扫描结果Future<List<WiFiAccessPoint>>
onScannedResultsAvailable扫描结果可用时的流Stream<List<WiFiAccessPoint>>

核心代码

下面挑四个关键的代码块来说明。

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 权限,在安装时授予,无需运行时弹窗授权。

新增特性

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

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

更多推荐