网络状态信息查询实战:基于状态管理 V2 的完整实现指南
摘要 :本文基于 HarmonyOS (API 23+)平台,使用 ArkTS 语言与状态管理 V2 (
@ComponentV2、@ObservedV2、@Trace、@Local、@Computed)开发一个动态网络状态信息查询 应用。通过@kit.NetworkKit的connection模块获取网络带宽、网络类型、网卡名称、MTU、链路地址、路由信息等核心数据,并以现代化卡片式 UI 呈现。文章涵盖架构设计、核心 API 解析、分步实现、深色模式适配及最佳实践,适合 HarmonyOS 开发者参考学习。
效果

一、项目概述
1.1 功能说明
本案例实现一个网络状态信息查询工具,用户点击"查询网络状态"按钮后,应用实时获取并展示以下信息:
| 序号 | 信息项 | 说明 |
|---|---|---|
| 1 | 上/下行带宽 | 当前网络的上行/下行带宽(kb/s),0 表示无法评估 |
| 2 | 网络类型 | 蜂窝网络、Wi-Fi、以太网、VPN 等 |
| 3 | 网卡名称 | 网络接口名称(如 wlan0、rmnet0) |
| 4 | 最大传输单元 | MTU 值,单位为字节 |
| 5 | 链路信息 | IP 地址列表(IPv4/IPv6) |
| 6 | 路由网卡名称 | 路由绑定的网络接口 |
| 7 | 目的地址 | 路由目标网络地址 |
| 8 | 网关地址 | 路由网关 IP 地址 |
| 9 | 是否有网关 | 标识路由是否配置了网关 |
| 10 | 默认路由 | 标识是否为系统默认路由 |
当设备未连接网络时,显示"未连接"提示;查询出错时,展示错误信息。
1.2 技术亮点
- 状态管理 V2 全面应用 :使用
@ComponentV2、@ObservedV2、@Trace、@Local、@Computed等 V2 装饰器 - 细粒度响应式更新 :
@ObservedV2+@Trace确保只有变化的属性触发 UI 刷新 @Computed缓存派生属性 :避免重复计算hasError、itemCount等派生值- async/await 异步模式:替代传统回调,代码逻辑更清晰
- 深色模式自动适配 :
base/与dark/双资源目录,跟随系统主题切换 - 类型安全的数据模型 :
@ObservedV2装饰的强类型数据类,替代数组索引取值
1.3 环境要求
| 工具/环境 | 版本要求 |
|---|---|
| DevEco Studio | 6.1 Release 及以上 |
| HarmonyOS SDK | API 23+ |
| 运行设备 | HarmonyOS NEXT(手机/平板/2in1) |
二、架构设计
2.1 项目目录结构
entry/src/main/
├── ets/
│ ├── constants/
│ │ └── Constants.ets # 常量定义 + @ObservedV2 数据模型
│ ├── entryability/
│ │ └── EntryAbility.ets # 应用入口 Ability
│ ├── pages/
│ │ └── Index.ets # 主页面(@ComponentV2)
│ └── utils/
│ └── NetworkUtil.ets # 网络查询工具类
├── resources/
│ ├── base/element/
│ │ ├── color.json # 浅色模式颜色资源
│ │ ├── float.json # 尺寸资源
│ │ └── string.json # 字符串资源
│ ├── dark/element/
│ │ └── color.json # 深色模式颜色资源
│ └── base/profile/
│ └── main_pages.json # 页面路由配置
└── module.json5 # 模块配置(含权限声明)
2.2 分层架构图
#mermaid-svg-hzbBhZOVUuhtSnSZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hzbBhZOVUuhtSnSZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hzbBhZOVUuhtSnSZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hzbBhZOVUuhtSnSZ .error-icon{fill:#552222;}#mermaid-svg-hzbBhZOVUuhtSnSZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hzbBhZOVUuhtSnSZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hzbBhZOVUuhtSnSZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hzbBhZOVUuhtSnSZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hzbBhZOVUuhtSnSZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hzbBhZOVUuhtSnSZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hzbBhZOVUuhtSnSZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hzbBhZOVUuhtSnSZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hzbBhZOVUuhtSnSZ .marker.cross{stroke:#333333;}#mermaid-svg-hzbBhZOVUuhtSnSZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hzbBhZOVUuhtSnSZ p{margin:0;}#mermaid-svg-hzbBhZOVUuhtSnSZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hzbBhZOVUuhtSnSZ .cluster-label text{fill:#333;}#mermaid-svg-hzbBhZOVUuhtSnSZ .cluster-label span{color:#333;}#mermaid-svg-hzbBhZOVUuhtSnSZ .cluster-label span p{background-color:transparent;}#mermaid-svg-hzbBhZOVUuhtSnSZ .label text,#mermaid-svg-hzbBhZOVUuhtSnSZ span{fill:#333;color:#333;}#mermaid-svg-hzbBhZOVUuhtSnSZ .node rect,#mermaid-svg-hzbBhZOVUuhtSnSZ .node circle,#mermaid-svg-hzbBhZOVUuhtSnSZ .node ellipse,#mermaid-svg-hzbBhZOVUuhtSnSZ .node polygon,#mermaid-svg-hzbBhZOVUuhtSnSZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hzbBhZOVUuhtSnSZ .rough-node .label text,#mermaid-svg-hzbBhZOVUuhtSnSZ .node .label text,#mermaid-svg-hzbBhZOVUuhtSnSZ .image-shape .label,#mermaid-svg-hzbBhZOVUuhtSnSZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-hzbBhZOVUuhtSnSZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hzbBhZOVUuhtSnSZ .rough-node .label,#mermaid-svg-hzbBhZOVUuhtSnSZ .node .label,#mermaid-svg-hzbBhZOVUuhtSnSZ .image-shape .label,#mermaid-svg-hzbBhZOVUuhtSnSZ .icon-shape .label{text-align:center;}#mermaid-svg-hzbBhZOVUuhtSnSZ .node.clickable{cursor:pointer;}#mermaid-svg-hzbBhZOVUuhtSnSZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hzbBhZOVUuhtSnSZ .arrowheadPath{fill:#333333;}#mermaid-svg-hzbBhZOVUuhtSnSZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hzbBhZOVUuhtSnSZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hzbBhZOVUuhtSnSZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hzbBhZOVUuhtSnSZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hzbBhZOVUuhtSnSZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hzbBhZOVUuhtSnSZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hzbBhZOVUuhtSnSZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hzbBhZOVUuhtSnSZ .cluster text{fill:#333;}#mermaid-svg-hzbBhZOVUuhtSnSZ .cluster span{color:#333;}#mermaid-svg-hzbBhZOVUuhtSnSZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hzbBhZOVUuhtSnSZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hzbBhZOVUuhtSnSZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-hzbBhZOVUuhtSnSZ .icon-shape,#mermaid-svg-hzbBhZOVUuhtSnSZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hzbBhZOVUuhtSnSZ .icon-shape p,#mermaid-svg-hzbBhZOVUuhtSnSZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hzbBhZOVUuhtSnSZ .icon-shape .label rect,#mermaid-svg-hzbBhZOVUuhtSnSZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hzbBhZOVUuhtSnSZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hzbBhZOVUuhtSnSZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hzbBhZOVUuhtSnSZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Index.ets (@ComponentV2)
NetworkUtil.ets (工具层)
Constants.ets (模型+常量)
@kit.NetworkKit (connection)
系统网络服务
@Local + AppStorage (安全区域)
EntryAbility.ets (AppStorage)
2.3 数据流图
#mermaid-svg-hfv6mWWLvOErxkPi{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hfv6mWWLvOErxkPi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hfv6mWWLvOErxkPi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hfv6mWWLvOErxkPi .error-icon{fill:#552222;}#mermaid-svg-hfv6mWWLvOErxkPi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hfv6mWWLvOErxkPi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hfv6mWWLvOErxkPi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hfv6mWWLvOErxkPi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hfv6mWWLvOErxkPi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hfv6mWWLvOErxkPi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hfv6mWWLvOErxkPi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hfv6mWWLvOErxkPi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hfv6mWWLvOErxkPi .marker.cross{stroke:#333333;}#mermaid-svg-hfv6mWWLvOErxkPi svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hfv6mWWLvOErxkPi p{margin:0;}#mermaid-svg-hfv6mWWLvOErxkPi .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hfv6mWWLvOErxkPi .cluster-label text{fill:#333;}#mermaid-svg-hfv6mWWLvOErxkPi .cluster-label span{color:#333;}#mermaid-svg-hfv6mWWLvOErxkPi .cluster-label span p{background-color:transparent;}#mermaid-svg-hfv6mWWLvOErxkPi .label text,#mermaid-svg-hfv6mWWLvOErxkPi span{fill:#333;color:#333;}#mermaid-svg-hfv6mWWLvOErxkPi .node rect,#mermaid-svg-hfv6mWWLvOErxkPi .node circle,#mermaid-svg-hfv6mWWLvOErxkPi .node ellipse,#mermaid-svg-hfv6mWWLvOErxkPi .node polygon,#mermaid-svg-hfv6mWWLvOErxkPi .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hfv6mWWLvOErxkPi .rough-node .label text,#mermaid-svg-hfv6mWWLvOErxkPi .node .label text,#mermaid-svg-hfv6mWWLvOErxkPi .image-shape .label,#mermaid-svg-hfv6mWWLvOErxkPi .icon-shape .label{text-anchor:middle;}#mermaid-svg-hfv6mWWLvOErxkPi .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hfv6mWWLvOErxkPi .rough-node .label,#mermaid-svg-hfv6mWWLvOErxkPi .node .label,#mermaid-svg-hfv6mWWLvOErxkPi .image-shape .label,#mermaid-svg-hfv6mWWLvOErxkPi .icon-shape .label{text-align:center;}#mermaid-svg-hfv6mWWLvOErxkPi .node.clickable{cursor:pointer;}#mermaid-svg-hfv6mWWLvOErxkPi .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hfv6mWWLvOErxkPi .arrowheadPath{fill:#333333;}#mermaid-svg-hfv6mWWLvOErxkPi .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hfv6mWWLvOErxkPi .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hfv6mWWLvOErxkPi .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hfv6mWWLvOErxkPi .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hfv6mWWLvOErxkPi .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hfv6mWWLvOErxkPi .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hfv6mWWLvOErxkPi .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hfv6mWWLvOErxkPi .cluster text{fill:#333;}#mermaid-svg-hfv6mWWLvOErxkPi .cluster span{color:#333;}#mermaid-svg-hfv6mWWLvOErxkPi div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hfv6mWWLvOErxkPi .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hfv6mWWLvOErxkPi rect.text{fill:none;stroke-width:0;}#mermaid-svg-hfv6mWWLvOErxkPi .icon-shape,#mermaid-svg-hfv6mWWLvOErxkPi .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hfv6mWWLvOErxkPi .icon-shape p,#mermaid-svg-hfv6mWWLvOErxkPi .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hfv6mWWLvOErxkPi .icon-shape .label rect,#mermaid-svg-hfv6mWWLvOErxkPi .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hfv6mWWLvOErxkPi .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hfv6mWWLvOErxkPi .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hfv6mWWLvOErxkPi :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户点击查询
Index: 设置 isLoading=true
NetworkUtil.queryNetworkStatus
connection.getDefaultNet
connection.getNetCapabilities
connection.getConnectionPropertiesSync
封装 NetworkStatusItem 数组
更新 NetworkStatusModel
@Trace 触发 UI 刷新
2.4 状态管理 V2 设计要点
本案例使用以下 V2 装饰器构建响应式数据流:
| 装饰器 | 用途 | 对应 V1 |
|---|---|---|
@ComponentV2 |
声明 V2 组件 | @Component |
@ObservedV2 |
装饰可深度观察的数据类 | @Observed |
@Trace |
标记需要追踪的属性 | 自动追踪 |
@Local |
组件内部响应式状态 | @State |
@Param |
父到子单向数据流 | @Prop |
@Computed |
缓存派生属性 | 计算属性 |
@Local + AppStorage.get() |
生命周期中读取 AppStorage | @StorageLink |
三、核心 API 解析
3.1 @kit.NetworkKit 网络连接管理
connection 模块提供以下核心 API:
typescript
import { connection } from '@kit.NetworkKit';
| API | 说明 | 返回值 |
|---|---|---|
getDefaultNet() |
获取系统默认激活的网络句柄 | Promise<NetHandle> |
getNetCapabilities(netHandle) |
获取网络能力信息(带宽、承载类型) | Promise<NetCapabilities> |
getConnectionPropertiesSync(netHandle) |
同步获取连接属性(网卡、MTU、路由) | ConnectionProperties |
hasDefaultNet() |
检查是否有可用网络 | Promise<boolean> |
3.2 关键数据类型
NetHandle --- 网络句柄:
typescript
interface NetHandle {
netId: number; // 网络 ID,0 表示无网络,有效值 >= 100
}
NetCapabilities --- 网络能力集:
typescript
interface NetCapabilities {
linkUpBandwidthKbps: number; // 上行带宽 (kb/s)
linkDownBandwidthKbps: number; // 下行带宽 (kb/s)
bearerTypes: NetBearType[]; // 网络承载类型
networkCap: NetCap[]; // 网络具体能力
}
ConnectionProperties --- 连接属性:
typescript
interface ConnectionProperties {
interfaceName: string; // 网卡名称
mtu: number; // 最大传输单元
linkAddresses: LinkAddress[]; // 链路地址列表
routes: RouteInfo[]; // 路由信息列表
dnses: NetAddress[]; // DNS 服务器列表
}
NetBearType --- 网络承载类型枚举:
| 枚举值 | 数值 | 说明 |
|---|---|---|
BEARER_CELLULAR |
0 | 蜂窝网络 |
BEARER_WIFI |
1 | Wi-Fi 网络 |
BEARER_ETHERNET |
3 | 以太网 |
BEARER_VPN |
4 | VPN 网络 |
四、分步实现
Task 1:配置模块权限
在 module.json5 中声明网络相关权限:
json5
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.GET_NETWORK_INFO"
},
{
"name": "ohos.permission.INTERNET"
}
]
}
}
说明 :
GET_NETWORK_INFO用于获取网络信息(查询类 API 必需),INTERNET用于网络访问能力。这两个权限均为system_grant类型,无需用户授权弹窗。
Task 2:定义数据模型与常量
创建 constants/Constants.ets,使用 @ObservedV2 + @Trace 定义响应式数据模型:
typescript
import { connection } from '@kit.NetworkKit';
/**
* 网络状态项 - 展示单条网络信息
* @ObservedV2 使类实例可被深度观察
* @Trace 标记需要追踪变化的属性
*/
@ObservedV2
export class NetworkStatusItem {
@Trace title: string = ''; // 信息标题(如"网络类型")
@Trace value: string = ''; // 信息值(如"Wi-Fi")
constructor(title: string, value: string) {
this.title = title;
this.value = value;
}
}
/**
* 网络状态聚合模型
* @Computed 缓存派生属性,仅在依赖变化时重新计算
*/
@ObservedV2
export class NetworkStatusModel {
@Trace items: NetworkStatusItem[] = [];
@Trace isConnected: boolean = false;
@Trace isLoading: boolean = false;
@Trace errorMessage: string = '';
@Computed
get hasError(): boolean {
return this.errorMessage.length > 0;
}
@Computed
get itemCount(): number {
return this.items.length;
}
}
设计要点:
@ObservedV2+@Trace替代 V1 的@Observed,提供属性级别的细粒度追踪。当title或value单独变化时,只有引用该属性的 UI 组件会刷新。@Computed自动缓存hasError和itemCount的计算结果。只有errorMessage或items变化时才重新计算,避免冗余运算。- 强类型数据类 替代数组索引取值(V1 方案中用
statusOutput[0]~statusOutput[9]),消除越界风险和语义不清问题。
常量定义部分:
typescript
export class Constants {
// 网络状态标签
static readonly LABEL_BANDWIDTH: string = '上/下行带宽';
static readonly LABEL_NET_TYPE: string = '网络类型';
static readonly LABEL_INTERFACE_NAME: string = '网卡名称';
static readonly LABEL_MTU: string = '最大传输单元';
static readonly LABEL_LINK_INFO: string = '链路信息';
// ... 更多标签
// 网络类型中文映射
static readonly CELLULAR: string = '蜂窝网络';
static readonly WIFI: string = 'Wi-Fi';
static readonly ETHERNET: string = '以太网';
static readonly VPN: string = 'VPN';
// 工具方法:将枚举值转为可读文本
static getNetBearTypeName(type: connection.NetBearType): string {
switch (type) {
case connection.NetBearType.BEARER_CELLULAR:
return Constants.CELLULAR;
case connection.NetBearType.BEARER_WIFI:
return Constants.WIFI;
case connection.NetBearType.BEARER_ETHERNET:
return Constants.ETHERNET;
case connection.NetBearType.BEARER_VPN:
return Constants.VPN;
default:
return Constants.UNKNOWN;
}
}
}
Task 3:实现网络查询工具类
创建 utils/NetworkUtil.ets,封装所有网络查询逻辑:
typescript
import { connection } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { Constants, NetworkStatusItem } from '../constants/Constants';
export class NetworkUtil {
/**
* 查询当前网络状态信息
* 使用 async/await 模式替代传统回调,逻辑更线性
*/
static async queryNetworkStatus(): Promise<NetworkStatusItem[]> {
// Step 1:获取默认网络句柄
const netHandle: connection.NetHandle = await connection.getDefaultNet();
// Step 2:检查是否有网络连接(netId 为 0 表示无网络)
if (netHandle.netId === Constants.DEFAULT_NET_ID) {
return [];
}
// Step 3:获取网络能力信息
const capabilities: connection.NetCapabilities =
await connection.getNetCapabilities(netHandle);
const items: NetworkStatusItem[] = [];
// 带宽信息
const bandwidth = `${capabilities.linkUpBandwidthKbps}/` +
`${capabilities.linkDownBandwidthKbps} ${Constants.BANDWIDTH_UNIT}`;
items.push(new NetworkStatusItem(Constants.LABEL_BANDWIDTH, bandwidth));
// 网络类型
items.push(new NetworkStatusItem(Constants.LABEL_NET_TYPE,
Constants.getNetBearTypeName(capabilities.bearerTypes[0])));
// Step 4:同步获取连接属性
const properties = connection.getConnectionPropertiesSync(netHandle);
items.push(new NetworkStatusItem(Constants.LABEL_INTERFACE_NAME,
properties.interfaceName));
items.push(new NetworkStatusItem(Constants.LABEL_MTU, `${properties.mtu}`));
// 链路地址
const linkInfo = NetworkUtil.formatLinkAddresses(properties.linkAddresses);
items.push(new NetworkStatusItem(Constants.LABEL_LINK_INFO, linkInfo));
// 路由信息
NetworkUtil.addRouteInfo(items, properties.routes);
return items;
}
}
关键技术点:
- async/await 替代回调 :原始方案使用
connection.getDefaultNet().then(...)+getNetCapabilities(netHandle, callback)的嵌套回调模式,容易出现"回调地狱"。改用 async/await 后,代码逻辑线性可读。 getConnectionPropertiesSync同步调用 :由于getDefaultNet和getNetCapabilities已经是异步的,在此之后再调用同步的getConnectionPropertiesSync可以避免额外的异步层级。- 无模块级状态 :原始方案使用模块级
let statusOutput: string[]变量,存在并发安全问题。新方案每次查询返回独立的数组实例。 - 无 eventHub 依赖 :原始方案通过
context.eventHub.emit/on传递数据,需要 Context 参数。新方案直接通过 Promise 返回值传递,更简洁。
Task 4:构建主页面 UI
创建 pages/Index.ets,使用 @ComponentV2 和 @Local 管理页面状态:
typescript
@Entry
@ComponentV2
struct Index {
// 页面状态 - 使用 @Local 声明响应式变量
@Local networkStatus: NetworkStatusModel = new NetworkStatusModel();
@Local hasQueried: boolean = false;
// 安全区域高度(由 EntryAbility 写入 AppStorage,在 aboutToAppear 中读取)
@Local topRectHeight: number = 0;
@Local bottomRectHeight: number = 0;
aboutToAppear(): void {
const topHeight = AppStorage.get<number>('topRectHeight');
const bottomHeight = AppStorage.get<number>('bottomRectHeight');
if (topHeight !== undefined) {
this.topRectHeight = topHeight;
}
if (bottomHeight !== undefined) {
this.bottomRectHeight = bottomHeight;
}
}
// 查询按钮点击事件
private onQueryClick(): void {
this.hasQueried = true;
this.networkStatus.isLoading = true;
this.networkStatus.errorMessage = '';
this.networkStatus.items = [];
NetworkUtil.queryNetworkStatus()
.then((items: NetworkStatusItem[]) => {
this.networkStatus.items = items;
this.networkStatus.isConnected = items.length > 0;
this.networkStatus.isLoading = false;
})
.catch((error: BusinessError) => {
this.networkStatus.errorMessage = error.message ?? 'Unknown error';
this.networkStatus.isLoading = false;
});
}
build() {
Column() {
// 标题栏
Text($r('app.string.page_title'))
.fontSize(24)
.fontWeight(FontWeight.Bold)
// 内容区域 - 条件渲染五种状态
if (this.networkStatus.isLoading) {
// 加载中
LoadingProgress().width(48).height(48)
} else if (!this.hasQueried) {
// 未查询 - 空状态提示
SymbolGlyph($r('sys.symbol.wifi_exclamationmark'))
.fontSize(64)
} else if (this.networkStatus.hasError) {
// 查询出错
Text(this.networkStatus.errorMessage)
} else if (this.networkStatus.isConnected) {
// 已连接 - 展示信息列表
ForEach(this.networkStatus.items, (item: NetworkStatusItem) => {
StatusCard({ item: item })
})
} else {
// 未连接网络
SymbolGlyph($r('sys.symbol.wifi_slash'))
}
// 查询按钮
Button($r('app.string.btn_query'))
.onClick(() => { this.onQueryClick(); })
}
}
}
状态管理 V2 在页面中的应用:
| 装饰器 | 变量 | 作用 |
|---|---|---|
@Local |
networkStatus |
管理网络状态模型,属性变化自动刷新 UI |
@Local |
hasQueried |
控制是否已执行过查询 |
@Local |
topRectHeight |
在 aboutToAppear 中从 AppStorage 读取状态栏高度 |
@Local |
bottomRectHeight |
在 aboutToAppear 中从 AppStorage 读取导航条高度 |
Task 5:构建可复用的信息卡片子组件
使用 @ComponentV2 + @Param 创建子组件,实现父到子的单向数据流:
typescript
@ComponentV2
struct StatusCard {
@Param item: NetworkStatusItem = new NetworkStatusItem('', '');
@Param index: number = 0;
build() {
Column() {
Row() {
// 图标区域
SymbolGlyph(this.getIconSymbol())
.fontSize(18)
// 标题
Text(this.item.title)
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
Blank()
// 值
Text(this.item.value)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.text_primary'))
}
}
.padding(16)
.backgroundColor($r('app.color.card_item_bg'))
.borderRadius(12)
.margin({ bottom: 8 })
}
}
V2 父子组件数据流:
#mermaid-svg-l5Q2g6TEiyy3CqtW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-l5Q2g6TEiyy3CqtW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-l5Q2g6TEiyy3CqtW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-l5Q2g6TEiyy3CqtW .error-icon{fill:#552222;}#mermaid-svg-l5Q2g6TEiyy3CqtW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-l5Q2g6TEiyy3CqtW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-l5Q2g6TEiyy3CqtW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-l5Q2g6TEiyy3CqtW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-l5Q2g6TEiyy3CqtW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-l5Q2g6TEiyy3CqtW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-l5Q2g6TEiyy3CqtW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-l5Q2g6TEiyy3CqtW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-l5Q2g6TEiyy3CqtW .marker.cross{stroke:#333333;}#mermaid-svg-l5Q2g6TEiyy3CqtW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-l5Q2g6TEiyy3CqtW p{margin:0;}#mermaid-svg-l5Q2g6TEiyy3CqtW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-l5Q2g6TEiyy3CqtW .cluster-label text{fill:#333;}#mermaid-svg-l5Q2g6TEiyy3CqtW .cluster-label span{color:#333;}#mermaid-svg-l5Q2g6TEiyy3CqtW .cluster-label span p{background-color:transparent;}#mermaid-svg-l5Q2g6TEiyy3CqtW .label text,#mermaid-svg-l5Q2g6TEiyy3CqtW span{fill:#333;color:#333;}#mermaid-svg-l5Q2g6TEiyy3CqtW .node rect,#mermaid-svg-l5Q2g6TEiyy3CqtW .node circle,#mermaid-svg-l5Q2g6TEiyy3CqtW .node ellipse,#mermaid-svg-l5Q2g6TEiyy3CqtW .node polygon,#mermaid-svg-l5Q2g6TEiyy3CqtW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-l5Q2g6TEiyy3CqtW .rough-node .label text,#mermaid-svg-l5Q2g6TEiyy3CqtW .node .label text,#mermaid-svg-l5Q2g6TEiyy3CqtW .image-shape .label,#mermaid-svg-l5Q2g6TEiyy3CqtW .icon-shape .label{text-anchor:middle;}#mermaid-svg-l5Q2g6TEiyy3CqtW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-l5Q2g6TEiyy3CqtW .rough-node .label,#mermaid-svg-l5Q2g6TEiyy3CqtW .node .label,#mermaid-svg-l5Q2g6TEiyy3CqtW .image-shape .label,#mermaid-svg-l5Q2g6TEiyy3CqtW .icon-shape .label{text-align:center;}#mermaid-svg-l5Q2g6TEiyy3CqtW .node.clickable{cursor:pointer;}#mermaid-svg-l5Q2g6TEiyy3CqtW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-l5Q2g6TEiyy3CqtW .arrowheadPath{fill:#333333;}#mermaid-svg-l5Q2g6TEiyy3CqtW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-l5Q2g6TEiyy3CqtW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-l5Q2g6TEiyy3CqtW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-l5Q2g6TEiyy3CqtW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-l5Q2g6TEiyy3CqtW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-l5Q2g6TEiyy3CqtW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-l5Q2g6TEiyy3CqtW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-l5Q2g6TEiyy3CqtW .cluster text{fill:#333;}#mermaid-svg-l5Q2g6TEiyy3CqtW .cluster span{color:#333;}#mermaid-svg-l5Q2g6TEiyy3CqtW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-l5Q2g6TEiyy3CqtW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-l5Q2g6TEiyy3CqtW rect.text{fill:none;stroke-width:0;}#mermaid-svg-l5Q2g6TEiyy3CqtW .icon-shape,#mermaid-svg-l5Q2g6TEiyy3CqtW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-l5Q2g6TEiyy3CqtW .icon-shape p,#mermaid-svg-l5Q2g6TEiyy3CqtW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-l5Q2g6TEiyy3CqtW .icon-shape .label rect,#mermaid-svg-l5Q2g6TEiyy3CqtW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-l5Q2g6TEiyy3CqtW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-l5Q2g6TEiyy3CqtW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-l5Q2g6TEiyy3CqtW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} @Param 单向传递
@Trace 属性变化
@Trace 属性变化
Index (@Local networkStatus)
StatusCard (@Param item)
Text(item.title) 刷新
Text(item.value) 刷新
当 networkStatus.items 被重新赋值时,ForEach 会为新创建的 NetworkStatusItem 实例生成新的 StatusCard,每个卡片通过 @Param 接收数据。当单个 item 的 title 或 value 变化时,只有引用该属性的 Text 组件会刷新。
Task 6:配置 EntryAbility 与窗口管理
typescript
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.getMainWindow().then((windowClass: window.Window) => {
// 设置全屏布局
windowClass.setWindowLayoutFullScreen(true);
// 获取安全区域高度并存入 AppStorage
const navArea = windowClass.getWindowAvoidArea(
window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
AppStorage.setOrCreate('bottomRectHeight', navArea.bottomRect.height);
const sysArea = windowClass.getWindowAvoidArea(
window.AvoidAreaType.TYPE_SYSTEM);
AppStorage.setOrCreate('topRectHeight', sysArea.topRect.height);
});
windowStage.loadContent('pages/Index');
}
}
AppStorage 数据传递链:
EntryAbility.onWindowStageCreate
→ getWindowAvoidArea → 获取安全区域高度
→ AppStorage.setOrCreate('topRectHeight', ...)
→ AppStorage.setOrCreate('bottomRectHeight', ...)
Index (主页面)
→ aboutToAppear() 中 AppStorage.get('topRectHeight') 读取状态栏高度
→ aboutToAppear() 中 AppStorage.get('bottomRectHeight') 读取导航条高度
→ 应用到 Column 的 padding 实现安全区域避让
Task 7:资源文件配置
base/element/string.json(浅色模式字符串):
json
{
"string": [
{ "name": "page_title", "value": "网络状态查询" },
{ "name": "btn_query", "value": "查询网络状态" },
{ "name": "empty_title", "value": "尚未查询" },
{ "name": "empty_hint", "value": "点击下方按钮查询当前网络状态信息" },
{ "name": "no_network_hint", "value": "当前设备未连接任何网络\n请检查网络设置后重试" },
{ "name": "query_error", "value": "查询失败" },
{ "name": "loading", "value": "正在查询网络信息..." }
]
}
base/element/color.json(浅色模式颜色):
json
{
"color": [
{ "name": "page_bg", "value": "#F5F5F5" },
{ "name": "card_item_bg", "value": "#FFFFFF" },
{ "name": "text_primary", "value": "#1A1A1A" },
{ "name": "text_secondary", "value": "#666666" },
{ "name": "text_hint", "value": "#999999" },
{ "name": "icon_bg", "value": "#E8F5E9" },
{ "name": "icon_tint", "value": "#4CAF50" },
{ "name": "status_connected", "value": "#4CAF50" },
{ "name": "status_disconnected", "value": "#F44336" },
{ "name": "btn_bg", "value": "#4CAF50" },
{ "name": "btn_text", "value": "#FFFFFF" }
]
}
dark/element/color.json(深色模式颜色):
json
{
"color": [
{ "name": "page_bg", "value": "#1A1A1A" },
{ "name": "card_item_bg", "value": "#2C2C2C" },
{ "name": "text_primary", "value": "#E0E0E0" },
{ "name": "text_secondary", "value": "#999999" },
{ "name": "text_hint", "value": "#666666" },
{ "name": "icon_bg", "value": "#1B3B1E" },
{ "name": "icon_tint", "value": "#66BB6A" },
{ "name": "btn_bg", "value": "#388E3C" },
{ "name": "btn_text", "value": "#FFFFFF" }
]
}
深色模式适配原理 :HarmonyOS 通过
resources/base/和resources/dark/双资源目录实现主题自动切换。当系统切换为深色模式时,框架自动加载dark/目录下的颜色资源,无需额外代码。
五、V1 到 V2 迁移对照
本案例的原始实现使用状态管理 V1,以下是关键迁移点:
| V1 写法 | V2 写法 | 改进说明 |
|---|---|---|
@Component struct MainPage |
@ComponentV2 struct Index |
使用 V2 组件装饰器 |
@State statusOutput: string[] |
@Local networkStatus: NetworkStatusModel |
强类型模型替代字符串数组 |
@StorageLink('topRectHeight') |
@Local + aboutToAppear 中 AppStorage.get() |
V2 不支持 @StorageProp,改用生命周期读取 |
eventHub.on/emit 传递数据 |
Promise 返回值直接赋值 | 消除事件总线依赖 |
模块级 let statusOutput |
函数内 const items |
消除全局状态,线程安全 |
ForEach 通过数组索引映射 |
ForEach 遍历强类型对象数组 |
消除越界风险 |
@Observed class |
@ObservedV2 class + @Trace |
属性级细粒度追踪 |
| 无计算属性 | @Computed get hasError() |
自动缓存派生值 |
原始 V1 代码的问题分析
- 模块级状态污染 :
let statusOutput: string[] = []是模块级变量,多次查询时旧数据会残留(使用了push而非重新赋值),导致数据累积。 - 数组索引脆弱性 :
statusOutput[0]~statusOutput[9]的索引映射完全依赖固定顺序,如果路由信息有多条,索引就会错位。 - eventHub 耦合 :通过
context.eventHub传递数据需要 Context 参数穿透,增加了组件间的耦合度。 - 回调嵌套 :
getDefaultNet().then()内嵌套getNetCapabilities(handle, callback),形成回调地狱。
六、运行效果与测试
6.1 页面状态切换
应用包含五种页面状态:
| 状态 | 触发条件 | 显示内容 |
|---|---|---|
| 未查询 | 首次进入页面 | 空状态图标 + 提示文字 |
| 加载中 | 点击查询按钮后 | LoadingProgress 动画 |
| 已连接 | 查询成功且有网络 | 网络信息卡片列表 |
| 未连接 | 查询成功但无网络 | 断网图标 + 提示 |
| 错误 | 查询异常 | 错误图标 + 错误信息 |
6.2 测试步骤
- 编译运行:在 DevEco Studio 中选择真机或模拟器,点击 Run
- 初始状态:页面显示空状态图标和"点击下方按钮查询"提示
- Wi-Fi 连接测试 :确保设备已连接 Wi-Fi,点击"查询网络状态"
- 预期:显示网络类型"Wi-Fi"、带宽信息、IP 地址等
- 蜂窝网络测试 :关闭 Wi-Fi,使用蜂窝数据
- 预期:网络类型显示"蜂窝网络"
- 无网络测试 :开启飞行模式
- 预期:显示"未连接"提示
6.3 调试技巧
使用 HiLog 查看查询日志:
bash
# 在 DevEco Studio 的 Log 窗口中过滤
NetworkAwareness
关键日志输出:
Query success, found N items--- 查询成功No network connected, netId is 0--- 无网络Query failed: ...--- 查询失败
七、最佳实践总结
7.1 状态管理 V2 实践建议
- 优先使用
@ComponentV2:新代码统一使用 V2 装饰器体系,获得更好的类型安全和细粒度更新能力。 @ObservedV2+@Trace建模 :将业务数据封装为@ObservedV2类,用@Trace标记需要在 UI 中响应的属性。@Computed表达派生值 :用@Computed替代手动计算的中间变量,框架自动处理缓存和失效。@Local管理页面状态 :替代@State,语义更明确,且与 V2 组件生态一致。@Param替代@Prop:父到子数据传递使用@Param,需要回传时配合@Event。
7.2 网络编程实践建议
- 权限前置声明 :
GET_NETWORK_INFO和INTERNET必须在module.json5中声明,否则 API 调用会抛异常。 - netId 零值检查 :
getDefaultNet()返回的netHandle.netId === 0表示无网络,需要特殊处理。 - 空值保护 :
linkAddresses和routes可能为空数组,使用for...of遍历天然安全。 - 异步模式选择:优先使用 Promise/async-await 模式,避免回调嵌套。
7.3 UI 开发实践建议
@Builder拆分 UI 片段 :将不同状态的视图提取为@Builder方法,保持build()方法清晰。- 条件渲染五种状态 :使用
if/else if链处理加载中、空状态、有数据、无网络、错误等状态。 - 安全区域适配 :通过
AppStorage+@Local+aboutToAppear传递状态栏和导航条高度,避免内容被系统 UI 遮挡。注意@StorageProp不兼容@ComponentV2,需在生命周期中手动读取。 - 深色模式双目录 :
base/和dark/资源目录实现零代码主题切换。
八、常见问题 FAQ
Q1:调用 getDefaultNet() 报错 "permission denied"?
A :请检查 module.json5 中是否声明了 ohos.permission.GET_NETWORK_INFO 权限。该权限为 system_grant 类型,声明后自动授权,无需弹窗。
Q2:查询结果中带宽显示为 "0/0 kb/s"?
A:带宽值 0 表示系统无法评估当前网络带宽,这是正常现象。部分网络环境(如某些 VPN)不提供带宽评估能力。
Q3:getConnectionPropertiesSync 返回的路由信息为空?
A :routes 数组可能为空,取决于当前网络配置。代码中使用 for...of 遍历空数组是安全的,不会抛出异常。
Q4:@ComponentV2 可以使用 @Reusable 吗?
A :不可以。@Reusable 仅支持 @Component(V1 组件)。在 V2 组件中,使用 @ObservedV2 + @Trace 实现细粒度响应式更新来替代组件复用的性能优化。
Q5:如何在 V2 组件中获取 Context?
A :在生命周期回调(如 aboutToAppear)中使用 getContext(this) 获取。本案例通过消除 eventHub 依赖,避免了在工具类中传递 Context 的需要。
九、总结
本案例展示了如何使用 HarmonyOS NEXT 的状态管理 V2 体系构建一个网络状态信息查询工具。核心技术要点:
@ObservedV2+@Trace提供属性级细粒度响应式更新@Computed自动缓存派生属性,减少冗余计算- async/await 替代回调嵌套,代码更线性可读
- 强类型数据类替代数组索引取值,消除越界风险
base/+dark/双资源目录实现深色模式零代码适配
相比 V1 版本,V2 方案在类型安全 、代码可维护性 、UI 刷新性能三个维度均有显著提升,推荐在新项目中优先采用。
参考文档:
本案例展示了如何使用 HarmonyOS NEXT 的状态管理 V2 体系构建一个网络状态信息查询工具。核心技术要点:
@ObservedV2+@Trace提供属性级细粒度响应式更新@Computed自动缓存派生属性,减少冗余计算- async/await 替代回调嵌套,代码更线性可读
- 强类型数据类替代数组索引取值,消除越界风险
base/+dark/双资源目录实现深色模式零代码适配
相比 V1 版本,V2 方案在类型安全 、代码可维护性 、UI 刷新性能三个维度均有显著提升,推荐在新项目中优先采用。
参考文档: