项目源码:InkNote 的 AtomGit 仓库
FRB 项目:oh-flutter/flutter_rust_bridge
Rust 社区:开放原子旋武开源社区

本地笔记应用看起来不复杂:左边列表,中间编辑器,右边预览,再加一个“自动保存”。可真正开始写以后,最先暴露的问题往往是数据什么时候落盘。

用户连续输入时,上一轮保存可能还没结束;切换笔记时,当前内容可能仍在防抖计时器里;进程恰好在写元数据时退出,还可能留下半截 JSON。InkNote 把这些问题拆成两层:Flutter 负责编辑状态与保存时机,Rust 负责文件组织、元数据恢复和原子写入,中间通过 flutter_rust_bridge(简称 FRB)连接。

本文从社区和框架说起,结合 InkNote 的实际代码,走完环境准备、功能实现、鸿蒙 PC 运行验证,以及问题反馈和代码贡献的完整流程。

一、认识旋武社区与 FRB

1. 旋武社区:Rust 学习与开源协作的入口

开放原子旋武开源社区是开放原子开源基金会旗下的 Rust 专项社区,围绕 Rust 学习、工具使用和开源项目协作建设国内生态。官网提供 Rust 发行版及镜像、学习资料、在线编码体验、社区项目和技术活动等资源。

对于准备使用 Flutter + Rust 开发应用的同学,可以从社区的学习资料和工具资源入手,了解 Rust 工具链与工程实践;遇到具体项目问题时,再进入对应仓库参与讨论和贡献。社区提供学习与交流入口,项目仓库则承载可以跟踪的 Issue 和 Pull Request。

2. FRB:把 Dart 界面与 Rust 能力连接起来

flutter_rust_bridge 是连接 Flutter/Dart 与 Rust 的跨语言绑定工具。开发者在 Rust 中定义对外 API 和数据结构,通过代码生成器生成桥接代码,Dart 侧便可使用生成的接口调用 Rust,并处理返回数据和错误。

本文使用的 FRB 项目入口是:https://atomgit.com/oh-flutter/flutter_rust_bridge。涉及鸿蒙平台的接入说明、版本变化和框架问题反馈,可从该仓库查阅。

在 InkNote 中,FRB 连接的是笔记 API:初始化、读取、创建、更新、删除和分类管理。Flutter 页面通过 Dart 仓库层调用这些 API,Rust 再完成文件操作。FRB 负责跨语言通信,保存顺序、异常恢复和界面状态仍需要应用自己设计。

二、为什么 InkNote 选择 Flutter + Rust + FRB

InkNote 面向鸿蒙 PC 的本地 Markdown 写作场景,需要同时处理桌面交互和文件可靠性。三者的职责可以这样划分:

组成在 InkNote 中的职责选择它的价值
Flutter笔记列表、编辑器、Markdown 预览、主题和快捷键将界面交互集中在一套 Dart 代码中,便于调整桌面与窄窗口布局
Rust文件读写、元数据、分类、校验和恢复将存储规则集中实现,并通过临时目录测试验证文件操作
FRB生成 Dart/Rust 绑定,传递参数、结果和错误减少手写跨语言类型转换和调用封装,保持业务 API 清晰

具体到这个项目,选择 FRB 有四个直接收益。

第一,跨语言接口可以围绕业务命名。 页面需要的是 createNoteupdateNoteloadNote,无需自行拼接消息协议。修改 Rust 对外结构后重新生成绑定,Dart 侧的调用变化也更容易通过静态检查发现。

第二,编辑状态与文件状态可以分开管理。 每次按键先更新 Dart 草稿,停止输入后再提交完整快照。这样减少了跨语言调用次数,也让输入响应不必等待每次文件写入完成。

第三,Rust 数据层可以独立验证。 存储测试直接操作临时目录;Flutter 控制器和组件测试可以替换成内存仓库。不同层的问题能在各自边界复现,再用真机验证整条调用链。

第四,存储逻辑具备复用基础。 文件组织、分类规则和恢复算法放在 Rust 中,后续扩展平台时可以复用。各平台仍需单独完成原生库构建、应用目录获取和生命周期接入,FRB 不会自动完成这些工作。

