鸿蒙端 SDK 创建、单元测试、发布与依赖完整指南

鸿蒙端 SDK 创建、单元测试、发布与依赖完整指南

本文档介绍从零创建鸿蒙(OpenHarmony/HarmonyOS)SDK、编写单元测试、发布到官方三方库中心仓,并在项目中依赖使用的完整流程。涉及 C 层(Native)开发时,提供基于源码(BUILD.gn)和应用层(CMake)两种方式的完整实现示例。


一、SDK 创建

1.1 项目结构

鸿蒙 SDK 通常以 HAR(Harmony Archive) 形式发布。根据是否包含 C/C++ 原生代码、是否包含页面,结构有所不同。

1.1.1 纯 ArkTS 结构(无 C 层、无页面)
bash 复制代码
my_sdk/
├── oh-package.json5          # 包配置(必填)
├── build-profile.json5       # 构建配置
├── src/main/
│   ├── module.json5          # 模块配置
│   └── ets/
│       ├── index.ets         # 入口,导出对外 API
│       ├── utils/             # 工具函数
│       └── ...
├── README.md
├── CHANGELOG.md
└── LICENSE
1.1.2 含页面 + C/C++ 的完整结构
bash 复制代码
my_sdk/
├── oh-package.json5
├── build-profile.json5
├── src/main/
│   ├── module.json5
│   ├── ets/                   # ArkTS 源码
│   │   ├── index.ets         # 入口:import 页面 + export API
│   │   ├── pages/             # 页面(@Entry 路由页)
│   │   │   ├── MainPage.ets   # 主页面
│   │   │   ├── DetailPage.ets
│   │   │   ├── components/    # 页面内可复用组件
│   │   │   │   ├── Toolbar.ets
│   │   │   │   └── BottomPanel.ets
│   │   │   └── utils/         # 页面相关工具
│   │   │       ├── OptionsParser.ets
│   │   │       └── BoundsUtils.ets
│   │   ├── ui/                # 通用 UI(Canvas 绘制、自定义 View)
│   │   │   └── OverlayPainter.ets
│   │   ├── crop/              # 业务核心(或 domain/)
│   │   │   ├── CropOptions.ets
│   │   │   ├── CropTask.ets
│   │   │   └── ResultHandler.ets
│   │   └── utils/             # 通用工具
│   │       └── HttpUtils.ets
│   └── cpp/                   # C/C++ 原生代码(可选)
│       ├── CMakeLists.txt
│       ├── napi_init.cpp
│       └── types/             # NAPI 类型声明
│           └── libentry/
│               ├── oh-package.json5   # name: "libentry.so"
│               └── index.d.ts
├── ohosTest/                  # 单元测试
│   └── ets/test/
├── README.md
├── CHANGELOG.md
└── LICENSE
1.1.3 目录职责说明
目录 职责 示例
pages/ @Entry 的路由页面,负责页面编排与生命周期 CropEntryPageMainPage
pages/components/ 页面内可复用 UI 组件(@Component CropToolbarCropBottomPanel
pages/utils/ 页面相关解析、计算、辅助逻辑 CropOptionsParserCropBoundsUtils
ui/ 通用 UI 绘制、Canvas、自定义 View CropOverlayRulerWidget
crop/domain/ 业务核心、数据模型、任务执行 CropOptionsImageCropTask
utils/ 通用工具(网络、文件、格式等) HttpDownloadUtils
cpp/ C/C++ 原生实现,通过 NAPI 暴露给 ArkTS napi_init.cpp
1.1.4 页面相关调整

新增页面

  1. pages/ 下新建 XxxPage.ets,使用 @Entry({ routeName: 'XxxPage' }) 装饰:

    typescript 复制代码
    @Entry({ routeName: 'XxxPage' })
    @Component
    struct XxxPage {
      build() {
        Column() { /* ... */ }
      }
    }
  2. index.ets 中增加 import './pages/XxxPage'(若 index.ets 在 src/main/ets/ 下)或 import './src/main/ets/pages/XxxPage'(若 index.ets 在包根),使路由能注册该页面。

  3. 调用方通过 router.pushNamedRoute({ name: 'XxxPage' })router.pushUrl() 跳转。

修改页面布局

  • 页面根节点一般为 ColumnStackRow,按需调整子组件顺序和 layoutWeight
  • 使用 @State 控制 UI 状态,@Prop 在父子组件间传参。
  • 链式写法注意 ArkTS 的 ASI:} 后的 .width() 等需与 } 同一行或确保不被解析为独立语句。

