【时光清单|15】HarmonyOS ArkTS 本地状态持久化实战:让保存、删除和页面返回后的数据即时一致

【时光清单|15】HarmonyOS ArkTS 本地状态持久化实战:让保存、删除和页面返回后的数据即时一致

本地应用的数据问题往往不是"有没有写进文件",而是三个时刻能否保持一致:用户点击保存后当前页显示成功,返回首页后列表立即更新,应用被系统回收再启动时仍能恢复同一份数据。只解决其中一个时刻,就会出现很典型的故障:详情页提示保存成功,首页还展示旧标题;删除后返回列表,条目仍停留到下次启动;首帧先出现默认主题,随后又突然切换;多个页面各自维护数组,最终谁也说不清哪份才是真实状态。

时光清单 的真实源码给出了一条可复核的实现链。DataStore.ets 用 ArkData Preferences 保存 JSON 字符串,AnniversaryRepository.ets 持有内存列表并统一增删改查,AppViewModel.ets 向页面暴露业务操作,StateKeys.DATA_VERSION 作为轻量变化信号,首页、全部列表和筛选列表通过 @StorageLink@Watch 重新加载数据。EntryAbility.ets 还在启动阶段同步读取心情背景,避免首帧先使用默认值。

本文重点不是堆砌 Preferences API,而是把"持久化事实、内存事实、页面快照、跨页通知"四层关系讲清楚。文中会区分现有代码与建议演进:当前源码包含纪念日、习惯、日记、相册等轻量数据的本地写入路径;版本号是刷新信号而不是数据本身;JSON 反序列化只有类型断言,没有运行时结构校验;错误目前主要写日志并回退默认值,后续可继续补充可观测错误状态。

本轮结论只来自列出的生产源码和历史错误记录,没有执行新的构建、模拟器、真机、强制结束进程、异常 JSON 注入或并发点击测试。因此,"当前事实"只描述可以从代码直接确认的调用顺序和边界;"历史证据"只描述文档在特定日期记录的现象与修复;所有返回结果类型、数据迁移、写队列和恢复事务都明确属于建议实现。这样划分可以避免把一段示例代码、一次旧构建或一个成功 Toast 误写成已经验证的持久化可靠性。

本文将解决:

  1. Preferences 初始化、读写和 flush() 如何组成可靠存储入口。
  2. Repository 为什么既维护内存列表,又必须把修改持久化。
  3. DATA_VERSION 如何让保存、删除和返回后的页面即时一致。
  4. 同步首帧读取与异步水合分别适合什么数据。
  5. 双 Key 兼容、JSON 校验、并发写入和备份恢复有哪些边界。
  6. 怎样验证冷启动、增删改、跨页返回及异常数据场景。

本文唯一标记:CSDN-SERIES:ALL-163208902

一、先区分四种状态,避免把 AppStorage 当数据库

在这套实现里,同一条纪念日会以四种形态出现:

层次 真实载体 生命周期 主要职责
持久化事实 Preferences 中的 JSON 字符串 跨进程、跨启动 保存用户数据
仓库内存 AnniversaryRepository.items 当前进程 集中排序、查找和修改
页面快照 @State anniversaries@State item 当前组件 驱动 ArkUI 渲染
变化信号 AppStorage 中的 DATA_VERSION 当前进程共享 通知其他页面重新读取

这四层不能互相替代。Preferences 不会自动让页面重绘,AppStorage 版本号也不会在应用重启后恢复业务数据。页面数组适合渲染,却不应该成为多个页面共同修改的永久事实。

真实数据流可以概括为:

复制代码
用户操作
  -> ViewModel
  -> Repository 修改内存数据
  -> DataStore 写入 Preferences 并 flush
  -> DATA_VERSION + 1
  -> 观察页面重新 loadData
  -> Repository 返回排序后的新快照

把变化信号放在持久化成功之后非常关键。否则其他页面可能先响应版本变化,却读到尚未落盘或尚未更新完成的数据。