对于只需少量文件读写的小工具,纯 Dart 同样是可选方案。InkNote 采用这套组合,是为了让逐渐增长的存储规则有独立边界;引入 Rust 工具链、原生编译和桥接版本管理,也是这次选型需要承担的成本。

三、环境搭建:先参考已有教程,再对齐项目版本

1. 两篇环境准备文章

基础安装过程可以直接参考下面两篇文章:

  1. 《2026 年如何上车 Flutter-OH:环境搭建与上手流程》:用于准备 Flutter-OH、DevEco Studio、HarmonyOS SDK,检查工具链并了解工程运行和签名流程。
  2. 《Rust | VS Code搭建Rust开发环境的超详细图文教程总结(含Rust开发常用插件)》:用于了解 Rust 安装、Cargo、VS Code 和常用扩展。该文主要以 Windows 为例;macOS 开发者应按本机平台准备编译与调试工具。

两篇文章分别解决 Flutter-OH 和 Rust 的入门环境问题。复现 InkNote 时,还要按项目构建指南配置 FRB、Rust OHOS target 和可写的 Native SDK;教程中的示例版本不能直接替代项目版本要求。

2. InkNote 的工具链要求

仓库 README 和 Docs/HARMONYOS_BUILD.md 记录的已验证组合如下:

工具或平台仓库记录
Flutter-OH3.35.8-ohos-0.0.3
Dart3.9.2
FRB Dart/Rust 依赖与 codegen2.13.0-beta.6
Ruststable,安装 aarch64-unknown-linux-ohos target
DevEco Studio6.0 系列
OpenHarmony SDKAPI 22,包含 ETS、Native 和 Toolchains
本文目标设备arm64 鸿蒙 PC / 2in1 真机

先确认终端中的 flutter 指向 Flutter-OH,再安装对应的 Rust target 和生成器:

flutter --version
flutter doctor -v
rustc --version
cargo --version
rustup target add aarch64-unknown-linux-ohos
cargo install flutter_rust_bridge_codegen --version 2.13.0-beta.6
flutter_rust_bridge_codegen --version

FRB 的 Dart 依赖、Rust 依赖和生成器在本项目中应保持同一版本。只升级其中一处,可能导致生成接口与原生库不匹配。

3. 获取项目并生成绑定

git clone https://atomgit.com/nutpi/InkNote.git
cd InkNote

# 首次克隆后创建本地构建配置;已有签名配置时不要覆盖。
cp ohos/build-profile.example.json5 ohos/build-profile.json5

flutter pub get
flutter_rust_bridge_codegen generate

FRB 配置文件 flutter_rust_bridge.yaml 内容如下:

rust_input: crate::api
rust_root: rust/
dart_output: lib/src/rust

生成器从 rust/src/api/ 对外模块生成绑定,输出包括 lib/src/rust/rust/src/frb_generated.rs。业务改动应写在 API、控制器、仓库层或 Store 中,再重新生成绑定。

鸿蒙构建还需要可写的 SDK 根目录。按项目构建指南,Native 工具链应能定位到 <SDK 根目录>/22/native/llvm/bin/clang。后续命令中的 /path/to/writable/sdk 必须替换为本机实际路径。

四、功能逻辑与核心代码:从编辑到重新加载

1. 先明确完整功能链路

当前 Flutter 版提供笔记创建、编辑、保存和删除,支持分类管理、搜索、Markdown 实时预览、视图切换、主题和字号设置。本文重点分析最影响用户信任的一条路径:

新建笔记 → 编辑草稿 → 自动保存 → Rust 写盘 → 显示保存状态 → 关闭应用 → 重启恢复。

内容变化

防抖与串行保存

FRB updateNote

返回结果或错误

初始化同一数据目录

Flutter 编辑器

AppController

NoteRepository

Rust notes API

NoteStore

Markdown 正文

JSON 元数据

列表、预览与保存状态

应用重新启动

推荐按下面的顺序阅读源码:

InkNote/
├── lib/main.dart                     FRB 初始化和应用入口
├── lib/src/app/app_controller.dart   草稿、revision、防抖和切换逻辑
├── lib/src/notes/note_repository.dart Dart 仓库接口与 FRB 实现
├── lib/src/notes/note_editor.dart     编辑与预览界面
├── rust/src/api/notes.rs              对 Flutter 暴露的笔记 API
├── rust/src/api/models.rs             跨语言数据结构
├── rust/src/store.rs                  文件写入、索引和恢复
├── lib/src/rust/                      自动生成的 Dart 绑定
├── rust_builder/                      原生库构建与 OHOS 集成
└── ohos/                              鸿蒙入口与 HAP 配置

2. 初始化与更新:跨语言接口只表达业务动作

应用先初始化 RustLib,再由 NativeNoteRepository 使用 path_provider 获取应用支持目录,将其中的 floral-notepaper 子目录传给 Rust。这个目录名沿用旧版本标识,用于保持数据兼容。

Rust API 的初始化和更新逻辑如下,代码来自 rust/src/api/notes.rs

pub fn initialize(data_dir: String) -> Result<AppSnapshot, String> {
    let store = NoteStore::open(PathBuf::from(data_dir)).map_err(|error| error.to_string())?;
    crate::store::replace_store(store).map_err(|error| error.to_string())?;

    with_store(|store| {
        Ok(AppSnapshot {
            settings: store.load_settings()?,
            notes: store.list_notes()?,
            categories: store.list_categories()?,
        })
    })
    .map_err(|error| error.to_string())
}

pub fn update_note(
    id: String,
    title: String,
    content: String,
    category: String,
) -> Result<Note, String> {
    with_store(|store| store.update_note(&id, title, content, category))
        .map_err(|error| error.to_string())
}

初始化一次返回包含设置、笔记摘要和分类的 AppSnapshot,Flutter 控制器可以基于同一份初始化结果更新页面。update_note 则只接收笔记 ID、标题、正文和分类,将操作交给 Store。

这里的错误通过 Result 返回,再由桥接层传递到 Dart。控制器必须捕获失败并保留草稿,不能把“调用结束”直接视为“保存成功”。

3. 自动保存:700ms 防抖、串行执行与 revision 分别解决什么

用户输入时,updateDraft() 更新草稿、递增 _revision、将状态设为 dirty,并重置 700ms 定时器。防抖减少保存次数;它本身不解决异步保存的先后顺序。

完整保存方法还需要处理正在执行的任务。下面是仓库 AppController.saveNow() 的实现:

  Future<void> saveNow() async {
    _autoSaveTimer?.cancel();
    if (selectedNote == null || saveState == SaveState.saved) return;
    final activeSave = _saveOperation;
    if (activeSave != null) {
      await activeSave;
      if (saveState == SaveState.dirty) return saveNow();
      return;
    }

    final completer = Completer<void>();
    _saveOperation = completer.future;
    final noteId = selectedNote!.id;
    final revision = _revision;
    final title = draftTitle;
    final content = draftContent;
    final category = draftCategory;
    saveState = SaveState.saving;
    notifyListeners();
    try {
      final updated = await _repository.updateNote(
        id: noteId,
        title: title,
        content: content,
        category: category,
      );
      if (selectedNote?.id == noteId) {
        selectedNote = updated;
      }
      await _refreshNotes();
      saveState = revision == _revision ? SaveState.saved : SaveState.dirty;
    } catch (error) {
      saveState = SaveState.failed;
      _reportError(error);
    } finally {
      _saveOperation = null;
      completer.complete();
      notifyListeners();
    }

    if (saveState == SaveState.dirty && settings.autoSave) {
      _autoSaveTimer = Timer(const Duration(milliseconds: 350), saveNow);
    }
  }

这段代码有三个关键点。

_saveOperation 串行化保存。 已有写入任务时,后续调用先等待它完成,再判断是否仍有脏草稿需要保存,避免控制器同时发起多轮写入。Rust 的 with_store 也使用全局写锁串行访问 Store。

revision 决定当前草稿能否显示“已保存”。 假设 revision 12 发起保存,等待返回时用户又输入,当前版本变为 13。第 12 版保存成功后,页面仍应保持 dirty。这里比较的是 Dart 在发起请求前捕获的局部变量和当前 _revision,revision 并没有作为参数传给 Rust,也没有由 Rust 回传。

旧保存结束后继续安排新保存。 若仍为 dirty 且自动保存开启,控制器安排 350ms 后补存最新草稿。因此,可靠自动保存依赖“防抖 + 串行执行 + 版本比较 + 后续补存”共同工作,单独增加版本号并不能防止旧写入覆盖新内容。

切换笔记前,selectNote() 会先调用 flushPendingSave():等待已有保存,必要时写入脏草稿或重试失败保存;若仍失败,则停留在当前笔记。对于“等待期间又继续输入”“连续快速切换”等情况,还应通过回归测试核对最终草稿和磁盘内容。

4. 文件落盘:先写临时文件,再替换正式文件

本地文件布局如下:

floral-notepaper/
├── metadata.json
├── settings.json
└── notes/
    ├── <note-id>.md
    └── <category>/<note-id>.md

正文使用 UTF-8 Markdown;标题、分类、字数和时间戳等信息保存在元数据中。write_jsonwrite_text 最终调用同一个写入方法:

    fn write_bytes(&self, path: &Path, bytes: &[u8]) -> Result<(), StoreError> {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        let temp_path = path.with_extension("tmp");
        let mut file = fs::File::create(&temp_path)?;
        file.write_all(bytes)?;
        file.sync_all()?;
        drop(file);
        fs::rename(temp_path, path)?;
        Ok(())
    }

代码先在目标文件同目录创建 .tmp,写入并调用 sync_all(),关闭文件句柄后执行 rename。在目标平台文件系统支持原子替换的前提下,替换前已有正式文件仍保留上一版,从而降低直接覆盖写导致正式文件只剩半截内容的风险。

这个实现的边界也要说清楚:单文件替换不等于 Markdown 与元数据之间的多文件事务;代码没有同步父目录,正常重启验证也不能证明断电级持久性。对写入中断和断电的要求更高时,需要额外设计事务或日志机制,并在目标设备上进行专项验证。

5. 元数据损坏:保留现场,再从正文恢复

load_metadata() 读取失败时,会先调用 back_up_corrupt_file() 保留原文件,再扫描笔记目录重建元数据,最后写回索引。当前实现中的备份文件名形如 metadata.corrupt-<时间戳>.json

重建会读取根笔记目录和分类子目录中的 .md 文件:从文件名取得 ID,从目录取得分类,从正文首个非空行推导标题,并使用文件修改时间重建时间字段。因此,正文可以重新出现在列表中,但原来独立填写的标题、创建时间等信息未必能够精确恢复。

扫描器只处理 .md,不会自动把残留 .tmp 当成有效笔记。若要进一步实现临时文件恢复,应先校验内容并保留现有正式文件,避免把不完整快照覆盖到最后一份有效数据上。

6. 仓库接口与测试:分别验证状态和文件

NoteRepository 保留 Dart 业务接口,生产环境通过 NativeNoteRepository 调用 FRB,控制器和 Widget 测试则可以注入内存实现。这样能稳定复现自动保存、写入延迟和保存失败,不必让每个 UI 测试都加载原生库。

仓库中已有的控制器测试覆盖切换前保存草稿、保存失败阻止切换,以及保存期间再次编辑后切换等场景;Rust 测试覆盖笔记读写、删除分类后移动笔记,以及损坏元数据的备份和重建。

发布前可在工程根目录执行:

dart analyze lib test
flutter test
cargo fmt --manifest-path rust/Cargo.toml --check
cargo test --manifest-path rust/Cargo.toml
cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings

内存仓库测试验证界面状态,Rust 测试验证存储行为,真机重启验证两者连通后的持久化结果。还可以补充“销毁 Store → 用同一目录重新打开 → 比较字段”的回归测试,固定正常重启场景下的数据预期。

五、运行方式及真机效果

1. 构建并安装到鸿蒙 PC

完成第三节的环境准备后,用 DevEco Studio 打开项目的 ohos/,在 File > Project Structure > Signing Configs 中配置当前开发者和目标设备的签名。Profile 需要匹配包名 com.floralnotepaper.app、开发者证书和目标 PC 的 UDID。

项目虽然已经更名为 InkNote,但保留旧包名和原生库名以维持兼容,不要仅为了统一显示名称而随意修改这些标识。

替换 SDK 路径后,构建签名 HAP:

OHOS_BASE_SDK_HOME=/path/to/writable/sdk \
  flutter build hap --release --target-platform ohos-arm64

按项目构建指南,签名产物位于 build/ohos/hap/entry-default-signed.hap。若当前只验证编译,可添加 --no-codesign 生成未签名包;真机安装应使用匹配设备的签名包。

