做移动数据使用监管助手这类 App 的时候,WiFi 详情页是那种“看起来平平无奇、做起来到处是坑”的模块。产品经理在需求里可能只写一句“显示当前 WiFi 信息”,但真正落到 Flutter for OpenHarmony 的双端开发里,你需要处理的是一整套数据采集、双端通信、权限合规、UI 刷新和边界情况处理的问题。
我这次在 OpenHarmony 设备上用 Flutter 完整落地了一个移动数据监管助手的 WiFi 详情页,覆盖当前连接的 SSID/BSSID、信号强度与频段、实时上下行速率、本次会话流量、WiFi 与移动数据用量的对照,以及历史 7 天流量趋势。整个过程中,Flutter 的 MethodChannel 和 EventChannel 在 OpenHarmony 上的行为、系统接口的差异、XTS 兼容性要求,都实打实验证了一遍。这篇文章把完整实现路径和踩坑排查链路写出来,适合正在用 Flutter 开发 OpenHarmony 系统信息类、网络监控类 App 的开发者参考。
1. 项目立项:移动数据监管助手为什么必须做 WiFi 详情页
1.1 监管类 App 的核心场景拆解
移动数据使用监管助手的本质,是帮助用户回答一个问题:我的流量到底去哪了。这里的“流量”要拆成两条线看——移动数据(蜂窝网络)对应的是资费成本,用户最在乎“本月还剩多少、哪个应用在偷跑”;WiFi 对应的是使用体验,用户更在乎“当前连接质量怎么样、在这个 WiFi 下刷掉了多少量”。两条线之间不是割裂的,很多用户会发现“我明明开着 WiFi,为什么还在扣移动数据”,这正是监管助手最典型的痛点场景。所以 WiFi 详情页不是独立的网络信息展示页,它要承担两个职责:一是把当前 WiFi 连接状态讲清楚,二是把 WiFi 维度的用量数据纳入整个统计闭环。
在这个定位下,WiFi 详情页需要回答的具体问题就清晰了:
- 当前连的是哪个热点,是不是自家的路由器,有没有被“蹭网”;
- 信号为什么差,现在落在 2.4GHz 还是 5GHz 频段;
- 当前实际上下行速度大致是多少,和宽带套餐是否匹配;
- 这次连接 WiFi 期间累计收发多少流量;
- 最近一段时间的 WiFi 用量趋势,哪天异常偏高。
这五个问题直接推导出页面需要采集的数据字段,也推导出后续双端通信的方案。凡是把 WiFi 详情页做成了“只显示一个 SSID 和信号格”的,基本都没想清楚它在监管场景里的真实价值。
1.2 为什么选 Flutter 而不直接写 ArkTS/Java
这个项目在立项时就有明确的跨端诉求:同一个 App 需要同时覆盖 Android 设备和 OpenHarmony 设备。团队里已有的 Flutter 技术栈占了相当比重,如果 Android 端用 Kotlin 写一套、OpenHarmony 端用 ArkTS 再写一套,两套 UI 代码的维护成本在监管类 App 这种信息密度高的页面上会非常可观。Flutter for OpenHarmony 在这两年已经具备工程化落地的条件,OpenHarmony 基金会侧维护了适配分支,配合 DevEco Studio 可以从同一份 Dart 代码构建出可在 OpenHarmony 设备上安装的 hap 包,这是选它最核心的理由。
当然,这不是说 Flutter 在 OpenHarmony 上完全没有代价。最大的代价是生态差异:Flutter 插件的原生部分需要针对 OpenHarmony 的 API 重新实现,社区里现成的网络插件基本都指望不上。凡是涉及系统能力获取的功能,都要自己补一层 OpenHarmony 插件。所以我的建议是:如果只做 OpenHarmony 单端、团队又够人盯 ArkTS,那直接用 ArkTS 开发更稳;但只要你有多端复用的诉求,Flutter 的收益立刻大于成本。我们最终的技术栈是 Flutter 做 UI,OpenHarmony 系统 API 靠自有插件桥接,本地数据存储用纯 Dart 方案,尽量不引入带原生依赖的第三方包。
1.3 模块架构与 WiFi 详情的数据流
整个 App 的模块我切成四块:总览页负责本月移动数据与水流量总览,WiFi 详情页负责连接质量与 WiFi 用量,应用排行页负责分应用流量,后台服务负责超限提醒。页面之间的数据流动是单向的:
- OpenHarmony 插件层定时/事件驱动地获取系统数据;
- 通过 MethodChannel 和 EventChannel 把结构化数据送回 Flutter 层;
- Flutter 层统一转换成不可变的 Model 对象;
- UI 层根据状态刷新页面;
- 历史统计数据落到本地数据库,供趋势图取用。
这样的好处是 WiFi 详情页本身不直接碰系统能力,只依赖一个定义好的数据仓库接口。后续如果要换系统版本、换接口,只改插件层即可,页面层不用动。这个架构在后来的 XTS 合规调整里帮了大忙,这个后面细说。
2. WiFi 数据采集的两条通道:MethodChannel 与 EventChannel 的职责切分
2.1 先列清楚要采集哪些数据
动手写代码之前,务必先把数据清单列完整。我在第一版里就吃了“想到什么采什么”的亏,导致页面做了一半发现还缺网关信息,又回去改插件。最终清单如下:
| 数据字段 | 系统侧来源 | 页面用途 |
|---|---|---|
| SSID | WiFi 连接信息 | 显示当前热点名称 |
| BSSID | WiFi 连接信息 | 识别是否为已保存热点 |
| RSSI 信号强度 | WiFi 连接信息 | 信号格、弱网提示 |
| 信号等级 | WiFi 连接信息 | 简化展示 1-4 格 |
| 频段 | WiFi 连接信息 | 2.4GHz/5GHz 标识 |
| 链路速率 | WiFi 连接信息 | 理论速率显示 |
| IP/网关/DNS | IP 信息接口 | 网络故障排查辅助 |
| 连接状态 | 连接事件 | 断开/重连状态展示 |
| 本次会话 rx/tx | 网络统计接口 | 会话流量卡片 |
| 累计 rx/tx | 网络统计接口 | 当日/本周统计 |
| 历史 7 天量 | 本地数据库聚合 | 趋势柱状图 |
OpenHarmony 上 WiFi 连接信息主要通过@ohos.wifiManager模块的getLinkedInfo()获取,IP 相关信息走getIpInfo()。这里必须提醒一句:不同 API Level 的字段命名有差异,比如有些版本返回signalLevel,有些版本只有rssi需要客户端自行换算,不能假设一次写完就永不过时。
2.2 Dart 侧 MethodChannel 与 EventChannel 的划分逻辑
我在设计通道时做了一个明确切分:MethodChannel 管“一次性查询”,EventChannel 管“持续事件流”。这是两个完全不同的语义,硬塞到一起后面一定出问题。
一次性查询包括:获取当前 WiFi 信息、获取会话流量、获取历史统计数据。这类操作调用频率低、请求和响应是严格一对一的,用 MethodChannel 最直观。而连接状态变化、信号强度突变这类事件,天然是异步推送,用 EventChannel 能让 Flutter 侧以 Stream 的方式消费,语义上跟系统回调一一对应。
Dart 侧封装大致是这样的结构:
class WifiInfoRepository { static const _methodChannel = MethodChannel('flutter_open_harmony/wifi_info'); static const _eventChannel = EventChannel('flutter_open_harmony/wifi_event'); Future<WifiDetail> fetchCurrent() async { final raw = await _methodChannel.invokeMapMethod<String, dynamic>( 'getCurrentWifiInfo', ); if (raw == null) return WifiDetail.empty; return WifiDetail.fromMap(raw); } Stream<WifiStateSnapshot> stateStream() { return _eventChannel .receiveBroadcastStream() .map((event) => WifiStateSnapshot.fromMap(event as Map)); } }这里有两个细节值得注意。第一,invokeMapMethod<String, dynamic>的泛型一定要写清楚,如果 OpenHarmony 侧某个字段返回了null,泛型不匹配会导致整个调用抛MissingPluginException之外的诡异异常。第二,MethodChannel 返回的 Map 建议立刻转换成强类型 Model,不要在页面里到处map['ssid']散弹式取值,字段一旦拼错编译期不会报错,运行期全是硬伤。
2.3 OpenHarmony 侧插件实现与权限声明
OpenHarmony 侧的插件实现,我按 Flutter for OpenHarmony SDK 的插件规范来写。整体思路是:实现插件入口,在引擎附加时注册 MethodChannel 和 EventChannel,然后在回调里调用系统 API。伪结构大概是:
// 示例结构,具体类名与包签名以你接入的 Flutter for OpenHarmony SDK 为准 export class WifiInfoPlugin { onAttach(engine) { this.registerMethodChannel(engine); this.registerEventChannel(engine); } getCurrentWifiInfo(call) { // 调用 @ohos.wifiManager.getLinkedInfo() // 组装成 Map 返回 } }真正容易翻车的是权限声明。WiFi 信息采集涉及ohos.permission.GET_WIFI_INFO,网络状态涉及ohos.permission.GET_NETWORK_INFO,如果要访问网络还要声明ohos.permission.INTERNET。这些权限必须在module.json5中声明,部分权限还需要在运行时通过动态授权弹窗获取。千万不要图省事把不用的权限一起声明了,后面 XTS 合规检查会非常被动。
第一个版本我只在配置里声明了GET_WIFI_INFO,结果真机上getLinkedInfo()直接报错,错误码暗示权限不足。排查了很久才发现GET_NETWORK_INFO也必须要带上,因为连接信息接口在部分系统版本上依赖网络信息权限。建议你拿到一台真实设备后,第一时间把插件里每个系统接口对应的权限试一遍,形成自己的权限对照表。
3. WiFi 详情页的界面实现、图表选型与刷新策略
3.1 页面结构:实时状态区、会话统计区、历史趋势区
WiFi 详情页我按信息层级切成三个区域,用户从上往下浏览正好是“先看当前状态,再看本次用量,再看历史趋势”的递进关系。
顶部实时状态区是一张卡片:左侧显示 SSID 和 BSSID,右侧用自绘的信号格展示当前强度,下面一行小字标注频段、链路速率和 IP 信息。信号格我用CustomPaint画了 4 格柱状条,根据 RSSI 值映射到 0-4 档,比直接贴一张静态图片可维护性高得多,信号强度变化时只要重绘即可。
中部会话统计区放两张卡片:WiFi 本次连接上行流量、下行流量,附带实时速率。实时速率不是系统直接给的,需要自己算——读取某个时间点的累计字节数,等 2 秒再读一次,差值除以间隔就是速率:
final deltaBytes = (currentRx - lastRx) + (currentTx - lastTx); final speedKbps = deltaBytes * 8 / 1000 / intervalSeconds;底部历史趋势区用 7 天柱状图展示 WiFi 使用量。我把它和移动数据使用量做了双色对比,这样用户能一眼看出“这两天是不是 WiFi 用量异常高”。柱状图本身不需要花哨,清楚最重要。
3.2 图表方案对比:纯 Dart 图表库 vs 自绘
图表选型上我做了两轮对比。第一轮想直接用fl_chart,它功能全、社区活跃,在 Android 上表现没问题。但在 OpenHarmony 上要格外谨慎:这类图表库如果带了平台相关依赖,插件适配 OpenHarmony 之前大概率跑不起来。我检查了它的依赖树,确认它主要是 Canvas 绘制,风险较低,但斟酌之后还是放弃了,原因是这个场景的柱状图实在太简单,不值得引入一个大依赖去赌兼容性。
最终我用最简单的办法实现:7 根竖柱用Row排开,每根柱高度按百分比归一化计算:
final barHeight = maxBarHeight * (value / maxValue);这段代码不到 30 行,零依赖,渲染引擎底层走的是 Flutter Canvas,跨端都稳定。这里想说的是一个原则:在 OpenHarmony 这种生态还没完全成熟的平台上,能自绘就自绘,能少依赖就少依赖,越是基础功能越不要给第三方包留机会。
3.3 刷新节奏管理:前台定时轮询 + 后台暂停
WiFi 详情页最忌讳的是“无脑定时刷新”。如果每 500 毫秒查一次全量数据,插件层要连续走系统接口,页面还要频繁 rebuild,在低端设备上很快就卡顿。我的节奏是这样:连接状态和信号强度靠 EventChannel 事件驱动,不主动轮询;会话流量和速率用Timer.periodic每 2 秒拉一次;历史趋势数据每次进入页面加载一次,离开页面不刷新。
定时器还需要配合应用生命周期管理。页面切到后台时停止定时器,回到前台再恢复,否则后台空转几个小时白白耗电。实现上让页面State混入WidgetsBindingObserver:
@override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { _startTimer(); } else { _stopTimer(); } }事件流用StreamBuilder消费,页面退出时在dispose里一定要取消监听和销毁定时器。这个“取消订阅”的细节在热重启场景下尤其重要,后面专门讲。
4. 实测阶段最磨人的四个坑:XTS 合规、数值单位、断网回调与热重启
4.1 XTS 兼容性认证对权限申请的约束
先说结论:XTS 认证不是“可选项”,它是 OpenHarmony 设备/系统发行方做兼容性验证时跑的那套测试。你开发的 App 虽然没有直接参加 XTS 测试,但你的目标设备如果通过了 XTS,意味着设备对权限管控、API 调用的行为更规范更严格,违规调用会被直接拦下。
我遇到的实际问题是:插件侧某个版本为了调试方便,临时加了日志输出到外部存储的代码,顺手在配置里声明了一个存储权限。结果在一台经过严格兼容性测试的设备上,系统在安装时直接把这个 App 的非必要权限项列为高风险,某些接口的返回开始变得不稳定。排查两天后,把多余权限删掉、重新构建 hap 包,立刻恢复正常。
这个教训翻译成可执行操作就是三条:权限声明遵循最小化原则;每个权限要在代码里能找到对应的实际调用点;发版前用配置最严格的设备做一遍全功能回归。XTS 环境下的行为偏差往往比开发机上更隐蔽,别只在开发机测完就自信发版。
4.2 流量数值单位不一致引发的显示事故
这是我在开发中真正踩过的“数据格式对齐”大坑。有段时间详情页显示的 WiFi 会话流量总是比实际用量大几百倍,一度怀疑是系统接口读错了,后来才发现是我在单位换算上双重除以了 1024。
排查过程是这样的:插件层从系统拿到的是一个无符号长整型的字节数,为了让日志直观,我在 ArkTS 侧先把它转成了 MB 字符串输出。Dart 侧收到后,模型代码里又做了一次/ 1024 / 1024,等于把已经是 MB 的值再当成字节转了一次。日志里插件层打印的是12.6 MB,Dart 层打印的却是12902 MB,两边一对齐立刻定位到问题。
这类单位问题在流量统计场景特别容易犯,因为系统各个接口返回的单位并不统一:有的返回字节,有的返回千字节,甚至同一个模块不同 API Level 都在变。我的解决方案是:插件层统一返回字节数,字段名里明确带Bytes后缀,Dart 侧只在 UI 展示的那一刻做一次单位换算。全链路只允许一个单位标准,杜绝“到哪一步再转一次”的模糊操作。
4.3 WiFi 断开与弱信号下的异常防护
WiFi 是出了名不稳定的连接。实测中我发现,断网瞬间去调用getLinkedInfo(),部分系统版本会直接抛异常,错误码各不相同;信号极弱时,返回的字段会出现大量null。如果代码不做防护,页面会频繁闪错误页或者直接白屏。
我最终的处理框架是三层兜底:
- 所有系统接口调用包在
try/catch里,异常时返回上一次缓存的数据; - 对返回 Map 里可能为
null的字段设置默认值,空数据也渲染“未连接”占位状态; - 断网或信号异常时不弹 Toast 轰炸用户,只在状态区静默显示“信号弱”或“已断开”标志。
缓存上一次数据这个决策很关键。用户拿着手机在家里走动,信号从满格掉到一格再恢复,过程可能只有几秒,如果每个异常都清空页面,视觉上就是白屏闪烁。保留旧数据、只更新状态标志,体验平滑很多。
4.4 EventChannel 在热重启后的重复订阅问题
Flutter 开发中最常用的热重启(Hot Restart),在 OpenHarmony 插件上是把双刃剑。热重启会重新附加引擎、重新走一遍插件注册流程,但旧的事件流可能没有完全释放。结果就是:页面每次热重启后订阅 EventChannel,旧订阅还在,新订阅又加上,同一个连接状态事件被回调两次、三次甚至更多。
排查时我在事件回调里加了计数器,发现同样的wifiConnectionChange事件在页面停留 10 分钟后就出现过 4 次重复回调。解决办法有两层:Dart 侧在dispose里必须cancel()订阅;插件侧不要每次onAttach都新建广播流,而是做一个单例通道,重复附加时先移除旧监听再注册新监听。
这里给一个测试建议:每改一次插件代码,都做“热重启 → 切走页面 → 切回 → 观察回调次数”的固定操作。很多类似问题只在热重启后暴露,冷启动反而一切正常。
5. 数据边界与后续扩展:监管类 App 的诚实设计
5.1 应用层能拿到的流量统计到底有多完整
做监管类 App 必须面对一个诚实的问题:应用层永远拿不到运营商侧的完整详单。你能统计到的数据,是系统开放给你这个 App 的数据,主要包括系统网络统计接口给出的接口级累计字节数,以及你自身应用产生的流量。想精确到“系统里每个 App 分别在移动数据/WiFi 下用了多少”,依赖的是系统级使用统计能力或者特权权限,普通第三方 App 拿不到。
所以我把产品定位成“使用提醒 + 本地记录”:提醒用户当前连接状态、估算用量、帮用户建立流量使用习惯,而不是号称“绝对精确到 KB 的账单”。这个边界在产品文案和隐私说明里都写得很清楚。技术上能做到的合理上限是:统计当前网络接口的整体收发量,再叠加应用自身模块的用量记录,给用户一个可靠的参考值。这个定位想清楚之后,很多不切实际的需求就可以直接砍掉,开发成本也降下来不少。
5.2 权限与隐私披露的实操细节
监管类 App 天然会索取较多权限,隐私合规必须前置。我的做法是:第一,进入 WiFi 详情页时,系统授权弹窗出现之前先展示一页“为什么需要这些权限”的说明页,每个权限对应一条用途说明,用户同意后才会触发系统授权;第二,所有采集到的 WiFi 信息和用量记录只保存在设备本地,不做云端上传;第三,隐私政策里列全数据字段清单,包括 SSID、BSSID、连接时间戳和用量数据,并说明这些数据不会被用于画像或共享。
隐私设计还有一个实操细节:应用市场审核时,权限申请必须和功能强绑定。我见过不少同类 App 因为“申请了定位权限却说不清用途”被卡,在 OpenHarmony 生态的合规审核里同样严格。宁可把权限拆细一点申请,也不要贪多。
5.3 可继续扩展的路线
WiFi 详情页跑通之后,我列的后续扩展优先级是这样的:分应用流量排行,基于系统使用统计能力,按应用维度拆出移动数据与 WiFi 用量;用量的超额提醒,在用户接近移动数据阈值时推送通知;多设备家庭模式,把家里几台设备的用量集中到一台主设备上看。这三个方向都建立在已有的数据仓库和权限框架之上,不需要推倒架构。
最后分享一个从这次开发里沉淀下来的小习惯:插件层每一条系统接口的返回值,我都会打一条带固定 tag 的日志,Dart 侧收到回调后再打一条。出问题时先翻这两条日志,就能快速区分到底是系统行为异常还是我自己的代码写错。表面看多打了几行日志,但这类双端联调场景里,它比任何调试器都直接。希望你做同样的事情时,能少走我踩过的这些弯路。