历史证据:页面返回后的旧数据曾经真实发生

PROJECT_ERRORS.md 记录了 2026 年 5 月 20 日一次与本文主题直接相关的审核反馈:从首页快捷入口进入倒计时、纪念日或恋爱事项,编辑并保存后,列表仍显示旧内容,必须切换页面才会刷新。记录给出的根因有两层。第一层是保存虽然已经写入数据,但跨页面列表缺少稳定的即时刷新通知;第二层是 ForEach 只使用稳定 id 作为 Key,同一条记录编辑后仍可能复用原来的 UI 节点。

针对这个问题,历史修复记录明确写到:首页、全部列表和筛选列表增加 DATA_VERSION 观察;详情页在保存、删除和置顶后递增版本;可编辑列表的 Key 改为 id + updatedAt。这与当前源码中的 @StorageLink@WatchbumpDataVersion() 和组合 Key 能够互相印证,因此可以把它作为"问题曾发生、修复路径被记录、当前代码保留了该路径"的历史证据。

但历史证据仍有边界。文档写有当时 assembleHap 成功,只能说明当时那组修改完成过一次本地构建;它不能替代本轮的当前构建、设备安装、进程重启、存储损坏和并发提交验证。文章后面的异常测试、迁移和串行化方案都属于建议实现,不能写成已经通过的结果。

还要注意,历史修复解决的是"内存状态与页面快照如何重新对齐",并没有改变 DataStore.putJson() 吞掉写入异常的行为。因此当前 Toast 和页面刷新链路可以即时响应,但底层写失败能否向用户明确反馈,仍是需要继续补齐的可靠性问题。

二、DataStore:把 ArkData Preferences 收敛成一个入口

DataStore 是单例,内部保存 preferences.Preferences 引用和初始化 Promise:

复制代码
export class DataStore {
  private static instance: DataStore | null = null;
  private pref: preferences.Preferences | null = null;
  private initPromise: Promise<void> | null = null;

  static getInstance(): DataStore {
    if (!DataStore.instance) {
      DataStore.instance = new DataStore();
    }
    return DataStore.instance;
  }

  init(context: Context): void {
    if (this.initPromise) return;
    this.initPromise = this.doInit(context);
  }
}

单例的价值不是"全局变量更方便",而是避免每个页面分别创建 Preferences 句柄、分别定义存储名和分别处理初始化时序。真实存储名是 timelist_data,所有业务 Key 也集中在 DataKeys

复制代码
export class DataKeys {
  static readonly ANNIVERSARIES = 'ds_anniversaries';
  static readonly WISHES = 'ds_wishes';
  static readonly DIARY = 'ds_diary';
  static readonly HABITS = 'ds_habits';
  static readonly HABIT_WALL = 'ds_habit_wall';
  static readonly COUPLE = 'ds_couple';
  static readonly ALBUM = 'ds_album';
  static readonly MOOD_BACKGROUND = 'ds_mood_background';
  static readonly WIDGET_ANNIVERSARY_ID = 'ds_widget_anniversary_id';
}

集中 Key 可以减少拼写漂移,也让数据迁移和清理范围更容易审计。新增字段时应先判断它是轻量设置、可序列化小集合,还是需要查询、索引和迁移的结构化数据;后者更适合关系型存储,而不是继续把大 JSON 塞进 Preferences。

三、写入完成的定义:put、flush 与错误路径

真实 putJson() 先等待初始化,再序列化、写入并刷新:

复制代码
async putJson<T>(key: string, value: T): Promise<void> {
  await this.ensureReady();
  if (!this.pref) return;
  try {
    const json = JSON.stringify(value);
    await this.pref.put(key, json);
    await this.pref.flush();
  } catch (e) {
    hilog.error(DOMAIN, TAG,
      'putJson [%{public}s] failed: %{public}s',
      key, JSON.stringify(e));
  }
}