调整页面层级

  • 将可复用部分抽到 pages/components/@Component export struct Xxx,在页面中 Xxx({ ... }) 使用。
  • 将通用绘制逻辑抽到 ui/,如 CropOverlayPainter 负责 Canvas 绘制。

路由与参数传递

  • 使用 AppStorage.setOrCreate('key', value) 存数据,页面通过 @StorageLink('key') 读取。
  • 或使用 router.pushUrl({ url: 'pages/XxxPage', params: { id: 1 } }),在目标页 router.getParams() 获取。url 格式需与 module.json5 中配置的 routes 一致。
1.1.5 C/C++ 结构说明

当 SDK 包含 Native 时,需在 src/main/ 下增加 cpp/

bash 复制代码
cpp/
├── CMakeLists.txt       # 构建配置
├── napi_init.cpp        # NAPI 注册与实现
└── types/               # 供 ArkTS 调用的类型声明
    └── libentry/        # 目录名随意,oh-package.json5 中 name 填 "libentry.so"
        ├── oh-package.json5   # name: "libentry.so", types: "./index.d.ts"
        └── index.d.ts         # 声明 C 层暴露的接口

主模块 oh-package.json5dependencies 中需添加:

json5 复制代码
"libentry.so": "file:./src/main/cpp/types/libentry"

build-profile.json5 中配置 externalNativeOptions 指向 CMakeLists.txt。详见第七章「C 层开发」。

1.2 oh-package.json5 配置

json5 复制代码
{
  "name": "my_sdk",                    // 包名,发布后用于依赖
  "version": "1.0.0",                 // 语义化版本
  "description": "SDK 功能描述",
  "main": "src/main/ets/index.ets",    // 入口文件
  "author": "your_name",
  "license": "Apache-2.0",
  "repository": "https://gitee.com/xxx/my_sdk",
  "dependencies": {}                    // 依赖的其他 ohpm 包
}

必填项nameversionmainlicense

1.3 module.json5 配置

json5 复制代码
{
  "module": {
    "name": "my_sdk",
    "type": "har",
    "deviceTypes": ["default", "tablet"]
  }
}
  • type: "har" 表示构建为 HAR 静态共享包
  • deviceTypes 指定支持的设备类型

1.4 构建 HAR

在项目根目录执行:

bash 复制代码
# 安装依赖
ohpm install

# 构建 HAR
hvigorw assembleHar

构建产物位于 build/default/outputs/default/ 目录,生成 .har 文件。


二、单元测试

2.1 测试类型

类型 目录 运行环境 适用场景
Local Test test/ 本地 JVM 纯逻辑、工具函数、不依赖设备
Instrument Test ohosTest/ 设备/模拟器 需系统 API、UI、生命周期

2.2 创建测试目录

在 DevEco Studio 中:

  1. 右键项目 → New → Directory → 输入 ohosTest(Instrument Test)或 test(Local Test)
  2. ohosTesttest 下创建 ets/test/ 子目录

或手动创建:

bash 复制代码
ohosTest/          # Instrument Test(设备/模拟器)
└── ets/
    └── test/
        └── MySdkTest.ets

test/              # Local Test(本地 JVM,可选)
└── ets/
    └── test/
        └── MyUtilTest.ets

Instrument Test 更常用,HAR 包通常使用 ohosTest

2.3 使用 Hypium 框架

oh-package.json5devDependencies 中添加:

json5 复制代码
{
  "devDependencies": {
    "@ohos/hypium": "1.0.x"
  }
}

2.4 编写测试用例

typescript 复制代码
// ohosTest/ets/test/MySdkTest.ets
import { describe, it, expect } from '@ohos/hypium';
import { MyUtil } from '../../src/main/ets/MyUtil';  // 路径以实际目录层级为准

