一、章节概览

1. 学习目标

完成本章学习后,你应当能够:

  1. 解释端云一体化开发的基本思想和价值。
  2. 在 AGC 平台创建项目并规划认证服务、云数据库等资源。
  3. 使用 DevEco Studio 云开发模板创建与云端资源关联的工程。
  4. 理解云端对象类型、字段类型、主键和数据约束。
  5. 在云数据库控制台设计农业设备数据表。
  6. 根据云端对象类型生成或维护端侧数据模型类。
  7. 使用 ArkUI 声明式组件构建设备数据卡片。
  8. 使用状态变量实现 UI 与云端数据的动态绑定。
  9. 编写端侧 Cloud DB 查询,完成条件过滤和排序。
  10. 监听云端数据变化,实现查询结果的同步和刷新。
  11. 处理认证失败、查询失败、权限不足和数据格式异常。
  12. 通过实践验证端云设计方案的可行性和有效性。
  13. 形成从需求、建模、实现、联调到验证的系统化开发思维。
  14. 在云数据安全、权限配置、隐私保护和工程协作方面承担责任。

2. 知识结构

端云一体化开发(一)

├── 端云一体化基础
│   ├── 端侧与云侧
│   ├── 数据流
│   ├── 认证与权限
│   └── 开发流程
├── AGC 项目初始化
│   ├── 创建项目
│   ├── 认证服务
│   ├── Cloud DB
│   └── 应用关联
├── 端云工程
│   ├── DevEco Studio 云开发模板
│   ├── 工程配置
│   ├── SDK 初始化
│   └── 环境区分
├── 数据建模
│   ├── 对象类型
│   ├── 字段与类型
│   ├── 主键和约束
│   └── 端侧模型生成
├── ArkUI 卡片
│   ├── 组件化
│   ├── 声明式布局
│   ├── @State 绑定
│   └── 加载与错误状态
├── Cloud DB
│   ├── 查询
│   ├── 过滤
│   ├── 排序
│   ├── 分页
│   └── 订阅更新
└── 工程实践
    ├── 联调
    ├── 测试
    ├── 安全
    └── 复盘

二、端云一体化开发基础

2.1 什么是端云一体化

端云一体化是把端侧应用与云端资源作为一个整体进行设计和开发:

端侧(手机、平板、元服务卡片)
        │
        │ API / SDK / 数据订阅
        ▼
云侧(认证、数据库、存储、函数和管理控制台)

端侧负责:
• 展示界面;
• 接收用户输入;
• 发起查询和操作;
• 管理页面状态;
• 提供卡片和交互入口。
云侧负责:
• 用户认证;
• 数据存储;
• 数据访问控制;
• 多端共享;
• 数据备份和管理;
• 向端侧提供可访问的服务能力。

2.2 为什么要采用端云一体化

传统单机应用可以把数据保存在本地,但在多设备、多用户和实时协作场景中会遇到:
• 数据无法跨设备共享;
• 更换设备后数据丢失;
• 多人协作难以同步;
• 需要自行搭建后端接口;
• 权限和数据安全难以统一管理。
端云一体化可以帮助开发者:
• 快速创建云端资源;
• 使用统一数据模型;
• 减少重复编写基础后端代码;
• 实现多端数据共享;
• 让 UI 与云端数据保持同步;
• 集中管理认证和访问权限。

2.3 端云数据流

农业设备列表的典型数据流:

用户打开元服务卡片
      ↓
端侧初始化 Cloud DB SDK
      ↓
检查认证状态
      ↓
查询云端设备对象
      ↓
过滤、排序和转换数据
      ↓
更新 @State
      ↓
卡片显示最新设备信息
      ↓
云端数据发生变化
      ↓
订阅事件触发
      ↓
重新查询或局部更新 UI

2.4 端云一体化不等于“所有逻辑都放云端”

应合理划分职责:
在这里插入图片描述
端侧的按钮隐藏不能代替云端权限控制。任何重要数据和高风险操作都必须在云侧再次验证。

三、AGC 项目环境初始化

3.1 AGC 项目平台的作用

AGC(AppGallery Connect)平台通常用于管理应用和云端服务资源。课程要求在平台完成:
• 创建项目;
• 关联应用;
• 配置认证服务;
• 创建或配置云数据库;
• 管理应用标识和服务开关;
• 查看云端数据;
• 配置访问规则;
• 获取开发所需的项目配置。
平台界面会随版本调整,但核心思路保持不变:先建立云端项目和资源,再让端侧工程与其关联。

3.2 创建 AGC 项目

通用步骤:

  1. 登录 AGC 平台。
  2. 创建一个新的项目。
  3. 填写项目名称。
  4. 添加与 HarmonyOS 应用对应的应用条目。
  5. 确认包名、应用标识和项目归属。
  6. 开启课程需要的认证服务和 Cloud DB。
  7. 记录项目环境信息。
  8. 下载或生成端侧工程需要的配置。
  9. 在 DevEco Studio 工程中完成关联。
  10. 使用最小查询验证连接是否成功。
    项目命名建议
    AgriCloudDemo
    GreenhouseCloudLab
    EquipmentCloudService
    建议:
    • 名称具有业务含义;
    • 不使用临时测试词作为正式项目名称;
    • 开发、测试和生产项目分开;
    • 团队统一命名规则;
    • 不把密钥或敏感信息写入项目名称和代码。

3.3 认证服务

认证服务用于确认“当前用户是谁”,并为数据访问规则提供身份依据。
常见认证流程:

用户打开应用
  ↓
检查是否已认证
  ├── 已认证 → 访问 Cloud DB
  └── 未认证 → 登录/匿名认证/提示用户

认证信息可以影响:
• 当前用户能查看哪些设备;
• 当前用户能否修改设备;
• 用户创建的数据归属;
• 设备和组织之间的关系;
• 访问日志和审计记录。
认证设计原则
• 认证状态要明确;
• 登录过期要能恢复;
• 未认证时不要直接展示敏感数据;
• 不在日志中输出令牌;
• 不把用户身份只交给端侧传参决定;
• 云端规则应根据可靠身份信息判断权限。

3.4 云数据库服务

Cloud DB 用于保存结构化业务数据。农业场景可以存储:
• 大棚信息;
• 设备信息;
• 传感器数据;
• 告警记录;
• 设备操作日志;
• 用户与设备的关联关系。
初始化时要规划:

项目
 ├── 认证
 ├── 数据库
 │    ├── greenhouse
 │    ├── equipment
 │    └── alarm
 └── 应用关联和权限规则

3.5 环境区分

建议区分:
开发环境:学生个人测试数据
测试环境:团队联调和验收
生产环境:真实业务和真实用户
不要让课堂测试代码直接访问生产数据库。环境配置应明确:
• 项目 ID;
• 应用标识;
• 数据库资源;
• 认证方式;
• 日志级别;
• 数据权限;
• 发布渠道。

四、端云一体化工程创建

4.1 使用云开发模板

在 DevEco Studio 中,可以使用与云开发相关的工程模板创建项目。模板的价值在于:
• 生成基本项目结构;
• 配置端云关联所需文件;
• 提供 Cloud DB 初始化示例;
• 创建基础页面和模型;
• 减少手动配置错误。
通用步骤:

  1. 打开 DevEco Studio。
  2. 选择创建新工程。
  3. 选择云开发或端云一体化相关模板。
  4. 填写项目名称、包名和保存路径。
  5. 选择 SDK 和设备类型。
  6. 选择或关联 AGC 项目。
  7. 确认云端资源配置。
  8. 等待工程同步和依赖解析。
  9. 检查生成的配置文件和入口页面。
  10. 执行一次最小编译和运行。

4.2 工程中的重要部分

项目根目录

├── AppScope/
├── entry/
│   └── src/main/
│       ├── ets/
│       │   ├── pages/
│       │   ├── components/
│       │   ├── model/
│       │   └── service/
│       ├── resources/
│       └── module.json5
├── 云端关联配置
├── 构建配置
└── 依赖配置

建议按职责组织端侧代码:

ets/
├── pages/          # 页面
├── components/     # 可复用 UI
├── model/          # 端侧对象类型
├── service/        # Cloud DB 和认证服务
├── mapper/         # 云端数据到 UI 模型的转换
└── utils/          # 格式化和校验工具

4.3 初始化 Cloud DB SDK

概念流程:

async function initializeCloud(): Promise<void> {
  // 1. 初始化应用上下文
  // 2. 初始化认证服务
  // 3. 初始化 Cloud DB
  // 4. 检查登录或认证状态
}

实际初始化 API 和配置方式需遵循当前 SDK 版本。初始化失败时应明确区分:
• 配置文件缺失;
• 项目关联错误;
• SDK 未安装;
• 网络不可用;
• 认证失败;
• 权限不足。

4.4 初始化验收

• [ ] 云开发模板工程可以打开。
• [ ] AGC 项目与应用包名匹配。
• [ ] 认证服务配置正确。
• [ ] Cloud DB 服务已开启。
• [ ] 端侧依赖可解析。
• [ ] SDK 初始化没有异常日志。
• [ ] 可以执行一个最小查询或连接检查。
• [ ] 开发环境与生产环境没有混淆。

五、云端对象类型设计

5.1 什么是对象类型

对象类型是对云端数据结构的描述。它决定:
• 对象有哪些字段;
• 每个字段是什么类型;
• 哪些字段必填;
• 哪些字段可以为空;
• 哪些字段作为主键;
• 数据能否被查询、排序和订阅。
一个农业设备对象可以表示为:

interface Equipment {
  id: string;
  name: string;
  category: string;
  status: string;
  greenhouseId: string;
  battery: number;
  updatedAt: string;
}

云端控制台中的对象类型应与端侧模型保持一致。

5.2 字段设计原则

字段名称清晰

推荐:

greenhouseId
updatedAt
batteryPercent

不推荐:

a
temp1
data2

类型稳定

如果 battery 表示百分比,应统一使用数字,不要有时返回数字、有时返回字符串:

正确:battery = 82
不一致:battery = "82"

时间统一

统一选择:
• 时间戳;
• ISO 8601 字符串;
• 云数据库支持的日期类型。
不要在不同表中混用多种无法转换的时间格式。

状态受约束

online
offline
maintenance

状态值应由统一类型或枚举约束,避免出现:

onlie
Online
在线中

5.3 大棚对象类型

interface Greenhouse {
  id: string;
  name: string;
  location: string;
  area: number;
  status: 'normal' | 'warning' | 'offline';
  updatedAt: string;
}

5.4 设备与大棚的关系

设备通常属于某个大棚:

Greenhouse 1 ──── * Equipment

设备表中可以保存:

greenhouseId

查询某个大棚的设备:

WHERE greenhouseId = 'GH-001'

如果还需要设备历史数据,可以拆分:

equipment
equipment_reading
equipment_alarm

不要把所有历史数据不断追加到设备对象的一个大字段中,这会降低查询和维护效率。

5.5 主键与索引

主键
主键应:
• 唯一;
• 稳定;
• 不随名称变化;
• 便于路由和查询;
• 能在多端共享。
索引
适合建立索引的字段可能包括:
• greenhouseId;
• status;
• updatedAt;
• category。
索引不是越多越好。应根据真实查询场景规划,避免增加无用写入成本。

5.6 数据约束

示例约束:

id:必填、唯一
name:必填、长度限制
battery:0~100 的整数
status:限定为三种状态
greenhouseId:必须关联有效大棚
updatedAt:必须是合法时间

客户端可以做第一层校验,云端还必须做最终校验。

六、端侧对象模型生成

6.1 为什么需要端侧模型

如果端侧直接操作原始对象,会出现:
• 字段名称拼写错误;
• 云端字段更新后端侧未同步;
• 状态值不统一;
• 查询结果无法获得编辑器提示;
• UI 与数据结构耦合过深。
端侧模型可以提供:
• 类型检查;
• 自动补全;
• 统一字段;
• 数据转换入口;
• 与云端对象类型的对应关系。

6.2 自动生成与手工维护

理想流程:

云端对象类型
      ↓
模型生成工具
      ↓
端侧数据模型类
      ↓
页面和服务层使用

如果当前工具支持自动生成,应在云端模型变更后重新生成并检查差异。若必须手工维护,应建立字段变更记录。

6.3 端侧模型示例

export type EquipmentStatus =
  | 'online'
  | 'offline'
  | 'maintenance';

export class EquipmentModel {
  id: string = '';
  name: string = '';
  category: string = '';
  status: EquipmentStatus = 'offline';
  greenhouseId: string = '';
  battery: number = 0;
  updatedAt: string = '';
}

实际生成模型的构造函数、装饰器和字段定义需要以当前 Cloud DB SDK 模板为准。

6.4 云端模型与 UI 模型分离

云端模型:

interface EquipmentRecord {
  id: string;
  name: string;
  status: EquipmentStatus;
  updatedAt: string;
}

UI 模型:

interface EquipmentCardModel {
  title: string;
  statusText: string;
  statusColor: string;
  updatedAtText: string;
}

转换函数:

function toCardModel(
  record: EquipmentRecord
): EquipmentCardModel {
  return {
    title: record.name,
    statusText: getStatusText(record.status),
    statusColor: getStatusColor(record.status),
    updatedAtText: formatTime(record.updatedAt)
  };
}

分离的好处:
• 数据层和界面层解耦;
• UI 不需要知道数据库字段细节;
• 更容易适配不同卡片尺寸;
• 后端字段变化影响范围更小。

6.5 模型同步检查

云端字段变更后检查:

云端对象类型
  ↓
端侧模型
  ↓
查询字段
  ↓
转换函数
  ↓
UI 绑定
  ↓
测试数据和卡片配置

不要只修改云端表而忘记端侧代码。

七、ArkUI 声明式卡片开发

7.1 页面与卡片结构

端侧卡片可以拆成:

GreenhouseCardPage
├── CardHeader
├── EnvironmentMetrics
├── DeviceSummary
├── AlarmSummary
└── CardFooter

每个组件只负责一类 UI 任务。

7.2 卡片示例

@Component
struct GreenhouseDataCard {
  @Prop name: string;
  @Prop temperature: number;
  @Prop humidity: number;
  @Prop alarmCount: number;

  build() {
    Column({ space: 8 }) {
      Text(this.name)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)

      Row({ space: 16 }) {
        Column() {
          Text(`${this.temperature.toFixed(1)}℃`)
            .fontSize(22)
          Text('温度')
            .fontSize(12)
        }

        Column() {
          Text(`${this.humidity.toFixed(0)}%`)
            .fontSize(22)
          Text('湿度')
            .fontSize(12)
        }
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceAround)

      Text(this.alarmCount > 0
        ? `告警:${this.alarmCount} 条`
        : '当前无告警')
        .fontSize(12)
    }
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
  }
}

7.3 数据绑定

@Component
struct GreenhousePage {
  @State data: GreenhouseData | null = null;

  build() {
    Column() {
      if (this.data === null) {
        Text('暂无大棚数据')
      } else {
        GreenhouseDataCard({
          name: this.data.name,
          temperature: this.data.temperature,
          humidity: this.data.humidity,
          alarmCount: this.data.alarmCount
        })
      }
    }
  }
}

当执行:

this.data = latestData;

绑定 data 的界面会重新构建。

7.4 加载和错误状态

type LoadState<T> =
  | { kind: 'loading' }
  | { kind: 'success'; data: T }
  | { kind: 'empty' }
  | { kind: 'error'; message: string };

使用状态驱动:

@State state: LoadState<GreenhouseData> = {
  kind: 'loading'
};

这样可以清晰区分:
• 正在加载;
• 获取成功;
• 成功但无数据;
• 请求失败。
不要用一个空对象同时表示“未加载”“加载失败”和“真实数据全为零”。

八、云数据库查询操作

8.1 查询流程

确定对象类型
  ↓
创建查询对象
  ↓
添加过滤条件
  ↓
设置排序
  ↓
设置分页
  ↓
执行查询
  ↓
转换结果
  ↓
更新 @State

8.2 查询全部设备

概念示例:

async function queryAllEquipments(): Promise<Equipment[]> {
  const query = createEquipmentQuery();

  const records = await query.execute();

  return records.map(toEquipmentModel);
}

实际查询对象、执行函数和返回类型需按照当前 Cloud DB SDK 的 API 编写。

8.3 条件过滤

查询某个大棚的在线设备:

async function queryOnlineEquipments(
  greenhouseId: string
): Promise<Equipment[]> {
  const query = createEquipmentQuery()
    .where('greenhouseId', '==', greenhouseId)
    .where('status', '==', 'online');

  const records = await query.execute();

  return records.map(toEquipmentModel);
}

注意:
• 过滤字段名称要与云端模型一致;
• 参数应进行类型校验;
• 多条件组合要确认逻辑是 AND 还是 OR;
• 过滤条件应尽量在云端执行,减少端侧数据量。

8.4 排序

按更新时间倒序:

const query = createEquipmentQuery()
  .orderBy('updatedAt', 'desc');

按电量从低到高:

const query = createEquipmentQuery()
  .orderBy('battery', 'asc');

排序前要确认字段类型统一,否则字符串排序和数字排序可能产生不同结果。

8.5 分页

interface PageResult<T> {
  records: T[];
  page: number;
  pageSize: number;
  total?: number;
  hasMore: boolean;
}

分页的价值:
• 降低首次请求数据量;
• 减少页面渲染压力;
• 改善大棚设备数量较多时的体验;
• 控制网络和内存消耗。

8.6 查询错误处理

async function safeQuery(): Promise<Equipment[]> {
  try {
    return await queryAllEquipments();
  } catch (error: unknown) {
    if (error instanceof Error) {
      console.error(`查询失败:${error.message}`);
    }

    throw new Error('设备数据暂时无法获取');
  }
}

应区分:
• 认证失败;
• 权限不足;
• 网络异常;
• 查询条件错误;
• 云服务异常;
• 数据格式错误。
不要把所有错误都显示成“暂无数据”。

九、数据订阅与更新

9.1 为什么需要订阅

如果多个用户或设备都能修改云端数据,仅在页面首次进入时查询一次是不够的:

云端状态已改变
  ↓
页面仍显示旧状态
  ↓
用户做出错误判断
数据订阅可以在云端对象变化时通知端侧:
云端数据变化
  ↓
订阅回调触发
  ↓
接收变化事件
  ↓
重新查询或更新本地状态
  ↓
UI 自动刷新

9.2 订阅方式

具体 API 取决于当前 Cloud DB SDK,常见思路包括:
• 订阅查询结果;
• 监听对象变化;
• 接收新增、修改和删除事件;
• 通过回调更新页面;
• 使用事件后重新查询。
概念示例:

private unsubscribe?: () => void;

onPageShow(): void {
  this.unsubscribe = subscribeEquipmentChanges(
    (change: EquipmentChange) => {
      this.handleChange(change);
    }
  );
}

onPageHide(): void {
  this.unsubscribe?.();
  this.unsubscribe = undefined;
}

9.3 重新查询与局部更新

重新查询

收到变化事件
  ↓
重新查询当前条件下的数据
  ↓
整体更新 @State

优点:逻辑简单,一致性较好。
缺点:可能增加请求量。
局部更新

收到修改事件
  ↓
找到对应设备
  ↓
只更新该项

优点:响应快、数据量小。
缺点:需要正确处理新增、修改、删除、排序和筛选。
初学阶段可先实现重新查询,再根据性能需求进行局部优化。

9.4 订阅生命周期

页面显示 → 建立订阅
页面隐藏 → 取消订阅
页面重新显示 → 恢复订阅
页面销毁 → 确保全部释放
如果忘记取消订阅,可能出现:
• 重复回调;
• 页面离开后仍更新状态;
• 内存泄漏;
• 同一个事件触发多次查询;
• 卡片和页面数据互相覆盖。

9.5 事件顺序和重复事件

订阅事件可能存在:
• 重复投递;
• 网络恢复后的批量事件;
• 事件乱序;
• 事件与手动刷新同时到达。
建议:
• 使用事件 ID 或版本号去重;
• 关键修改后重新查询;
• 记录数据更新时间;
• 不盲目相信事件到达顺序;
• 对删除对象进行存在性检查。

十、综合案例:云端大棚设备卡片

10.1 需求说明

实现一个端云一体化大棚卡片:
• 云端保存大棚设备数据;
• 端侧查询指定大棚;
• 卡片显示温度、湿度、在线设备数和告警数;
• 页面打开时加载数据;
• 数据变化时自动更新;
• 卡片点击进入完整设备页面;
• 请求失败时显示缓存或重试;
• 页面隐藏时取消订阅;
• 完成至少一次端云联调。

10.2 云端对象类型

interface GreenhouseReading {
  id: string;
  greenhouseId: string;
  temperature: number;
  humidity: number;
  illumination: number;
  alarmCount: number;
  recordedAt: string;
}

interface Equipment {
  id: string;
  greenhouseId: string;
  name: string;
  status: 'online' | 'offline' | 'maintenance';
  updatedAt: string;
}

10.3 统计模型

interface GreenhouseSummary {
  greenhouseId: string;
  greenhouseName: string;
  temperature: number;
  humidity: number;
  illumination: number;
  onlineCount: number;
  alarmCount: number;
  updatedAt: string;
}

10.4 数据服务层

class GreenhouseService {
  async getSummary(
    greenhouseId: string
  ): Promise<GreenhouseSummary> {
    const reading = await this.queryLatestReading(
      greenhouseId
    );

    const equipments = await this.queryEquipments(
      greenhouseId
    );

    return {
      greenhouseId,
      greenhouseName: '大棚一号',
      temperature: reading.temperature,
      humidity: reading.humidity,
      illumination: reading.illumination,
      onlineCount: equipments.filter(
        item => item.status === 'online'
      ).length,
      alarmCount: reading.alarmCount,
      updatedAt: reading.recordedAt
    };
  }

  private async queryLatestReading(
    greenhouseId: string
  ): Promise<GreenhouseReading> {
    // 按 recordedAt 倒序查询最新一条
    throw new Error('示例方法');
  }

  private async queryEquipments(
    greenhouseId: string
  ): Promise<Equipment[]> {
    // 按 greenhouseId 查询设备
    throw new Error('示例方法');
  }
}

服务层的价值:
• 页面不直接拼接查询细节;
• 卡片和完整页面可以复用;
• 查询、转换和错误处理集中管理;
• 便于单元测试和后续替换数据源。

10.5 页面状态管理

type SummaryState =
  | { kind: 'loading'; previous?: GreenhouseSummary }
  | { kind: 'success'; data: GreenhouseSummary }
  | { kind: 'error'; message: string; previous?: GreenhouseSummary };

@Component
struct GreenhouseCloudPage {
  @State state: SummaryState = {
    kind: 'loading'
  };

  private readonly service = new GreenhouseService();
  private greenhouseId: string = 'GH-001';
  private unsubscribe?: () => void;

  aboutToAppear(): void {
    void this.loadSummary();
  }

  onPageShow(): void {
    this.subscribeChanges();
  }

  onPageHide(): void {
    this.unsubscribe?.();
    this.unsubscribe = undefined;
  }

  private async loadSummary(): Promise<void> {
    const previous =
      this.state.kind === 'success'
        ? this.state.data
        : undefined;

    this.state = { kind: 'loading', previous };

    try {
      const data = await this.service.getSummary(
        this.greenhouseId
      );

      this.state = { kind: 'success', data };
    } catch (error: unknown) {
      this.state = {
        kind: 'error',
        message: '大棚数据获取失败,请稍后重试',
        previous
      };
    }
  }

  private subscribeChanges(): void {
    this.unsubscribe = subscribeGreenhouseChanges(
      this.greenhouseId,
      () => {
        void this.loadSummary();
      }
    );
  }

  build() {
    if (this.state.kind === 'loading') {
      Text('正在获取大棚数据……')
    } else if (this.state.kind === 'error') {
      Column({ space: 8 }) {
        if (this.state.previous !== undefined) {
          Text(`数据可能已过期:${this.state.previous.updatedAt}`)
        }

        Text(this.state.message)

        Button('重试')
          .onClick(() => {
            void this.loadSummary();
          })
      }
    } else {
      GreenhouseSummaryCard({
        data: this.state.data
      })
    }
  }
}

说明:示例中的订阅函数、Cloud DB 查询和路由函数是概念接口,实际代码必须根据当前 HarmonyOS/Cloud DB SDK 版本调整。

10.6 端云联调步骤

  1. 在 AGC 创建项目
  2. 开启认证和 Cloud DB
  3. 创建设备/大棚对象类型
  4. 配置字段、权限和测试数据
  5. 创建端云工程
  6. 初始化 Cloud DB SDK
  7. 端侧生成或维护对象模型
  8. 执行最小查询
  9. 将结果绑定到 @State
  10. 在卡片中显示
  11. 修改云端数据
  12. 验证订阅或重新查询
  13. 验证错误、权限和断网场景
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