确认 hdc 已加入 PATH,将下面的 <device-id> 替换为 hdc list targets 输出的实际设备标识:

hdc list targets
hdc -t <device-id> install -r build/ohos/hap/entry-default-signed.hap
hdc -t <device-id> shell aa start \
  -a EntryAbility \
  -b com.floralnotepaper.app

签名配置完成后,也可以直接调试运行:

OHOS_BASE_SDK_HOME=/path/to/writable/sdk \
  flutter run -d <device-id>

如需在 macOS 辅助调试,可在该平台依赖与桌面构建环境就绪后执行 flutter run -d macos;鸿蒙 PC 的最终结果仍应在目标设备上验证。更完整的说明见项目仓库中的 Docs/HARMONYOS_BUILD.md

2. 当前 Flutter 版效果图

InkNote 在 HarmonyOS PC 真机上的 Markdown 编辑、分栏预览与保存状态

图中展示 InkNote 在 HarmonyOS PC 真机上的运行状态:新建 FRB Storage Acceptance,输入正文,等底部显示“已保存”,关闭应用窗口,再从设备端重新启动。原有实操记录显示,重启后标题、113 字正文和分栏预览仍然存在。

当前效果图使用 Flutter 版应用窗口。仓库 Docs/images 中的早期 Tauri 截图和 GIF 属于迁移历史,不能用来证明当前 Flutter + FRB 的运行效果。

3. 手工验证完整闭环

按下面步骤复现保存与恢复:

  1. 新建笔记,确认编辑器可以输入标题和正文。
  2. 输入 Markdown,检查预览内容与编辑区对应。
  3. 停止输入,等待 700ms 防抖触发保存,直到状态显示“已保存”;磁盘写入耗时需另外计算。
  4. 修改分类,确认保存后列表与分类筛选同步更新。
  5. 关闭应用窗口,使用同一包名再次启动,核对标题、正文、分类和字数。
  6. 再次编辑并保存,确认恢复后的笔记仍可正常更新。
  7. 在测试数据目录中模拟写入失败,核对草稿保留与切换阻止;破坏元数据后,核对备份文件及重建列表。

原有实操记录的结果范围如下,后两项需要在交付环境中另行验证:

验收项原有记录证据或验证方式
鸿蒙应用启动通过真机按包名启动
新建与正文编辑通过编辑区与预览区出现对应正文
自动保存通过底部显示“已保存”
关闭窗口后重新打开通过标题、113 字正文和预览恢复
损坏元数据恢复交付环境需复测Rust 测试与测试目录中的人工验证
断电级持久性未证明需要独立的断电与写入中断测试

六、FAQ:从问题定位到 Issue 与 PR

1. 遇到框架问题,如何提 Issue?

先确定问题出现在哪一层。InkNote 的草稿状态、分类逻辑或元数据恢复问题,优先进入 InkNote 仓库;若最小示例中也能复现 FRB 生成失败、类型映射错误或桥接调用异常,则进入 FRB 仓库Issues 页面反馈。

提交前先搜索相同关键词、报错和版本,查看是否已有解决方案。若仓库提供 Issue 模板,按模板填写;至少应包含以下信息:

标题:[OHOS][具体版本] 操作或触发条件 + 实际错误

环境:
- 主机系统与 CPU 架构:
- Flutter-OH / Dart 版本:
- Rust / Cargo 版本:
- FRB Dart、Rust 依赖与 codegen 版本:
- DevEco Studio / SDK API:
- 目标设备型号与系统版本:
- 应用仓库分支或 commit:

复现步骤:
1. 获取哪个最小示例,执行什么命令
2. 修改了哪些配置或 API
3. 触发哪个操作后出现问题

预期行为:
实际行为:
完整错误日志和堆栈:
最小复现仓库或补丁:
已尝试的排查步骤及结果:

尽量把示例缩减到一个 Rust API 和一个 Dart 调用,并说明相同代码在其他平台是否可以运行。日志使用文本保留关键上下文,上传前去除签名口令、令牌和私人笔记内容。这样维护者才能独立复现,而无需先搭建完整笔记应用。

2. 问题定位并完成修复后,如何提 PR?