export default function test() {
  describe('MyUtil 测试', function () {
    it('add 应返回两数之和', 0, async () => {
      const result = MyUtil.add(1, 2);
      expect(result).assertEqual(3);
    });

    it('formatPath 应正确处理路径', 0, async () => {
      const path = MyUtil.formatPath('/a/b/c');
      expect(path).assertContain('a');
    });
  });
}

2.5 运行测试

  • DevEco Studio:右键测试文件 → Run 'MySdkTest'
  • 命令行
bash 复制代码
hvigorw test

2.6 常用断言

断言 说明
expect(x).assertEqual(y) 相等
expect(x).assertTrue() 为 true
expect(x).assertContain(y) 包含
expect(x).assertNotNull() 非空
expect(x).assertDeepEquals(y) 深度相等

三、发布到官方库(OpenHarmony 三方库中心仓)

3.1 注册与准备

  1. 打开 OpenHarmony 三方库中心仓
  2. 使用 Gitee 账号登录并完成实名认证
  3. 进入「个人中心」完成发布者信息

3.2 生成密钥

bash 复制代码
# 创建目录
mkdir -p ~/.ssh_ohpm

# 生成 RSA 密钥对
ssh-keygen -m PEM -t RSA -b 4096 -f ~/.ssh_ohpm/mykey -N ""

~/.ssh_ohpm/mykey.pub 公钥内容上传到中心仓个人中心的「公钥管理」。

3.3 配置 ohpm

bash 复制代码
# 设置私钥路径
ohpm config set key_path ~/.ssh_ohpm/mykey

# 设置发布 ID(个人中心获取)
ohpm config set publish_id <your_publish_id>

# 设置发布仓库地址
ohpm config set publish_registry https://ohpm.openharmony.cn/ohpm

3.4 准备发布文件

确保项目根目录包含:

文件 说明
oh-package.json5 包配置
README.md 使用说明、API 介绍、示例
CHANGELOG.md 版本变更记录
LICENSE 开源协议(如 Apache-2.0)

3.5 发布命令

bash 复制代码
# 进入 HAR 所在目录(或包含 oh-package.json5 的目录)
cd /path/to/my_sdk

# 发布
ohpm publish .

3.6 常见问题

问题 处理
Missing file "oh-package.json5" 确保在包含 oh-package.json5 的包根目录执行 ohpm publish .
签名失败 检查 key_path、公钥是否已上传
版本冲突 修改 oh-package.json5 中的 version 后重新发布
Node 版本 建议使用 Node 18+,执行 node -v 检查

四、在项目中依赖使用

4.1 配置依赖

在项目根目录的 oh-package.json5dependencies 中声明:

json5 复制代码
{
  "dependencies": {
    "my_sdk": "1.0.0"
  }
}

依赖版本号支持语义化范围,如 "1.0.x""^1.0.0"

4.2 安装依赖

bash 复制代码
ohpm install

4.3 导入使用

typescript 复制代码
// 按包名导入
import { MyUtil, MyClass } from 'my_sdk';

// 使用
const result = MyUtil.doSomething();
const obj = new MyClass();

4.4 依赖私有仓或特定源

在项目根目录创建或编辑 oh-package.json5

json5 复制代码
{
  "dependencies": {
    "my_sdk": "1.0.0"
  },
  "devDependencies": {},
  "registry": "https://ohpm.openharmony.cn/ohpm"
}

或通过环境变量:

bash 复制代码
export OHPM_REGISTRY=https://ohpm.openharmony.cn/ohpm
ohpm install

五、Flutter 插件中的鸿蒙 SDK 结构示例

image_cropper 为例,其鸿蒙端结构:

bash 复制代码
image_cropper/ohos/
├── oh-package.json5           # 主包配置
├── build-profile.json5
├── src/
│   ├── main/
│   │   ├── module.json5
│   │   └── ets/
│   │       └── components/
│   │           └── plugin/
│   │               └── ImageCropperPlugin.ets
│   └── lib/
│       └── image_crop_ohos/    # 子库(HAR)
│           ├── oh-package.json5
│           ├── build-profile.json5
│           ├── index.ets       # 导出入口
│           └── src/main/ets/
│               ├── crop/
│               ├── pages/
│               └── ui/
└── ...

主包通过 dependencies: { "image_crop_ohos": "file:./src/lib/image_crop_ohos" } 引用本地子库。若将 image_crop_ohos 单独发布到中心仓,其他项目可直接依赖:

json5 复制代码
"dependencies": {
  "image_crop_ohos": "1.0.0"
}

六、流程总览

java 复制代码
┌─────────────────┐
│ 1. 创建 SDK 项目  │
│ oh-package.json5 │
│ module.json5     │
└────────┬────────┘
         │
         ├──────────────────────────────────┐
         │ 若需 C 层能力                      │
         ▼                                  │
┌─────────────────┐                         │
│ 1.5 添加 Native  │  CMake / BUILD.gn       │
│ NAPI 模块        │  → libxxx.so            │
└────────┬────────┘                         │
         │                                  │
         ▼                                  ▼
┌─────────────────┐
│ 2. 编写单元测试   │
│ ohosTest/       │
│ @ohos/hypium    │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ 3. 构建 HAR     │
│ hvigorw         │
│ assembleHar     │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ 4. 发布到中心仓  │
│ ohpm publish    │
│ 密钥 + 文档      │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ 5. 项目依赖使用  │
│ ohpm install    │
│ import 'my_sdk' │
└─────────────────┘

七、C 层(Native)开发

当 SDK 需要调用 C/C++ 代码(如高性能计算、移植三方库、系统底层能力)时,需使用 NAPI(Native API) 作为 ArkTS 与 C/C++ 的桥梁,相当于 Android 的 JNI。

7.1 两种开发方式对比

方式 适用场景 构建系统 是否需要 OpenHarmony 源码
方式一:源码 + BUILD.gn 系统级、预装应用、设备厂商 BUILD.gn ✅ 需要
方式二:应用层 + CMake 普通应用、HAR 包、发布到中心仓 CMake ❌ 不需要

7.2 方式一:基于 OpenHarmony 源码(BUILD.gn

适用于有完整 OpenHarmony 源码、需将 NAPI 编译进系统镜像的场景。

7.2.1 目录结构
bash 复制代码
mysubsys/                    # 子系统
├── ohos.build
└── hello/                   # 组件
    └── hellonapi/          # 模块
        ├── BUILD.gn
        └── hellonapi.cpp
7.2.2 添加子系统 ohos.build

mysubsys/ohos.build

json 复制代码
{
  "subsystem": "mysubsys",
  "parts": {
    "hello": {
      "module_list": [
        "//mysubsys/hello/hellonapi:hellonapi"
      ],
      "inner_kits": [],
      "system_kits": [],
      "test_list": []
    }
  }
}
7.2.3 注册到 build/subsystem_config.json
json 复制代码
"mysubsys": {
  "project": "hmf/mysubsys",
  "path": "mysubsys",
  "name": "mysubsys",
  "dir": ""
}
7.2.4 C++ 源码实现 hellonapi.cpp
cpp 复制代码
#include <string>
#include "napi/native_api.h"
#include "napi/native_node_api.h"

// 1. 业务接口实现
static napi_value getHelloString(napi_env env, napi_callback_info info) {
  napi_value result;
  std::string words = "Hello OpenHarmony NAPI";
  NAPI_CALL(env, napi_create_string_utf8(env, words.c_str(), words.length(), &result));
  return result;
}

// 2. 注册对外接口
static napi_value registerFunc(napi_env env, napi_value exports) {
  static napi_property_descriptor desc[] = {
    DECLARE_NAPI_FUNCTION("getHelloString", getHelloString),
  };
  NAPI_CALL(env, napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc));
  return exports;
}

// 3. 定义并注册 NAPI 模块
static napi_module hellonapiModule = {
  .nm_version = 1,
  .nm_flags = 0,
  .nm_filename = nullptr,
  .nm_register_func = registerFunc,
  .nm_modname = "hellonapi",
  .nm_priv = nullptr,
  .reserved = { 0 },
};

extern "C" __attribute__((constructor)) void hellonapiModuleRegister() {
  napi_module_register(&hellonapiModule);
}
7.2.5 BUILD.gn 构建脚本
gn 复制代码
import("//build/ohos.gni")

ohos_shared_library("hellonapi") {
  include_dirs = [
    "//foundation/arkui/napi/interfaces/kits",
    "//foundation/arkui/napi/interfaces/inner_api",
  ]
  cflags_cc = [ "-Wno-error", "-Wno-unused-function" ]
  sources = [ "hellonapi.cpp" ]
  deps = [ "//foundation/arkui/napi:ace_napi" ]
  relative_install_dir = "module"
  subsystem_name = "mysubsys"
  part_name = "hello"
}
7.2.6 编译与烧录
bash 复制代码
# 增量编译
./build.sh --product-name rk3568 --ccache --build-target=hellonapi --target-cpu arm64

# 全量编译后烧录镜像
7.2.7 ETS 调用
typescript 复制代码
import hellonapi from '@ohos.hellonapi';

let str = hellonapi.getHelloString();

需在 SDK 目录下提供 @ohos.hellonapi.d.ts 声明:

typescript 复制代码
declare namespace hellonapi {
  function getHelloString(): string;
}
export default hellonapi;

7.3 方式二:基于 DevEco Studio + CMake(应用层)

适用于普通应用开发者,无需 OpenHarmony 源码,在 DevEco Studio 中创建 Native C++ 模块。

7.3.1 项目结构
bash 复制代码
entry/
├── src/
│   └── main/
│       ├── cpp/                    # C++ 源码
│       │   ├── CMakeLists.txt
│       │   ├── napi_init.cpp
│       │   └── types/              # 类型声明(可选)
│       │       └── libentry/
│       │           ├── oh-package.json5
│       │           └── index.d.ts
│       └── ets/
│           └── ...
├── build-profile.json5
└── oh-package.json5
7.3.2 CMakeLists.txt
cmake 复制代码
cmake_minimum_required(VERSION 3.4.1)
project(entry)

set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
include_directories(${NATIVERENDER_ROOT_PATH})

# 生成 libentry.so
add_library(entry SHARED napi_init.cpp)

# 链接 NAPI 库
target_link_libraries(entry PUBLIC libace_napi.z.so)
7.3.3 napi_init.cpp 实现
cpp 复制代码
#include "napi/native_api.h"
#include "napi/native_node_api.h"

static napi_value add(napi_env env, napi_callback_info info) {
  size_t argc = 2;
  napi_value args[2];
  napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

  int32_t a, b;
  napi_get_value_int32(env, args[0], &a);
  napi_get_value_int32(env, args[1], &b);

  napi_value result;
  napi_create_int32(env, a + b, &result);
  return result;
}

static napi_value registerFunc(napi_env env, napi_value exports) {
  napi_property_descriptor desc[] = {
    { "add", nullptr, add, nullptr, nullptr, nullptr, napi_default, nullptr }
  };
  napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
  return exports;
}

static napi_module entryModule = {
  .nm_version = 1,
  .nm_flags = 0,
  .nm_filename = nullptr,
  .nm_register_func = registerFunc,
  .nm_modname = "libentry",
  .nm_priv = nullptr,
  .reserved = { 0 },
};

extern "C" __attribute__((constructor)) void entryModuleRegister() {
  napi_module_register(&entryModule);
}
7.3.4 build-profile.json5 配置

在模块的 build-profile.json5 中配置 Native 编译:

json5 复制代码
{
  "apiType": "stageMode",
  "buildOption": {
    "externalNativeOptions": {
      "path": "./src/main/cpp/CMakeLists.txt",
      "arguments": "-DOHOS_STL=c++_shared",
      "abiFilters": ["armeabi-v7a", "arm64-v8a"]
    }
  }
}
7.3.5 类型声明与 SO 关联

src/main/cpp/types/libentry/ 下创建:

oh-package.json5

json5 复制代码
{
  "name": "libentry.so",
  "types": "./index.d.ts",
  "version": "1.0.0",
  "description": "Native NAPI module"
}

index.d.ts

typescript 复制代码
export const add: (a: number, b: number) => number;
7.3.6 主模块 oh-package.json5 引用

entry/oh-package.json5dependencies 中添加:

json5 复制代码
{
  "dependencies": {
    "libentry.so": "file:./src/main/cpp/types/libentry"
  }
}
7.3.7 ETS 调用
typescript 复制代码
import libentry from 'libentry.so';

let sum = libentry.add(1, 2);  // 3

7.4 调用已有三方 SO

若已有预编译的 .so 文件,无需编写 C++ 源码,只需:

  1. .so 放入 src/main/libs/<abi>/ 目录(如 src/main/libs/arm64-v8a/libmylib.so
  2. 创建类型声明包,在 oh-package.json5name 填 SO 名(如 libmylib.so),types 指向 index.d.ts
  3. 主模块 oh-package.json5dependencies 中添加:"libmylib.so": "file:./src/main/cpp/types/libmylib"
  4. build-profile.json5externalNativeOptions 中配置 abiFilters,或通过 CMake 的 add_library(IMPORTED) 引入预编译 so(具体以 DevEco 文档为准)

目录示例

bash 复制代码
src/main/
├── libs/
│   ├── arm64-v8a/
│   │   └── libmylib.so
│   └── armeabi-v7a/
│       └── libmylib.so
└── cpp/types/libmylib/
    ├── oh-package.json5
    └── index.d.ts

types/libmylib/oh-package.json5

json5 复制代码
{
  "name": "libmylib.so",
  "types": "./index.d.ts",
  "version": "1.0.0"
}

index.d.ts

typescript 复制代码
export const nativeMethod: (param: string) => number;

主模块 oh-package.json5

json5 复制代码
{
  "dependencies": {
    "libmylib.so": "file:./src/main/cpp/types/libmylib"
  }
}

7.5 NAPI 常用类型转换

ETS 类型 C/C++ 获取 C/C++ 返回
number napi_get_value_int32 / napi_get_value_double napi_create_int32 / napi_create_double
string napi_get_value_string_utf8 napi_create_string_utf8
boolean napi_get_value_bool napi_get_boolean(创建 true/false 的 napi_value)
ArrayBuffer napi_get_arraybuffer_info napi_create_arraybuffer
对象 napi_get_property napi_create_object

7.6 C 层流程总览

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│ C++ 实现 (napi_init.cpp)                                      │
│  - 实现业务逻辑 napi_value xxx(napi_env, napi_callback_info) │
│  - registerFunc 注册接口                                     │
│  - napi_module_register 注册模块                             │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│ 构建 (CMake / BUILD.gn)                                      │
│  - add_library(entry SHARED ...)                              │
│  - target_link_libraries(entry libace_napi.z.so)             │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│ 产物 libentry.so + index.d.ts                                │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│ ETS: import libentry from 'libentry.so'                       │
│      libentry.add(1, 2)                                       │
└─────────────────────────────────────────────────────────────┘

八、参考链接

相关推荐
richard_yuu28 分钟前
鸿蒙心理测评模块实战|PHQ-9/GAD7双量表答题、实时计分与结果本地化存储
华为·harmonyos
不爱吃糖的程序媛3 小时前
2026年Electron 鸿蒙PC环境搭建指南
人工智能·华为·harmonyos
nashane4 小时前
HarmonyOS 6学习:长截图功能开发中的滚动拼接与权限处理实战
人工智能·华为·harmonyos
大师兄66685 小时前
从零开发一个 HarmonyOS 输入法——KikaInputMethod 完整拆解
harmonyos·服务卡片·harmonyos6·formkit
Python私教10 小时前
鸿蒙 NEXT 也能接 MCP?用 ArkTS 跑通 AI Agent 工具链
人工智能·华为·harmonyos
Swift社区13 小时前
分布式能力在鸿蒙 PC 上到底怎么用?
分布式·华为·harmonyos
nashane1 天前
HarmonyOS 6学习:外接键盘CapsLock与长截图功能的实战调试与完整解决方案
学习·华为·计算机外设·harmonyos
aqi001 天前
一文理清 HarmonyOS 6.0.2 涵盖的十个升级点
android·华为·harmonyos·鸿蒙·harmony
环信即时通讯云1 天前
环信Flutter UIKit适配鸿蒙实战指南
flutter·华为·harmonyos