这里有两个值得保留的工程点。第一,调用方拿到 Promise,可以把"保存成功"放在 await 之后;第二,每次写入后执行 flush(),使提交边界清晰。

不过当前实现也有一个真实边界:异常被捕获后只记日志,方法仍然以正常 Promise 结束。页面因此无法区分"已经写入"和"写入失败但被吞掉"。如果业务数据重要,建议把结果改为显式类型:

复制代码
export interface StoreResult {
  ok: boolean;
  message?: string;
}

async putJson<T>(key: string, value: T): Promise<StoreResult> {
  await this.ensureReady();
  if (!this.pref) return { ok: false, message: 'store not ready' };
  try {
    await this.pref.put(key, JSON.stringify(value));
    await this.pref.flush();
    return { ok: true };
  } catch (e) {
    return { ok: false, message: JSON.stringify(e) };
  }
}

这是演进建议,不是对当前源码能力的虚构。只有当 Repository 收到成功结果后,页面才应该显示"保存成功"并递增版本号。

四、读取默认值只能兜底,不能代替数据校验

当前异步读取逻辑会在无值、解析失败或存储未初始化时返回调用方提供的默认值:

复制代码
async getJson<T>(key: string, defaultValue: T): Promise<T> {
  await this.ensureReady();
  if (!this.pref) return defaultValue;
  try {
    const raw = await this.pref.get(key, '');
    if (typeof raw === 'string' && raw.length > 0) {
      return JSON.parse(raw) as T;
    }
  } catch (e) {
    hilog.error(DOMAIN, TAG,
      'getJson [%{public}s] failed: %{public}s',
      key, JSON.stringify(e));
  }
  return defaultValue;
}

JSON.parse(raw) as T 只告诉编译器"把它当成 T",不会检查运行时对象是否真的包含 idtitletargetDate 等字段。如果旧版本结构、手工导入文件或意外损坏的数据进入存储,解析成功仍可能在页面计算时出错。

纪念日数组可以增加轻量守卫:

复制代码
function isAnniversary(value: object): value is Anniversary {
  const item = value as Anniversary;
  return typeof item.id === 'string' &&
    typeof item.title === 'string' &&
    typeof item.targetDate === 'number' &&
    typeof item.type === 'string';
}

function normalizeAnniversaries(value: Object): Anniversary[] {
  if (!Array.isArray(value)) return [];
  return value.filter((item: object) => isAnniversary(item));
}

守卫之后还可以补默认字段和版本迁移。原则是:默认值解决"没有数据",校验解决"数据形状错误",迁移解决"旧结构仍然合法但字段语义发生变化",三者不是一回事。

五、同步首帧与异步水合为什么同时存在

EntryAbility.onCreate() 先同步初始化 DataStore,读取心情背景,再执行全局状态初始化和异步水合:

复制代码
DataStore.getInstance().initSync(this.context);
const mood = DataStore.getInstance()
  .getJsonSync<string>(DataKeys.MOOD_BACKGROUND, 'auto');
this.setStorageString(StateKeys.MOOD_BACKGROUND, mood);

AppStore.bootstrap(this.context);
DataStore.getInstance().init(this.context);
this.hydratePersistentState();

这段顺序针对的是首帧一致性。心情背景直接影响首页视觉,如果先使用 'auto' 构建页面,再异步读取真实值,用户可能看到一次闪变。同步读取小型设置可以把真实值提前放进 AppStorage

但同步读取不应无限扩大。大型列表、图片索引或耗时迁移若阻塞 onCreate(),会延长启动时间。更稳妥的边界是:

数据类型 建议策略
主题、语言、极小开关 可考虑同步首帧读取
纪念日、习惯等小列表 Repository 异步初始化
大列表、可查询结构 使用关系型存储并分页
图片与备份文件 文件 API,异步读取