如果修复来自自己的代码改动,可以在本地修复和验证后提交 Pull Request。通常不需要先把 Issue 关闭;让 PR 关联 Issue,待评审合并并确认结果后再关闭,更便于跟踪。

具体流程如下:

  1. 在对应 AtomGit 仓库中确认贡献指南及目标分支,必要时先在 Issue 中说明修复思路。
  2. Fork 仓库并克隆个人 Fork,从维护者要求的目标分支创建修复分支,例如 fix/ohos-native-library-loading
  3. 修改导致问题的源代码,补充能复现缺陷的回归测试。涉及 FRB API 或生成器变化时,按仓库约定重新生成相关文件。
  4. 运行该仓库要求的检查,并在原来报错的平台验证修复。应用修复可使用本文第四节的检查命令;FRB 框架修复应以 FRB 仓库自身的贡献指南和 CI 要求为准。
  5. 将提交推送到个人 Fork,在 AtomGit 的 Pull Requests 页面选择个人分支作为源分支、原仓库要求的分支作为目标。
  6. 在 PR 描述中写清问题触发条件、根因、修改后的行为、关联 Issue、执行的测试及平台结果,按评审意见继续修改。
  7. 合并后记录包含修复的提交或版本,在 InkNote 中更新对应依赖并重新生成、构建和回归验证,最后更新 Issue 状态。

PR 描述可以采用下面的简短结构,示例中的编号应替换为真实 Issue:

问题:在什么环境、执行什么操作时出现什么错误。
原因:错误发生在哪个环节。
修改:修复后有什么可观察的行为变化。
验证:测试命令、目标设备、修复前后的结果。
关联:Issue #<实际编号或链接>。

如果问题只是 SDK 路径配置错误,按说明修复环境并反馈结果即可;若维护者已经合并修复,则升级并验证即可。只有新增代码、测试或文档改动时,才需要提交相应 PR。

3. 其他常见问题与解决方案

问题或现象优先检查处理方式
flutter doctor 没有正确识别鸿蒙工具链当前 Flutter 路径、SDK 配置按 Flutter-OH 环境文章检查 PATH 与 ohos-sdk,确保执行的是 Flutter-OH
FRB 生成后 Dart 接口或原生调用不匹配三处 FRB 版本是否一致对齐 Dart、Rust 和 codegen;本项目使用 2.13.0-beta.6,随后重新生成并构建原生库
failed to find tool "llvm/bin/clang"Native SDK 绝对路径检查可写 SDK 中 22/native/llvm/bin/clang 是否存在,以及 Cargokit 使用的 SDK 路径
构建尝试 x86_64-unknown-linux-ohosOHOS ABI 配置与旧缓存按当前 arm64 目标检查 abiFilters,必要时 flutter clean 后重新构建
安装报 fail to verify pkcs7 fileProfile、包名、证书和 UDID用 DevEco Studio 为当前包名和真机重新配置匹配的调试签名
启动时报原生库加载失败架构、库名及 HAP 中的动态库检查 arm64 包内的 libfloral_notepaper_native.so 及其依赖,修复构建集成后重新安装
保存中继续输入,旧请求返回后状态不正确_saveOperation、revision 与补存分支用延迟仓库复现,检查串行等待和补存,确认最终文件包含最新输入
保存失败后无法切换笔记数据目录权限、空间及写入日志保留草稿,修复写入原因后重试;阻止切换用于避免静默丢稿
metadata.json 损坏,笔记列表异常正文目录及损坏备份在备份基础上验证重建结果,核对正文、推导标题和分类,不直接清空笔记目录
重启后像是进入了新工作区包名、应用支持目录、是否卸载重装检查是否仍使用同一应用身份和数据路径;覆盖安装验证期间避免随意修改兼容标识

七、后续扩展与项目入口

InkNote 当前已经将 Markdown 编辑、预览、分类管理与本地保存串联起来。后续扩展可以继续围绕数据边界推进:加强退出期间的草稿保护,补充更严格的写入中断测试,并为数据格式升级设计可回退的迁移流程。

普通 Markdown 文件提供了开放的数据基础,但当前 Flutter 版的导入导出等旧 Tauri 功能尚未迁移完成。介绍功能时,应以当前 Flutter 实现和目标设备验证结果为准。

想进一步阅读或参与项目,可以从以下入口开始:

Logo

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

更多推荐