尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Flutter插件鸿蒙化适配:flutter_iot_wifi WiFi配网功能迁移实战

Flutter插件鸿蒙化适配:flutter_iot_wifi WiFi配网功能迁移实战 前阵子把一个智能家居 App 的 Flutter 工程往 OpenHarmony 设备上迁移页面、状态管理、网络层都还算顺利卡得最久的反而是一个平时没人注意的插件flutter_iot_wifi。这个插件干的是 IoT 设备 WiFi 配网里最基础的事——扫描附近热点、读取当前连接 SSID、连接指定 WiFi是配网链路里绕不开的一环。如果你也在做 Flutter 鸿蒙化适配尤其是智能家居、摄像头、门锁这类需要配网能力的项目这篇文章应该能帮你少踩几个坑。我把这次适配过程完整复盘一遍包括工程结构差异、MethodChannel 桥接方式、ohos.wifiManager的具体调用姿势、权限和 Context 的坑以及最后真机验证的清单。整个过程不复杂但细节特别多任何一个地方没对齐配网就静默失败。1. 被低估的 flutter_iot_wifi项目立项时没人想到它是鸿蒙化的硬骨头1.1 这个插件在配网链路里的真实职责先捋清楚一件事flutter_iot_wifi不是什么炫技插件它在 IoT 配网里扮演的是系统 WiFi 能力翻译官。常见的配网方式有两种一种是设备开热点手机连上设备热点后把目标 WiFi 的 SSID 和密码发给设备另一种是手机和设备处在同一个局域网通过广播或扫码方式把 WiFi 凭证下发。不管哪种方式App 端都需要能扫描 WiFi 列表、获取当前连接的 WiFi 名称、发起连接切换这些能力在 Android/iOS 上被封装成了插件方法。这个插件实际暴露给 Dart 层的方法通常是这几个getSSID获取当前手机连接的 WiFi 名称。scan触发一次 WiFi 扫描。getScanResults拿到扫描到的热点列表包括 SSID、BSSID、信号强度、加密方式等。connect连接指定 WiFi配网场景里一般用于从设备热点切回目标路由器。在 Android 上这些功能全靠WifiManager配合一组动态权限实现在 iOS 上由于系统限制大部分插件只能做到跳转系统设置页真正能扫描和连接 WiFi 的少之又少。所以这个插件天然有很强的 Android 倾向鸿蒙化的时候没法简单平移得对着系统 API 重新实现一遍。1.2 为什么说最难的是系统能力适配Flutter 框架本身确实是跨平台的Dart 层代码基本不用改但插件不跨平台。插件本质是 MethodChannel 桥接到原生能力Android 端写得再好到了 OpenHarmony 上也得重写原生侧。难点集中在三块第一WiFi 系统 API 完全不同。Android 是WifiManagerOpenHarmony 是ohos.wifiManager方法名、参数类型、回调模型全不一样。第二权限模型不一样。Android 的 WiFi 权限集中在ACCESS_FINE_LOCATION、CHANGE_WIFI_STATE这类OpenHarmony 则是ohos.permission.GET_WIFI_INFO、ohos.permission.SET_WIFI_INFO、ohos.permission.MANAGE_WIFI_CONNECTION等命名体系基本不重叠。第三设备环境差异大。同一个 API 在不同的 OpenHarmony 版本、不同芯片方案上表现可能差很多x86 模拟器上 WiFi 模块甚至可能是摆设。理解了这个背景你就明白为什么说Flutter 鸿蒙化最花时间的往往不是 UI而是这些系统能力插件。下面我按实际适配顺序讲。2. 鸿蒙端 Flutter 插件开发从 Dart 通道到 ArkTS 原生实现的桥接差异2.1 先搭好 OpenHarmony Flutter SDK 环境鸿蒙化适配第一步是确认你手上的 Flutter 是不是支持 OpenHarmony 的版本。标准 Flutter SDK 目前还不会直接识别 OpenHarmony 设备得用社区维护的 OpenHarmony 分支或者华为提供的 Flutter OHOS SDK。这一步最容易踩的坑是环境串了电脑上既有标准 Flutter 又有 OHOS 分支命令敲错就构建到错误目标上。我的建议是用fvm管理多版本 Flutter。项目根目录加一个.fvmrc里面锁定用于 OpenHarmony 构建的 Flutter 版本然后统一用fvm flutter执行命令。这样标准 Flutter 和 OHOS 分支互不干扰哪天切回 Android/iOS 构建flutter pub get也不会把平台目录搞乱。环境准备好之后用flutter doctor确认设备发现是否正常。能识别到 OpenHarmony 设备通常意味着 SDK 路径、系统镜像都对齐了。如果识别不到先检查 DevEco Studio 版本和 Flutter OHOS 分支的匹配关系不要一上来就怀疑项目配置。2.2 插件工程的 MethodChannel 注册方式flutter_iot_wifi的 Dart 侧代码不用动还是那套MethodChannel。比如定义通道名和调用方法class FlutterIotWifi { static const MethodChannel _channel MethodChannel(flutter_iot_wifi); static FutureString? get ssid async { return await _channel.invokeMethod(getSSID); } static FutureListdynamic scan() async { return await _channel.invokeMethod(scan); } static Futurebool connect(String ssid, String password, String securityType) async { return await _channel.invokeMethod(connect, { ssid: ssid, password: password, securityType: securityType, }); } }鸿蒙原生侧要做的事是把这个flutter_iot_wifi通道注册到插件的原生模块里实现对应的 MethodCallHandler。OpenHarmony 的 Flutter 插件原生侧推荐用 ArkTS 写注册方式大致是import { MethodChannel } from ohos/flutter_ohos; let channel new MethodChannel(flutter_iot_wifi); channel.setMethodCallHandler((call) { switch (call.method) { case getSSID: return getCurrentSsid(); case scan: return scanWifi(); case connect: let args call.arguments as Recordstring, string; return connectWifi(args[ssid], args[password], args[securityType]); default: return Promise.reject(method not found); } });注意不同版本的 OHOS Flutter SDK 里MethodChannel的导入路径和构造函数可能有差异但注册逻辑基本是这个套路。重点是通道名必须和 Dart 侧完全一致大小写都不能差方法名、参数 key 也一样。我建议把方法名和参数名抽成常量Dart 和 ArkTS 各自维护一份避免改了 Dart 侧忘改原生侧。2.3 Flutter 版本和 OpenHarmony SDK 版本的匹配关系还有一点必须提前摸清Flutter 版本和 OpenHarmony SDK 版本是有对应关系的。社区维护的 OHOS Flutter 分支一般会滞后上游 Flutter 几个版本不可能上游刚发 3.24 你立刻在 OpenHarmony 上用。项目如果用了很新的 Flutter 特性要看 OHOS 分支是否已经支持否则编译期就会挂。这个版本匹配问题还牵扯到渲染。OpenHarmony 设备上跑 Flutter某些版本默认开启 Impeller 渲染后可能出现画面渲染异常比如黑屏、残影、文字不刷新。热搜词里 openharmony 画面渲染异常 和 flutter impeller 其实说的就是这一类问题。遇到这种情况先别怀疑插件适配可以用启动参数临时切回 Skia 渲染试试比如在启动时加渲染相关参数。我之前因为渲染异常白屏一度以为是flutter_iot_wifi把主线程卡死了查了很久才发现是渲染器兼容性问题。另外注意构建工具链的差异。OpenHarmony 的 Flutter 工程构建 HAP 包用的是hvigor不是 Android 的 Gradle。如果在构建时报 you are applying flutters main gradle plugin imperatively using the apply 这类错误多半是用了错误的构建命令或者分支混用。这个提示会误导人往 Gradle 方向排查实际上鸿蒙侧根本不走那套链路。3. 配网能力逐段翻译从 Android WifiManager 到 OpenHarmony wifiManager这一章是适配的核心我把flutter_iot_wifi的每个方法怎么在鸿蒙上实现讲清楚。这里所有 API 我基于 OpenHarmony 常见的 WiFi 能力接口来说明具体方法名可能随 SDK 版本略有调整但实现思路是一样的。3.1 扫描周边 Wi-Fi方法名一样回调模型完全不同Android 上扫描 WiFi 是两步走先wifiManager.startScan()然后监听SCAN_RESULTS_AVAILABLE_ACTION广播广播到了再调getScanResults()。OpenHarmony 的做法更像 Promise 风格扫描和拿结果都是直接调用模块方法。import wifiManager from ohos.wifiManager; async function scanWifi(): PromiseArrayRecordstring, Object { let results: ArrayRecordstring, Object []; if (!wifiManager.isWifiActive()) { await wifiManager.setWifiEnabled(true); } await wifiManager.scan(); // 扫描结果不是立即生效需要给系统一点时间缓存 let scanInfos await wifiManager.getScanResults(); scanInfos.forEach((item) { results.push({ ssid: item.ssid, bssid: item.bssid, securityType: securityToString(item.securityType), rssi: item.rssi, frequency: item.frequency, }); }); return results; }这中间有一个非常隐蔽的坑scan()返回成功不代表扫描结果已经刷新系统固件从扫描完成到结果可查之间可能有几百毫秒甚至更长的延迟。如果scan()之后立刻调getScanResults()拿到的很可能是上一次扫描的缓存。保险的做法是拿到scan()的完成信号后稍微等一下再读结果或者注册 WiFi 扫描完成事件事件到了再读取。具体等多久因设备方案而异保守起见 1 秒是底线。另一个坑是权限。扫描结果里包含热点名称和 BSSID这在很多系统里属于敏感信息除了 WiFi 权限外可能还需要位置权限。App 没授权位置信息时getScanResults()可能直接抛错也可能返回空数组不同版本表现不一样。适配的时候一定要把错误码透传回 Flutter 层而不是吞掉之后返回空列表否则业务层会误以为是周围没有 WiFi。3.2 连接指定 Wi-Fi从 WifiConfiguration 到 CandidateConfigAndroid 上连接 WiFi 的传统写法是构造一个WifiConfiguration设置 SSID、密码、安全类型然后addNetwork再enableNetwork。OpenHarmony 的接口设计有所不同一种常见思路是用候选配置的方式先把目标 WiFi 配置加入候选列表再发起连接。async function connectWifi(ssid: string, password: string, securityType: string): Promiseboolean { let security parseSecurityType(securityType); let config: wifiManager.WifiDeviceConfig { ssid: ssid, preSharedKey: password, securityType: security, }; let configId await wifiManager.addCandidateConfig(config); await wifiManager.connectToCandidateConfig(configId); return true; }这里有个细节需要特别小心不同 OpenHarmony 版本对连接 WiFi的 API 命名差异很大。有的版本提供的是connectToDevice(config)有的版本提供addCandidateConfig connectToCandidateConfig甚至部分版本还要求传入 BSSID 才能保证连接的是目标路由器而非同名热点。我在适配时专门翻了好几版 SDK 的接口差异最后选定候选配置方案因为它在无密码和 WPA2 场景下表现最稳定。安全类型映射也是个容易翻车的地方。Dart 层传来的是字符串比如WPA、WPA2、OPEN鸿蒙侧必须要转成系统枚举。映射写错最典型的情况是开放网络却填了 WPA2结果手机一直连不上目标 WiFi。建议在插件层做一次严格的白名单映射遇到不认识的字符串直接返回错误而不是默认当 WPA2 处理。3.3 获取当前 SSID看似简单却最容易暴露权限配置问题获取当前连接 WiFi 的 SSID 在 Android 上是wifiManager.connectionInfo.ssidOpenHarmony 上对应的是wifiManager.getLinkedInfo()async function getSSID(): Promisestring { let linkedInfo await wifiManager.getLinkedInfo(); return linkedInfo.ssid; }这个接口看起来简单但实际踩坑概率很高。如果你的权限配置只声明了ohos.permission.GET_WIFI_INFO某些版本上能拿到 WiFi 开关状态但拿不到 SSID 字段返回空字符串。需要把权限补齐比如增加ohos.permission.GET_WIFI_CONFIG或者位置相关权限。最头疼的是这种缺失不会让接口抛异常而是静默返回空值等传到 Flutter 层就变成没有连接 WiFi的假象排查起来非常绕。所以适配这个模块时最好在getSSID的返回值里同时带上当前连接的 BSSID 和 IP 信息这样排查问题时有更多线索。另外如果设备没有连接任何 WiFi不同版本可能抛errorCode也可能返回一个有默认值的对象Dart 侧要统一做空安全处理。3.4 配网特有的热点模式从设备热点切回目标路由器的完整链路IoT 配网里最典型的一个场景是设备热点配网。手机先连接设备开出来的热点比如SmartLife_XXXX然后 App 自动调用插件把手机切回目标 WiFi。这个切换过程不是发一条命令就结束需要等待系统真正连上目标 WiFi并且要感知到手机已经和旧热点断开这个状态变化。OpenHarmony 侧可以用wifiManager.on(wifiConnectionChange, callback)监听连接状态变化。需要在插件里把这种事件通过 EventChannel 暴露给 Flutter 层Dart 侧才能拿到连接成功或失败的结果。如果不想引入 EventChannel也可以在 Dart 侧轮询getSSID每 500ms 查一次等 SSID 变成目标 WiFi 名称就认为切换成功。轮询方案实现简单适配初期先用它快速跑通后期待 UI 稳定了再替换成事件驱动。热点模式还有一个体验问题切换 WiFi 会让当前 App 的 Socket 连接全部断开如果配网进程正在和设备做数据交互必须先让设备侧进入等待 WiFi 凭证的状态再发起切换否则设备那边会直接超时。3.5 返回数据结构保持兼容别让业务层到处if (isOpenHarmony)最后一点很重要鸿蒙端实现的返回数据结构要和 Android 端保持一致。比如 Android 版的扫描结果返回Mapkey 是字符串那么鸿蒙版也必须是同样的 key 集合安全类型返回的是字符串WPA2鸿蒙版就不要返回枚举值。这样业务层代码不用为鸿蒙单独写一套分支适配成本和回归风险都小很多。如果 Android 版某些字段在鸿蒙上拿不到宁可先不返回该字段也不要返回一个类型不匹配的占位值。Dart 层解析时用as Mapdynamic, dynamic再做一次类型收窄比依赖StandardMethodCodec自动转换更安全。4. 权限、Context 与线程三处最容易让真机崩溃的细节4.1 权限声明不是写在 module.json5 里就完事Android 的权限配置在AndroidManifest.xmlOpenHarmony 对应的是entry/src/main/module.json5里的requestPermissions。以 WiFi 配网能力为例通常要声明以下几类权限{ module: { name: entry, requestPermissions: [ { name: ohos.permission.GET_WIFI_INFO }, { name: ohos.permission.SET_WIFI_INFO }, { name: ohos.permission.LOCATION }, { name: ohos.permission.MANAGE_WIFI_CONNECTION } ] } }这里有两个陷阱。第一不同 OpenHarmony 版本的权限名不完全一致比如MANAGE_WIFI_CONNECTION在某个 API 等级之后可能改名或拆分直接照抄网上旧教程会掉坑。第二位置权限通常属于动态权限光声明还不够运行时得通过系统能力发起用户授权用户点击允许之后才能正常扫描 WiFi。如果跳过动态授权扫描结果就是空。另外插件的模块和应用主模块的权限声明是叠加关系。如果这是你自定义插件记得把权限声明写到最终会打包进 HAP 的模块里而不是写在纯插件工程的配置里就算了。我之前在 DevEco Studio 里改的是插件子工程的配置文件打包出来 HAP 里根本没有权限真机上直接表现为功能失效这个问题花了大半天才定位到。4.2 用对 ContextStage 模型下别乱拿全局上下文OpenHarmony 应用默认使用 Stage 模型整个应用的生命周期和 Android 的 Activity 模型很不一样。WiFi 接口本身大多不需要 Context但如果你要实现跳转到系统 WLAN 设置页这类功能就必须要有正确的UIAbilityContext不能随手在模块顶层调getContext()。我的做法是在 EntryAbility 的生命周期里拿到 context然后注入到插件模块中onWindowStageCreate(windowStage: window.WindowStage): void { let context this.context; // 注入到插件或工具类中 WifiBridge.getInstance().setContext(context); }千万注意不要在onCreate阶段就调用需要 UI 上下文的方法这时候窗口还没创建完一些系统服务未必可用。我见过有同事把 WiFi 初始化逻辑放在 Application 级入口里结果在冷启动阶段直接崩了。跳转 WLAN 设置页在 OpenHarmony 上也没有 Android 那么简单不同的系统版本可能要通过不同的 Ability 名称拉起设置应用。如果只是配网场景我的建议是不做跳转系统设置而是把去设置页手动连接当作备用引导文案让用户自己打开系统设置省去几十行系统能力差异的适配代码。4.3 线程与回调别把扫描结果序列化全塞到 Flutter 主线程wifiManager.scan()是异步 Promise本身不会阻塞主线程但要注意整个链路的线程模型。Flutter 的 MethodChannel 调用进来之后ArkTS 侧的处理逻辑如果太耗时比如把大量扫描结果做字符串拼接、类型转换、JSON 序列化仍然会卡住 UI 响应。OpenHarmony 上做耗时操作的正规姿势是用TaskPool或 Worker 把计算任务分发出去整理好结果再通过 Promise 返回给 Flutter。不过配网场景的扫描结果数据量通常不会特别大一般几十个 AP直接在主模块里处理问题不大。真正需要加锁的是重复调用问题。用户手快点了三次扫描按钮Dart 侧连着三次调scan()底层就可能出现上一次扫描还没结束、下一次又发起的情况结果返回的数据错乱或直接超时。我在适配时给扫描动作加了一个信号量如果上一次扫描尚未完成新的扫描请求直接返回最近一次成功的结果而不是重新触发系统扫描。这样用户感知不到异常代码也不会崩。4.4 状态监听EventChannel 的鸿蒙实现和释放逻辑如果你要做连接状态感知就要在插件里实现 EventChannel。Android 用 BroadcastReceiver 注册监听鸿蒙用wifiManager.on(wifiConnectionChange, ...)。对应关系是wifiManager.on(wifiConnectionChange, this.onWifiConnectionChange); wifiManager.off(wifiConnectionChange, this.onWifiConnectionChange);EventChannel 的 StreamHandler 里onListen回调触发时注册系统监听onCancel回调触发时注销。这里最容易被忽略的是多个页面同时监听同一个 EventChannel 时StreamHandler 的onCancel可能在还有页面需要监听的时候就被触发导致其余页面收不到事件。我建议状态监听只做一层封装由单例对象持有真正的系统监听页面订阅和取消订阅只在 Dart 侧维护计数不要反复on/off系统监听。另外如果 App 进入后台WiFi 状态变化事件依然可能触发Dart 侧收到事件后如果去做 UI 更新要考虑页面是否在前台。我见过配网页退到后台后连接成功回调回来时页面已经释放导致状态错乱。适配时可以在 Dart 侧加一个生命周期标记只有页面可见时才处理状态流转。5. 真机与模拟器上的实测复盘现象、根因、验证清单5.1 模拟器上扫不到热点是真的不行不是代码错适配过程中最打击人的一幕是代码逻辑检查了好几遍权限也补了API 也对齐了但在 OpenHarmony 官方 x86 模拟器上跑getScanResults()永远是空数组。一开始我怀疑是权限动态申请没生效反复切授权流程最后才发现是这个模拟器镜像的 WiFi 模块本身就不完整。这不是个例OpenHarmony 的 x86 模拟器对无线网卡的虚拟化支持一直很弱很多镜像里 WiFi 功能就是一个空壳能开关但不能真正扫描。如果你也遇到模拟器上跑不了配网功能不要死磕尽早换到 arm64 开发板或真机上验证。热搜词里 openharmony x86 和 liteos-m openharmony 设备兼容性测评 能看出大家在不同设备形态上适配时都栽过类似的跟头。我在 RK3568 开发板上测试时同一份代码在半小时内就扫到了一堆热点顺利得让人害怕。现在我的习惯是凡是涉及系统硬件能力的插件适配第一时间就用开发板做真实验证不要在模拟器上浪费超过一晚上。5.2 Impeller 渲染异常与 Flutter 分支切换的连带问题前面说过渲染异常容易干扰适配排查。这里补充一个实际场景我把 App 跑在 OpenHarmony 平板上第一次进入配网页时页面正常从配网页退出再进入页面偶尔会变成灰屏。一开始我以为是 EventChannel 泄漏导致内存暴涨抓了半天内存才发现是渲染层问题。后来切到软件渲染模式后灰屏消失。虽然软件渲染性能差一些但配网页本身 UI 不复杂对性能要求不高。如果你的 App 在鸿蒙上遇到偶发性白屏、残影、文字重叠可以先切渲染模式验证排除掉渲染问题后再回头排查插件逻辑。不然很容易被表面现象带偏把时间浪费在明明没问题的代码上。另外用fvm切换分支时注意一个顺序问题切换 Flutter 版本之后要重新执行fvm flutter pub get否则ohos平台目录可能是旧的。我遇到过切分支后直接构建 HAP结果插件原生目录缺失报错信息又给得模棱两可最后重新 pub get 才解决。5.3 一份可复用的验证清单整个适配完成后我用一份清单在开发板上逐项过避免遗漏。你也照着测一遍能省不少返工时间。验证项操作预期结果设备发现flutter doctor能识别到 OpenHarmony 设备权限声明检查 module.json5包含 WiFi 相关权限且版本正确动态授权首次启动配网页弹出位置授权框拒绝后不崩溃扫描热点调 scan getScanResults能列出周边 AP含 SSID、RSSI、安全类型连接开放网络connect无密码手机成功连接目标 WiFi连接 WPA2 网络connectWPA2手机成功连接目标 WiFi获取当前 SSIDgetSSID返回正确 WiFi 名称非空状态变化事件手动断开 WiFiDart 侧能收到连接状态变化异常输入传空 SSID 或错误安全类型返回明确错误码不崩溃回归 Android/iOS同一份 Dart 代码切回标准 Flutter配网功能不受鸿蒙分支影响适配完成的那天晚上我反复来回验证这十项发现最值得骄傲的不是把代码跑通了而是找到了一套不会误导自己的排查路径先确认 Flutter 分支和渲染模式再确认权限三件套最后才怀疑 API 调用本身。flutter_iot_wifi的鸿蒙化适配本身不算难难的是在一堆容易混淆的现象里准确找到真正的问题根源。最后再分享一个小技巧调试阶段在插件原生侧把每个方法的入参、错误码、耗时都打日志输出到文件。配网场景涉及手机和设备的交互一旦出错App 端、设备端、路由器三方都可能成为疑点。有了完整日志你能快速判定是插件没发起扫描还是扫描到了但连接失败省下来的是半夜抓耳挠腮的时间。
返回列表