当前 initSync() 成功后会把 initPromise 设为已完成 Promise,随后 init() 检测到初始化 Promise 已存在便直接返回,因此不会重复打开存储。

六、Repository:让页面不直接操作持久化数组

AnniversaryRepository 同时持有内存列表和 DataStore

复制代码
private items: Anniversary[] = [];
private initialized: boolean = false;
private store: DataStore = DataStore.getInstance();

async init(): Promise<void> {
  if (this.initialized) return;
  this.items = await this.store
    .getJson<Anniversary[]>(DataKeys.ANNIVERSARIES, []);
  this.initialized = true;
}

页面通过 AppViewModel 调用仓库,而不是直接拼 Key、序列化 JSON。这样排序规则、更新规则和持久化时机都集中在一处。

查询返回排序副本:

复制代码
private sortedCopy(): Anniversary[] {
  return [...this.items].sort((a, b) => {
    if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
    return a.targetDate - b.targetDate;
  });
}

复制后排序避免查询操作改变仓库内部数组顺序。页面拿到的是用于显示的快照,真正的修改仍应回到 Repository。

七、保存事务:先改内存,再持久化,再发通知

新增和编辑都走 save()。存在 ID 时修改原对象,不存在时创建新对象并加入列表,随后统一 persist()

复制代码
async save(data: AnniversarySaveData): Promise<Anniversary> {
  await this.init();
  const existing = data.id
    ? this.items.find((item) => item.id === data.id)
    : undefined;

  if (existing) {
    if (data.title) existing.title = data.title;
    if (data.targetDate) existing.targetDate = data.targetDate;
    if (data.remark !== undefined) existing.remark = data.remark;
    existing.updatedAt = Date.now();
    await this.persist();
    return existing;
  }

  const newItem = createAnniversary(data);
  this.items.push(newItem);
  await this.persist();
  return newItem;
}

新增页在 await this.viewModel.saveAnniversary(data) 返回后,才更新卡片默认选择并递增 DATA_VERSION。顺序形成了清晰提交点:

复制代码
const saved = await this.viewModel.saveAnniversary(data);
const ver = AppStorage.get<number>(StateKeys.DATA_VERSION) ?? 0;
AppStorage.set<number>(StateKeys.DATA_VERSION, ver + 1);

页面输入清空和成功提示也位于持久化调用之后。若后续让 DataStore 返回失败结果,应把清空表单、成功提示和版本递增都放到成功分支,避免用户误以为数据已经保存。

八、删除事务:成功后发信号,再退出详情页

详情页删除链路是另一个关键场景:

复制代码
const deleted =
  await this.viewModel.deleteAnniversary(this.item.id);
if (deleted) {
  this.bumpDataVersion();
  this.pathStack.pop();
}

Repository 只有在找到目标 ID、从数组移除并完成 persist() 后才返回 true

复制代码
async delete(id: string): Promise<boolean> {
  await this.init();
  const index = this.items.findIndex((item) => item.id === id);
  if (index >= 0) {
    this.items.splice(index, 1);
    await this.persist();
    return true;
  }
  return false;
}

这个顺序保证返回列表之前已经发出变化信号。列表页面即使一直保留在导航栈中,也能通过版本观察重新读取,而不必依赖组件被销毁后再次进入。

需要注意当前 persist() 对底层写入失败没有向上传播,所以 delete()true 更准确地表示"内存中找到并执行了删除流程",不是可证明的磁盘提交成功。若要提升可靠性,仍应把 DataStore 的结果向 Repository 传播。

九、DATA_VERSION 是失效标记,不是业务版本

AppStore.bootstrap() 初始化:

复制代码
AppStorage.setOrCreate<number>(StateKeys.DATA_VERSION, 0);

首页、全部列表和筛选列表使用相同模式:

复制代码
@StorageLink(StateKeys.DATA_VERSION)
@Watch('onDataChange')
dataVersion: number = 0;

onDataChange(): void {
  this.loadData();
}

