欢迎加入 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七种字符串之一
onNetworkStateChangedconnection_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_typefluttertpc_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.etspubspec.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/5Gradio.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 才发现 onNetConnection 实例的方法,得先建实例。而且更关键的一点:只挂 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;
}

这个映射不是"理论上要写",它在验证时直接命中了——后面第七节会看到,模拟器的默认网络本身就是以太网。

顺带一个容易记错的枚举值:NetBearTypeBEARER_CELLULAR=0BEARER_WIFI=1BEARER_BLUETOOTH=2BEARER_ETHERNET=3BEARER_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.signalTypeNetworkType 枚举,取值 NETWORK_TYPE_GSM=1CDMA=2WCDMA=3TDSCDMA=4LTE=5NR=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——模拟器的默认网络实际是以太网(ifconfigeth0 持有 10.0.2.15wlan0 连 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

Logo

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

更多推荐