Flutter for OpenHarmony 实战:三方库 connection_network_type 的鸿蒙化适配指南
欢迎加入 CPF-Flutter 鸿蒙社区:https://atomgit.com/CPF-Flutter
connection_network_type 干的事很单一:告诉你当前是 Wi-Fi、还是 2G/3G/4G/5G,或者干脆没网;外加一条"网络变了通知我"的流。Android 侧靠 ConnectivityManager,iOS 侧靠 Reachability,鸿蒙侧对应 @ohos.net.connection。
选它是因为它把一类看起来很简单、实际上到处是坑的适配暴露得很干净:状态只有七个,但每个状态背后都要跟系统接口对齐语义;而鸿蒙这边"监听网络变化"的写法和多数人的直觉不一样,写错了不报错、也不生效。
适配后的仓库:https://atomgit.com/oh-flutter/connection_network_type

环境准备:本文只讲适配本身,不重复环境搭建步骤。Flutter for OpenHarmony SDK、DevEco Studio、模拟器/真机的完整配置见官方指引:
https://atomgit.com/CPF-Flutter/flutter_samples/blob/master/docs/ohos/getting-started/flutter-oh-env-setup.md
一、先看清上游给了什么契约
契约只有两条通道。lib/src/connection_network_type_method_channel.dart:
final methodChannel = const MethodChannel('connection_network_type');
final eventChannel = const EventChannel("connection_network_type_status");
Future<NetworkStatus> currentNetworkStatus() async {
final String state = await methodChannel.invokeMethod("networkStatus");
return _convertFromState(state);
}
networkStatus 无参数,返回一个字符串;EventChannel 推的也是同一个字符串。字符串到枚举的映射是硬编码的 switch:
NetworkStatus _convertFromState(String state) {
switch (state) {
case "unreach": return NetworkStatus.unreachable;
case "mobile2G": return NetworkStatus.mobile2G;
case "mobile3G": return NetworkStatus.mobile3G;
case "wifi": return NetworkStatus.wifi;
case "mobile4G": return NetworkStatus.mobile4G;
case "mobile5G": return NetworkStatus.mobile5G;
case "mobileOther": return NetworkStatus.otherMobile;
default: return NetworkStatus.unreachable;
}
}
整理成表:
| Dart 侧 | 通道 | 参数 | 返回/事件 |
|---|---|---|---|
currentNetworkStatus() | connection_network_type / networkStatus | 无 | 七种字符串之一 |
onNetworkStateChanged | connection_network_type_status | 无 | 同上 |
这里有个必须留意的细节:枚举的声明顺序和字符串是错位的。
enum NetworkStatus {
unreachable, wifi, mobile2G, mobile3G, mobile4G, mobile5G, otherMobile
}
wifi 排在第二个,而字符串表里 wifi 在中间;otherMobile 对应的是 mobileOther(词序还反了)。所以原生侧只能传字符串,不能传下标——如果图省事传个 index,Dart 侧收到的 "0" 会直接落到 default 分支变成"无网络",而且不会报任何错。
还有一处宽容设计要注意:default 分支兜底成 unreachable。这意味着原生侧拼错一个字符串,表现是"永远无网络",不是崩溃。这类静默失败后面还会再遇到一次。
二、动手前先查重
查重结果干净:CPF-Flutter / oh-flutter / hxa-flutter / oh-tpc 四个组织、connection_network_type 与 fluttertpc_connection_network_type 两种命名都不存在,AtomGit 与 Gitee 全站搜索 0 条。
上游仓库 carlosgabrielmelo/connection_network_type 只有 20 条提交,原生目录是 [android, ios],没有 ohos,也没有鸿蒙分支。
查重的技术细节前面几篇讲过,这里只留结论:不能用仓库页面的 HTTP 状态码判断仓库是否存在(AtomGit 前端是 SPA 路由,不存在的仓库也返回 200),要用 contents API;也不能只看默认分支,oh-flutter 里有 feat/ohos_<库>_<版本> 这种分支存已完成的适配。
三、鸿蒙侧的 API 选型
flutter create --platforms ohos . 生成 ohos/ 后,本次适配真正要动的还是那一个 ArkTS 文件:
ohos/
├── index.ets # 只做导出,一行
├── oh-package.json5 # HAR 元信息
├── build-profile.json5 # 编译配置
└── src/main/
├── module.json5 # 模块声明
└── ets/components/plugin/
└── ConnectionNetworkTypePlugin.ets # ★ 全部 ArkTS 代码
index.ets 与 pubspec.yaml 里的 pluginClass 同名即可生效:
export { default } from './src/main/ets/components/plugin/ConnectionNetworkTypePlugin';
七种状态在鸿蒙上的判定链路:
| 判定 | OHOS 接口 | 是否需要权限 |
|---|---|---|
| 当前默认网络 | connection.getDefaultNetSync() | 需要 GET_NETWORK_INFO |
| 承载类型(WiFi/以太网/蜂窝…) | connection.getNetCapabilitiesSync(handle) | 需要 GET_NETWORK_INFO |
| 蜂窝制式 2G/3G/4G/5G | radio.getSignalInformationSync(slotId) | 不需要 |
| 网络变化监听 | NetConnection.on(...) + register() | register() 需要 GET_NETWORK_INFO |
要点一:connection.on(...) 不存在
按直觉写,会想当然地以为连接事件挂在 connection 命名空间上:
// ✗ 编译不过:Property 'on' does not exist on type 'typeof connection'
connection.on('netAvailable', callback);
翻 .d.ts 才发现 on 是 NetConnection 实例的方法,得先建实例。而且更关键的一点:只挂 on() 还不算订阅,必须再调 register():
const netConnection: connection.NetConnection = connection.createNetConnection();
netConnection.on('netAvailable', this.onNetHandleEvent);
netConnection.on('netLost', this.onNetHandleEvent);
netConnection.on('netCapabilitiesChange', this.onNetCapabilitiesEvent);
netConnection.on('netUnavailable', this.onNetUnavailableEvent);
// register() 才真正开始监听
netConnection.register((error: BusinessError): void => { /* ... */ });
on() 而不 register() 属于静默失效:编译通过、运行不报错、回调永远不来。这一条比前面说的"字符串拼错"更难查,因为它连日志都没有——只能靠通读接口注释发现 register() 的存在。
四个事件对应 Android 的一个 CONNECTIVITY_ACTION 广播:Android 只注册一个广播接收器,任何连通性变化都会回调;鸿蒙这边把"网络可用 / 丢失 / 完全不可用 / 能力变化"拆成了四个事件,所以四个都要挂上,行为才对等。
要点二:权限只影响一部分接口,缺了不崩
getDefaultNetSync()、getNetCapabilitiesSync()、register() 三个都要 ohos.permission.GET_NETWORK_INFO,而 getSignalInformationSync() 不需要。宿主应用得在 module.json5 里声明:
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" },
{ "name": "ohos.permission.GET_NETWORK_INFO" }
]
缺权限的后果不是崩溃,是 getDefaultNetSync() 抛 201:
try {
netHandle = connection.getDefaultNetSync();
} catch (error) {
const err = error as BusinessError;
Log.e(TAG, `getDefaultNetSync failed code=${err.code} message=${err.message}`);
return STATE_UNREACHABLE;
}
配合第一节说的"default 分支兜底成 unreachable",最终表现就是界面永远显示无网络——新手很容易据此判断"适配没生效",其实是权限没声明。
要点三:netId === 0 就是没有默认网络
getDefaultNetSync() 没网时不会抛错,而是返回一个 netId 为 0 的句柄。官方注释写得很明确:
Network ID, a value of 0 means that there is no default network, and the other values
must be greater than or equal to 100.
所以判定顺序是"先看有没有网,再看是什么网":
if (netHandle === null || netHandle === undefined || netHandle.netId === INVALID_NET_ID) {
return STATE_UNREACHABLE;
}
要点四:以太网要归到 wifi
这是本次适配里唯一一个不是技术问题、而是语义问题的决定。
上游的 NetworkStatus 枚举里有 wifi、有四种移动网络、有 unreachable,但没有以太网。Android 实现的做法是把以太网塞进 wifi:
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
return NetworkState.wifi.toString()
}
鸿蒙侧为了跨平台行为一致,照做:
// 与 Android 实现保持一致:以太网也归到 wifi
if (bearers.includes(connection.NetBearType.BEARER_WIFI) ||
bearers.includes(connection.NetBearType.BEARER_ETHERNET)) {
return STATE_WIFI;
}
这个映射不是"理论上要写",它在验证时直接命中了——后面第七节会看到,模拟器的默认网络本身就是以太网。
顺带一个容易记错的枚举值:NetBearType 是 BEARER_CELLULAR=0、BEARER_WIFI=1、BEARER_BLUETOOTH=2、BEARER_ETHERNET=3、BEARER_VPN=4。以太网是 3 不是 2,中间夹着蓝牙。
要点五:蜂窝制式靠"取最高的一条"
2G/3G/4G/5G 的判定要用 @ohos.telephony.radio。Android 读的是 networkInfo.subtype(当前数据网的子类型),鸿蒙没有这个字段,能拿到的是信号信息列表:
const infos: Array<radio.SignalInformation> = radio.getSignalInformationSync(DEFAULT_SLOT_ID);
SignalInformation.signalType 是 NetworkType 枚举,取值 NETWORK_TYPE_GSM=1、CDMA=2、WCDMA=3、TDSCDMA=4、LTE=5、NR=6。映射关系是现成的,麻烦的是列表里可能有多条——双卡、或者 4G 与 2G 共存时,官方没有说明哪一条是当前数据网。
处理办法是给每个制式打一个代次分,取最高的那条:
private static rankOf(type: radio.NetworkType): number {
switch (type) {
case radio.NetworkType.NETWORK_TYPE_NR: return 5;
case radio.NetworkType.NETWORK_TYPE_LTE: return 4;
case radio.NetworkType.NETWORK_TYPE_WCDMA:
case radio.NetworkType.NETWORK_TYPE_TDSCDMA: return 3;
case radio.NetworkType.NETWORK_TYPE_GSM:
case radio.NetworkType.NETWORK_TYPE_CDMA: return 2;
default: return 0;
}
}
这是本次适配里唯一一个靠判断而非文档确定的地方,所以它也进了"已知限制"——只在这条分支(默认网络承载是蜂窝)上才会走到,影响面有限,但语义上不如 Android 精确。
另外,getSignalInformationSync() 在无 SIM 卡、无电话能力的设备上会抛错(如部分模拟器),这里按"其它移动网络"兜底,与 Android 取不到 subtype 时的回退一致:
} catch (error) {
// 无 SIM 卡 / 无电话能力(如部分模拟器)时按"其它移动网络"上报
const err = error as BusinessError;
Log.i(TAG, `getSignalInformationSync unavailable code=${err.code}`);
return STATE_MOBILE_OTHER;
}
四、生命周期:unregister() 不能省
NetConnection 的注销方式和"挂上什么就摘掉什么"的直觉不同——它没有 off(),只有一个 unregister():
private stopListening(): void {
const netConnection = this.netConnection;
if (netConnection === null) {
return;
}
try {
// 必须 unregister:不注销会让订阅残留在网络服务上,反复热重载最终撞到
// register() 的 2101022(The number of requests exceeded the maximum allowed)。
netConnection.unregister((error: BusinessError): void => { /* ... */ });
} catch (error) {
// ...
}
this.netConnection = null;
this.listening = false;
}
不注销的代价是实打实的,register() 的注释里列了两个错误码:
2101008 - The callback already exists.
2101022 - The number of requests exceeded the maximum allowed.
也就是说开发期反复热重载,订阅会一直累加,最后直接注册失败。所以 onDetachedFromEngine 里第一件事就是注销:
onDetachedFromEngine(binding: FlutterPluginBinding): void {
this.stopListening();
this.sink = null;
this.channel?.setMethodCallHandler(null);
this.channel = null;
this.eventChannel = null;
}
还有一处和 bonsoir 一样的守卫:发事件前判 sink 是否为 null。Dart 侧 receiveBroadcastStream() 建立监听与原生侧 onListen 回调之间是有时序的,而鸿蒙的 register() 会立刻触发事件(下一节会看到),正好落在这个窗口里:
private notifyNetworkChanged(): void {
const sink = this.sink;
if (sink === null) {
return;
}
// ...
}
顺带一提,EventChannel.setStreamHandler() 的签名在类型上不接受 null,所以卸载时只把引用丢掉、不去反注册 handler——引擎都已经分离了,没必要。
五、pubspec 只加两行
ios:
pluginClass: ConnectionNetworkTypePlugin
ohos:
pluginClass: ConnectionNetworkTypePlugin
Dart 层一个字都没改。同样是单包仓库,下游引用也不需要额外路径:
dependencies:
connection_network_type:
git:
url: https://atomgit.com/oh-flutter/connection_network_type.git
ref: 1.0.1-ohos-1.0.0-beta.1
六、踩坑记录
flutter create --platforms ohos 会顺手补一堆模板文件
给已有插件加鸿蒙平台,标准做法是在工程根目录跑:
flutter create --platforms ohos .
它会生成 ohos/,这没问题。但同时它按默认模板补了一堆工程里本来没有的文件:
?? lib/connection_network_type_method_channel.dart # 和 lib/src/ 下已有的实现重名
?? lib/connection_network_type_platform_interface.dart
?? ios/Classes/ConnectionNetworkTypePlugin.swift # 和 ios/Classes/ 下已有的重名
?? android/build.gradle.kts
?? example/integration_test/
...
这些是模板自带的"默认工程"文件,跟本库的真实结构(实现都在 lib/src/)重复。它们不会被引用,但留在仓库里会误导后来者,必须逐个删掉——只保留 ohos/ 和 example/ohos/。
.metadata 的改动则是合法的,它记录新增了 ohos 平台,应该保留:
- platform: ios
create_revision: ...
base_revision: ...
+ - platform: ohos
+ create_revision: ...
+ base_revision: ...
示例工程的语言版本还锁在 Dart 2.18
示例的 pubspec.yaml 里是:
environment:
sdk: '>=2.18.1 <3.0.0'
语言版本由 SDK 约束的下界决定,也就是 2.18。我第一版演示页用了 records 和解构:
final (String label, IconData icon, Color color) = _statusMeta[_status] ?? ...;
编译直接失败:
lib/main.dart:119:54: Error: Expected an identifier, but got '='.
lib/main.dart:120:33: Error: The 'records' language feature is disabled for this library.
解决办法不是去改上游的 SDK 约束,而是把示例代码写成 2.18 兼容的形式——用一个普通类代替 record:
class _StatusStyle {
const _StatusStyle(this.label, this.icon, this.color);
final String label;
final IconData icon;
final Color color;
}
顺带踩到空安全的一个基础问题:非空字段声明了不初始化会直接编译不过,String _error; 要写成 String? _error;。
前几篇踩过的坑这里同样成立
不重复展开,只列结论:--target-platform ohos-x64(模拟器是 x64,默认 ohos-arm64 会装不上)、PUB_CACHE 与项目同盘、启动前 hdc shell power-shell wakeup、提交前清空 signingConfigs。
七、真机验证
验证在 HarmonyOS 7.0.0(26.0.0) Beta2 的 API 26 模拟器(ohos-x64)上完成。
查询(MethodChannel):启动后界面立即显示当前状态。