版本值本身没有业务含义,不需要持久化,也不要求等于修改次数。它只表示"你手里的页面快照可能过期,请重新读取"。这种设计比在多个页面之间传递新数组更简单,也避免每个写入页面逐一知道有哪些读页面。

版本号不是万能事件总线。若未来需要区分"纪念日变化""习惯变化""主题变化",可以拆成不同领域的 revision,或建立类型化的刷新服务。当前单一 DATA_VERSION 适合规模较小、重新读取成本较低的应用。

十、为什么 aboutToAppear 仍然要保留

页面既在 aboutToAppear() 加载,也监听版本变化:

复制代码
aboutToAppear(): void {
  this.loadData();
}

onDataChange(): void {
  this.loadData();
}

两条路径覆盖不同情况。首次进入页面时还没有发生版本变化,需要生命周期加载;页面已经存在于 Tab 或导航栈中时,其他页面保存和删除会触发版本刷新。两者合起来,才能覆盖"第一次打开"和"回来时已经过期"。

要防止快速连续写入造成旧请求覆盖新请求,可以给读取增加序号:

复制代码
private loadSeq: number = 0;

private async loadData(): Promise<void> {
  const seq = ++this.loadSeq;
  const items = await this.viewModel.loadAnniversaries();
  if (seq !== this.loadSeq) return;
  this.anniversaries = items;
}

当前 Repository 读取主要来自内存,竞争窗口较小;若后续改成数据库、文件或网络同步,这个防护会更有价值。

十一、习惯数据双 Key 合并是兼容逻辑

HabitService.list() 同时读取 HABITSHABIT_WALL,按 ID 合并,更新时间较新的记录获胜,然后把结果回写两个 Key:

复制代码
async list(): Promise<HabitItem[]> {
  const habits =
    await this.store.getJson<HabitItem[]>(DataKeys.HABITS, []);
  const wallHabits =
    await this.store.getJson<HabitItem[]>(DataKeys.HABIT_WALL, []);
  const merged = this.merge(habits, wallHabits);
  await this.persist(merged);
  return merged;
}

private async persist(habits: HabitItem[]): Promise<void> {
  await this.store.putJson(DataKeys.HABITS, habits);
  await this.store.putJson(DataKeys.HABIT_WALL, habits);
}

这说明真实项目里已经存在两个历史入口。合并并双写能保持兼容,但双写并非原子事务:第一个 Key 成功、第二个 Key 失败时会再次出现分叉。后续完成版本迁移后,最好选定唯一 Key,并记录迁移完成标记;若仍需双写,应让写入结果可观察,并定义失败后的重试或回滚策略。

十二、文件备份与 Preferences 不是同一条恢复链

BackupService 把结构化备份写入 context.filesDir,文件名含时间戳:

复制代码
const fileName = `backup_${Date.now()}.json`;
const filePath = `${this.baseDir}/${fileName}`;
const json = JSON.stringify(data, null, 2);
const file = fileIo.openSync(
  filePath,
  fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY
);
fileIo.writeSync(file.fd, json);
fileIo.closeSync(file);

导入时会读取文件并检查 versionanniversaries。这是一条真实的本地文件能力,但当前校验仍比较基础:没有限制文件大小,没有逐字段验证,也没有在本文所读源码中展示"导入后如何写回各 Repository 和 Preferences"的完整提交事务。

因此不能把"能解析备份文件"直接描述成"已经实现完整恢复"。可靠恢复至少需要:

  1. 校验文件大小、编码、版本和字段结构。
  2. 在内存外构建候选数据,不直接覆盖当前数据。
  3. 写入所有目标存储并检查结果。
  4. 更新 Repository 内存缓存。
  5. 递增对应 revision,让页面重新加载。
  6. 失败时保留原数据并给出明确提示。

十三、并发写入要避免读改写丢失

当前仓库单例把大部分纪念日修改串在同一对象上,降低了页面各自读写整个数组的风险。但 save()delete()togglePin() 仍是异步方法;用户快速连点或不同页面同时提交时,可能发生多次 persist() 交错。

可在 Repository 内建立简单写队列:

复制代码
private writeQueue: Promise<void> = Promise.resolve();

private enqueuePersist(): Promise<void> {
  this.writeQueue = this.writeQueue
    .catch(() => {})
    .then(() => this.store.putJson(
      DataKeys.ANNIVERSARIES,
      this.items
    ));
  return this.writeQueue;
}

同时,按钮应有 savingdeleting 状态,提交期间禁用重复点击。这样 UI 防重与数据层串行化形成两道保护。这里同样属于建议演进,当前源码尚未展示写队列。

十四、AppStorage 中不要放完整持久化数据

这套代码只把 DATA_VERSION、主题、安全区和导航栈放进 AppStorage,纪念日数组仍由 Repository 管理。这个边界是合理的:

  • AppStorage 适合跨组件共享的运行时状态和失效信号。
  • Preferences 适合轻量、可序列化、需要跨启动保存的数据。
  • Repository 负责业务规则与数据访问。
  • 页面只持有渲染快照和短生命周期输入。

如果把完整数组同时放进 AppStorage 和 Repository,就会出现双真源:某个页面只改共享数组,另一个服务只改仓库,持久化又写入第三份值。版本号模式的优点正是只传"变化发生了",不复制业务事实。

十五、错误处理要覆盖用户可见状态

当前 HomeView.loadData() 使用 finally 结束加载状态,DataStore 则在读取失败时返回默认值。这样页面不会一直转圈,但损坏数据和"确实没有数据"可能都表现为空列表。

更完整的页面状态应至少包括:

复制代码
type LoadState = 'loading' | 'content' | 'empty' | 'error';

@State loadState: LoadState = 'loading';
@State errorMessage: string = '';

Repository 可以返回结构化错误,页面据此显示"暂无内容"或"读取失败,请重试"。日志只记录 Key、阶段和错误类别,不应输出纪念日备注、日记正文、情侣空间内容或完整备份 JSON。

十六、验证矩阵:不要只验证"下次启动还在"

建议按下面矩阵验证项目实际支持的目标设备:

场景 操作 预期
冷启动 已有数据后强制结束并重启 列表与主题恢复,无首帧明显闪变
新增 新建纪念日并切回首页 首页和全部列表立即出现
编辑 详情修改标题并保存 当前详情更新,返回列表显示新标题
删除 详情删除后自动返回 原列表立即移除,无残留卡片
置顶 切换置顶状态 排序按 pinned 与日期重新计算
快速点击 连续点击保存或删除 只提交一次,不重复新增
数据损坏 写入非法 JSON 测试数据 不崩溃,有日志和可见错误或安全默认
旧结构 缺失新增字段的数据 经 normalize 后可显示
深浅色 切换系统模式并重启 主题状态与文字对比正常
小窗恢复 修改后切后台、调整窗口再返回 页面读取一致,无旧快照覆盖

验证时应同时观察 Preferences 中的值、Repository 返回值、DATA_VERSION 变化和页面最终渲染,不能只看 Toast。

十七、常见问题与定位

现象 可能原因 定位与修复
保存成功但返回仍是旧数据 未递增 DATA_VERSION,或观察页面未监听 检查提交顺序和 @Watch
删除后列表残留 pop() 后通知,或删除未成功 持久化成功后 bump,再返回
重启后数据消失 flush()、Key 不一致或初始化失败 核对存储名、DataKeys 与日志
首帧主题闪动 首帧使用默认值后再异步水合 仅对极小关键设置使用同步读取
非法 JSON 变空列表 catch 后返回默认值 增加错误状态、备份与校验
两个习惯入口数据不同 双 Key 写入部分失败 引入迁移标记和可观察写入结果
快速点击产生重复项 按钮未禁用、缺少写队列 增加 saving 状态和串行提交
导入后页面不刷新 只解析文件,未更新仓库和 revision 完成恢复事务后统一发通知

十八、上线前核对

  • Preferences 只保存轻量数据,未把大文件或可查询大集合塞入 JSON。
  • DataKeys 集中管理,旧 Key 有明确迁移和清理策略。
  • 保存、删除、置顶均在持久化完成后发出 revision。
  • 首页、全部列表和筛选列表覆盖首次加载与版本变化。
  • 写入失败不会显示成功,也不会清空用户输入。
  • JSON 数据有结构校验、默认字段和版本迁移。
  • 重复点击有禁用状态,数据层有必要的串行化保护。
  • 日志不包含备注、日记、情侣内容或完整备份正文。
  • 冷启动、后台恢复、横竖屏和小窗返回均完成实机验证。
  • 隐私说明与实际本地存储、备份文件行为保持一致。

十九、总结:一致性来自提交顺序与单一事实源

时光清单 的现有实现已经形成了清晰骨架:DataStore 封装 Preferences,Repository 管理内存事实和持久化,ViewModel 隔离页面与数据层,DATA_VERSION 让已有页面知道快照已过期,EntryAbility 对首帧关键设置做同步读取。保存和删除之所以能在返回后即时一致,不是因为 ArkUI 自动同步了数组,而是因为代码明确执行了"持久化完成、递增版本、观察页面重新读取"的顺序。

下一步优化也应沿着这条边界推进:让存储错误可向上传播,为 JSON 增加运行时校验与迁移,为并发写入增加串行化,为备份恢复补齐完整事务。不要再增加第二份业务真源,也不要让页面直接操作 Preferences。只要持久化事实唯一、提交边界明确、变化信号轻量,HarmonyOS 本地应用就能同时获得快速首帧、可靠保存和跨页面即时一致。


本文基于 D:\huawei\one8DataStore.etsAnniversaryRepository.etsAppStore.etsEntryAbility.etsAppViewModel.etsAddView.etsDetailView.etsHomeView.etsAllView.etsFilteredListView.etsHabitService.etsBackupService.ets 的真实源码复核。文中标注为"建议"或"演进"的代码未声称已经落地。

AI 辅助声明: 本文内容由作者结合真实项目源码整理,部分内容由 AI 辅助生成,并已进行人工核验与技术校正。

相关推荐
HarmonyOS_SDK8 小时前
焕新鸿蒙应用权限管理方案,应用授权体验再升级
harmonyos
黑臂麒麟2 天前
HarmonyOS鸿蒙实战应用7:随手账本——数据导出与分享
华为·app·arkts·鸿蒙
贾伟康2 天前
【天体运行模拟|11】HarmonyOS ArkTS 收藏与笔记实战:同步知识页和个人学习记录
harmonyos·arkts·preferences·多设备适配·arkdata
2501_919749032 天前
华为鸿蒙录音可视化APP—小羊声觉
华为·harmonyos·鸿蒙
大雷神2 天前
HarmonyOS AR Engine深度估计实战——把毫米距离画成实时热力图
华为·ar·harmonyos
GKxx2 天前
在 HarmonyOS 上给 QEMU 搭一个最小 aarch64 Linux guest(内核 + busybox initramfs)
linux·华为·qemu·harmonyos·鸿蒙·鸿蒙pc
黑臂麒麟2 天前
屏幕信息全解析:HarmonyOS的分辨率、刷新率、折叠状态一个API搞定
华为·arkts·鸿蒙
黑臂麒麟2 天前
HarmonyOS 网络连接诊断实战:检测网络状态、WiFi 切换、弱网监测一网打尽
网络·华为·arkts·鸿蒙
贾伟康2 天前
【天体运行模拟|08】HarmonyOS ArkTS 单位换算实战:处理天文尺度、科学计数与精度
harmonyos·arkts·arkui·数值精度·单位换算