订阅(EventChannel):onListen 里调 register() 之后,原生侧立刻推来两条事件,界面「变化记录」里出现两条 变化 → wifi。
这里有个与 Android 不同的行为:Android 注册广播接收器不会立刻回调,而鸿蒙的 register() 会触发 netAvailable / netCapabilitiesChange,所以事件流一开始就会收到 1–2 条与当前状态相同的事件。适配时保留了原生行为没有去重——上游示例在 Dart 侧自己做了"值变了才 setState"的比对:
if (networkStatus.name != _networkStatus) {
setState(() { _networkStatus = networkStatus.name; });
}
重复查询(MethodChannel):连点两次「刷新当前状态」,每次都返回 wifi,列表相应新增两条记录。

原生日志:
FlutterEngineCxnRegistry --> Adding plugin: ConnectionNetworkTypePlugin
ConnectionNetworkTypePlugin --> default net 101 bearerTypes=[3]
ConnectionNetworkTypePlugin --> network change listener registered
ConnectionNetworkTypePlugin --> network changed -> wifi
关键的一行是 bearerTypes=[3]。3 就是 BEARER_ETHERNET——模拟器的默认网络实际是以太网(ifconfig 里 eth0 持有 10.0.2.15,wlan0 连 IP 都没有)。也就是说第三节里"以太网归到 wifi"那条映射,不是纸面上的假设,而是被真实验证过的路径;如果当初照直觉按 BEARER_WIFI 单独判断,模拟器上会直接得到 unreach。
没能验证的一项:真实的链路中断与恢复。模拟器工具链没有网络开关(devecocli emulator 只有 shake / power / rotate / volume / battery / geolocation / scene / sensor),而 hdc shell 的身份是 uid=2000(shell),改网卡直接被拒:
ifconfig: ioctl 8914: Permission denied
所以"网络真的变化时事件是否触发"这一条没有实测。已实测的是:订阅注册成功(register() 回调无错)、事件能经 EventChannel 送达 Dart 并驱动界面刷新。两者之间的差别我写进了仓库的已知限制里,没有含糊过去。
八、已知限制
四条,都写进了仓库的 README.OpenHarmony_CN.md。
以太网会被报成 wifi。 为了与 Android 对齐,但语义上不精确。上游枚举没有以太网成员,不新增枚举就无法表达——而新增枚举就破坏了"Dart 上层 API 完全不变"的前提。
BEARER_VPN 和其它承载返回 unreach。 与 Android 的兜底行为一致(Android 对未知 transport 同样返回 unreachable)。VPN 场景下如果需要区分,得改 Dart 层的枚举,超出适配范围。
蜂窝制式取"已注册网络里制式最高的一条"。 原因见第三节要点五:官方没有说明 getSignalInformationSync() 返回的多条里哪一条是当前数据网。
注册时会立刻推送一次当前状态。 OHOS 的 register() 触发 netAvailable / netCapabilitiesChange,所以事件流开头会有 1–2 条与当前状态相同的事件;调用方如果只想要"变化",需要自己比对上一次的值。
另外还有一条不在插件职责内、但使用者一定会遇到的:宿主应用必须声明 ohos.permission.GET_NETWORK_INFO,缺了会表现为"永远无网络"而不是报错。
小结
这个库的状态空间很小,七个值,但适配过程把几类典型问题都过了一遍:
- 契约里藏着错位——枚举顺序和字符串不对应,传下标就会静默变成"无网络";
- 接口的挂载方式反直觉——事件在
NetConnection实例上,on()之后还得register(),漏了不报错; - 权限缺失表现为业务错误——缺
GET_NETWORK_INFO不是崩溃,是"永远无网络"; - 有些决定不是技术问题——以太网归到
wifi是跟着 Android 走的语义取舍,而这次验证恰好证明它是必需的。
最后一条我觉得最值得记:如果当初"按鸿蒙的接口如实映射",给以太网单独返回点什么,Dart 侧的 switch 会把它兜底成 unreachable,模拟器上直接显示"无网络"。跨平台适配里,"忠于本平台"常常是错的,"忠于上游既有行为"才是对的。
适配后的仓库和完整文档在这里:
https://atomgit.com/oh-flutter/connection_network_type
dependencies:
connection_network_type:
git:
url: https://atomgit.com/oh-flutter/connection_network_type.git
ref: 1.0.1-ohos-1.0.0-beta.1
欢迎加入 CPF-Flutter 鸿蒙社区:https://atomgit.com/CPF-Flutter
Flutter 三方库鸿蒙适配清单:https://atomgit.com/oh-flutter/flutter-ohos-adaptation-checklist
更多推荐




所有评论(0)