xiaohack's Blog - HarmonyOS 6
type: channel
link: https://blog.xiaohack.org/tag/harmonyos-6/
items: 鸿蒙 HarmonyOS 6 | 逻辑核心 (02)：全局状态管理与数据持久化
items: 鸿蒙 HarmonyOS 6 | 逻辑核心 (01)：状态管理进阶 @ObservedV2 与 @Trace 深度监听复杂对象
items: HarmonyOS 6（API 21） 精准日程管理完整开发教程
items: 鸿蒙 HarmonyOS 6 | ArkUI (08)：弹窗与覆盖 CustomDialog、Toast 与 Popup
items: 鸿蒙 HarmonyOS 6 | ArkUI (07)：导航架构 Navigation 组件 (V2) 与路由栈管理最佳实践

鸿蒙 HarmonyOS 6 | 逻辑核心 (02)：全局状态管理与数据持久化
type: item
date: 2026-01-29T14:24:04+08:00
description: 前言在上一篇文章中，我们探讨了组件内部以及深层嵌套对象的状态管理。但当我们把视角拉高，俯瞰整个应用时，会发现一个新的挑战：数据孤岛。用户在“我的”页面修改了头像，首页的左上角是不是也得跟着变？用户在“设置”里开启了夜间模式，是不是所有的页面都要瞬间切换颜色？如果只靠组件之间的父子传递（Props），我们需要把这些状态一路从根节点透传下去，这种属性钻取简直是代码维护的噩梦。更棘手的是，当用户划掉后台进程，杀掉应用，再次打开时，我们希望他上次设置的“夜间模式”依然生效。这就涉及到了内存数据与磁盘数据的同步问题。在鸿蒙 HarmonyOS 6 (API 20) 中，ArkUI 给出了一套完整的应用级状态管理方案：AppStorage全局内存、LocalStorage页面级内存以及 PersistentStorage持久化存储。今天，我们就来聊聊如何打通这三条数据流，构建一个数据互通且能记住用户习惯的应用。这份优化后的文案去除了口语化的比喻和非必要的符号，采用了更专业、精准的技术语言，强调了核心概念与运行机制。一、 AppStorage 应用内存状态的全局管理AppStorage 是应用在内存中的全局状态容器。一旦在其中存入属性，应用内的任意 Ability、Page 或自定义组件均可访问和修改。在声明式 UI 开发中，不建议在 build 函数中频繁直接调用 API。ArkUI 提供了两个专用的装饰器：@StorageLink：建立双向同步。组件内状态的修改会同步至全局，并驱动其他订阅该状态的组件更新。@StorageProp：建立单向订阅。仅接收全局状态的更新，组件内的修改不会同步回全局。例如，将“当前用户 Token”存储在 AppStorage 中。在任意页面声明 @StorageLink('userToken')，该变量即可与全局状态联通。无论何处更新了 Token，所有页面的对应变量均会实时刷新。二、 PersistentStorage 持久化数据同步AppStorage 仅存在于内存中，应用退出后数据会被清除。PersistentStorage 的作用是将 AppStorage 中的特定属性写入磁盘文件，实现持久化。PersistentStorage 并非独立的数据库，而是 AppStorage 与文件系统之间的同步机制。通常在应用启动早期（如 EntryAbility 或页面加载前）调用 PersistentStorage.persistProp('key', defaultValue)。系统会检查磁盘中是否存在该键值：若存在，则读取并覆盖 AppStorage 中的值；若不存在，则使用默认值初始化，并建立磁盘映射。连接建立后，开发者只需通过 @StorageLink 修改 AppStorage 中的数据，框架会自动监听变化并将新值写入磁盘，无需手动处理文件读写或 JSON 序列化。三、 LocalStorage 模块化状态隔离与全局的 AppStorage 不同，LocalStorage 用于解决模块化和多实例场景下的状态共享问题（如多窗口应用或独立模块）。LocalStorage 的生命周期绑定在 Ability 或 UIAbility 上下文中，创建了一个隔离的状态容器。在页面加载时，可传入独立的 LocalStorage 实例，组件内部则使用 @LocalStorageLink 进行对接。这种机制能有效保证状态的封装性与安全性，防止因全局变量过多导致的状态污染。四、 最佳实践 全局主题切换功能实现以下通过“全局主题切换”功能（支持深色模式与字体大小调整）演示三者的协同工作。需求包括：设置在所有页面生效，且应用重启后配置依然保留。实现时需遵循严格的初始化顺序：先持久化，再 UI 绑定。务必在 Ability 或文件顶层执行 PersistentStorage.persistProp，切勿在组件的 build 函数中执行持久化初始化，以免引发逻辑错误。import { promptAction } from '@kit.ArkUI'; // ============================================================= // 1. 初始化持久化数据 (必须在 @Entry 之前执行) // ============================================================= // 这里的逻辑是： // 1. 检查磁盘是否有 'appThemeMode'。 // 2. 如果有，将其加载到 AppStorage 中。 // 3. 如果没有，则使用默认值 'light' 初始化 AppStorage，并写入磁盘。 PersistentSto…

鸿蒙 HarmonyOS 6 | 逻辑核心 (01)：状态管理进阶 @ObservedV2 与 @Trace 深度监听复杂对象
type: item
date: 2026-01-28T12:03:30+08:00
description: 前言在鸿蒙应用的开发过程中，状态管理一直是我们绕不开的话题。如果你是从 API 9 或 API 10 一路走来的老兵，一定经历过被 @Observed 和 @ObjectLink 支配的恐惧。那时候，我们想要监听一个嵌套在对象深处的属性变化，简直就是一场噩梦。假设你有一个 User 对象，里面包含一个 Address 对象，当你试图修改 user.address.city 时，你会发现界面纹丝不动。为了解决这个问题，我们被迫把 Address 拆分成一个独立的子组件，或者暴力地重新赋值整个 Address 对象来触发更新。这种为了技术限制而通过增加组件层级来妥协的做法，不仅让代码变得臃肿，更带来了不必要的性能开销。在 HarmonyOS 6 (API 20) 中，ArkUI 团队终于为我们带来了状态管理的 V2 版本，其中 @ObservedV2 和 @Trace 的出现，彻底粉碎了嵌套对象监听的痛点，让我们终于可以像写原生 JS 一样自然地操作数据了。一、 告别 V1 时代的“洋葱式”更新在深入 V2 之前，我们有必要回顾一下 V1 版本状态管理的局限性，这样你才能深刻体会到新特性的甜头。在 V1 中，状态管理的粒度通常停留在 对象引用 级别。这意味着，框架只关心这个对象是不是原来那个对象，或者这个对象的一级属性有没有变。一旦数据结构变得立体，比如数组里套对象，对象里又套对象，框架的感知能力就会断崖式下跌。为了让 UI 响应深层数据的变化，我们过去不得不构建一种 洋葱式 的组件结构。父组件持有 User，子组件持有 Address，孙子组件持有 Street。每一层都必须严格使用 @ObjectLink 进行传递。这导致了一个后果：哪怕是一个简单的表单页，可能都需要拆分成七八个细碎的自定义组件。这不仅增加了代码的复杂度，还让组件之间的通信变得异常繁琐。而如果我们偷懒不拆组件，就只能通过 this.user.address = new Address(...) 这种“换血”的方式来强制刷新，这无疑是在用大炮打蚊子，性能损耗极大。二、 @ObservedV2 与 @Trace 的精准打击HarmonyOS 6 引入的 @ObservedV2 和 @Trace，采用了全新的代理（Proxy）机制，将监听的粒度精确到了 属性 级别。这就像是给每一个需要关注的数据字段都安装了一个微型的传感器，无论它被嵌套得有多深，只要数值发生变化，传感器就会立即向 UI 发送更新信号。使用这套新机制非常直观。首先，我们需要用 @ObservedV2 类装饰器来标记一个类，告诉框架：这个类产生的实例是需要被深度观察的。接着，对于类中那些会影响 UI 显示的核心属性，我们给它们加上 @Trace 装饰器。注意，这里有一个巨大的思维转变。我们不再需要把所有属性都变成状态，只有那些真正和界面绑定、变化时需要触发重绘的属性，才需要加 @Trace。这种按需监听的设计，从根源上减少了不必要的渲染消耗。我们可以看看下面这段定义代码，它展示了如何构建一个可深度监听的数据模型.// 定义一个深层嵌套的设置类 @ObservedV2 class Settings { @Trace theme: string = 'Light'; @Trace fontSize: number = 14; constructor(theme: string, fontSize: number) { this.theme = theme; this.fontSize = fontSize; } } // 定义用户类，嵌套了 Settings 类 @ObservedV2 class User { @Trace name: string; @Trace age: number; // 嵌套的复杂对象，只要 Settings 类被正确装饰，这里无需特殊处理 @Trace settings: Settings; constructor(name: string, age: number, settings: Settings) { this.name = name; this.age = age; this.settings = settings; } }在上面的代码中，不管是 User 还是嵌套在内部的 Settings，都被标记为了 V2 的观察对象。当你在组件中直接执行 this.user.settings.theme = 'Dark' 时，ArkUI 能够精准地捕获到这个深层属性的变化，并只更新依赖了 theme 属性的那一部分 UI，而不会导致整个 User 卡片甚至整个页面的重绘。三、 数组与集合的深度监听除了对象嵌套，数组操作也是 V1 版本的一大痛点。以前我们必须使用 ArkUI …

HarmonyOS 6（API 21） 精准日程管理完整开发教程
type: item
date: 2026-01-28T09:28:56+08:00
description: Hello，大家好，我是 V 哥。AI 智能体在2026年V 哥相信一定翻天覆地的变化，一大波企业和开发者纷纷涌入这个赛道，什么超级个体、一人公司、为企业节省几百万人力成本等等话题在网络上持续发酵，作为程序员的我们，如果还在观望，那等来就一定是被市场淘汰。我经常跟同学们说，程序员最大的优势是啥？就是不断持续学习的超强能力！干掉程序员的只会是程序员自己，未来的程序员不只是程序员，而是主导技术变现的超级魔术师。今天的内容，V 哥带大家一起来玩一玩，在鸿蒙6系统中，如何完成精准日程管理的完整案例开发。一、项目概述功能特性✅ 日程增删改查（支持标题、备注、时间、重复）✅ 后台精准提醒（应用关闭/重启后依然准时）✅ 智能提前提醒（5/10/30/60分钟）✅ 重复提醒（每天/每周/每月）✅ 自定义铃声+震动✅ 点击通知跳转详情技术方案┌─────────────────────────────────────────────────────────┐ │ 精准日程提醒架构 │ ├─────────────────────────────────────────────────────────┤ │ UI层 │ ArkUI 声明式UI │ ├─────────────────────────────────────────────────────────┤ │ 数据层 │ @ohos.data.relationalStore (关系型DB) │ ├─────────────────────────────────────────────────────────┤ │ 提醒层 │ @ohos.reminderAgentManager (代理提醒) │ ├─────────────────────────────────────────────────────────┤ │ 通知层 │ @ohos.notificationManager │ └─────────────────────────────────────────────────────────┘二、项目创建与配置步骤1：创建项目DevEco Studio → File → New → Create Project → 选择 "Empty Ability" → Project name: ScheduleManager → Bundle name: com.example.schedulemanager → Compile SDK: 5.0.0(API 12) 或更高 → Model: Stage步骤2：配置 module.json5{ "module": { "name": "entry", "type": "entry", "description": "$string:module_desc", "mainElement": "EntryAbility", "deviceTypes": ["phone", "tablet"], "deliveryWithInstall": true, "installationFree": false, "pages": "$profile:main_pages", "abilities": [ { "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "description": "$string:EntryAbility_desc", "icon": "$media:icon", "label": "$string:EntryAbility_label", "startWindowIcon": "$media:startIcon", "startWindowBackground": "$color:start_window_background", "exported": true, "skills": [ { "entities": ["entity.system.home"], "actions": ["action.system.home"] } ] } ], "requestPermissions": [ { "name": "ohos.permission.PUBLISH_AGENT_REMINDER", "reason": "$string:reminder_reason", "usedScene": { "abilities": ["EntryAbility"], "when": "always" } }, { "name": "ohos.permission.NOTIFICATION_CONTROLLER", "reason":…

鸿蒙 HarmonyOS 6 | ArkUI (08)：弹窗与覆盖 CustomDialog、Toast 与 Popup
type: item
date: 2026-01-21T10:26:07+08:00
description: 前言在一个优秀的应用设计中，界面不仅仅是平铺直叙的展示，更需要有层级感。当用户点击删除按钮时，我们需要一个确认框来防止误触；当后台数据加载完成时，我们需要一个轻量的提示告诉用户 好了 ；当用户对某个晦涩的功能图标感到困惑时，我们需要一个气泡弹窗来解释它的含义。这些浮在主界面之上的交互层，我们统称为 覆盖物（Overlays）。在早期的开发中，很多工程师习惯直接使用系统原生的 AlertDialog，那种灰底黑字的弹窗虽然功能健全，但在如今这个颜值为王的时代，它打断了用户的情绪流，也破坏了应用的整体设计语言。在鸿蒙 HarmonyOS 6 中，ArkUI 为我们提供了极其强大的弹窗定制能力。无论是转瞬即逝的 Toast，还是完全自定义的 CustomDialog，亦或是指向性明确的 Popup 气泡，我们都可以像搭积木一样，用声明式的代码构建出既美观又灵动的交互体验。一、 轻量级反馈与上下文气泡在进入复杂的弹窗之前，我们先解决最基础的反馈需求。当用户复制了一段文本，或者刷新列表成功时，我们不需要让用户进行任何操作，只需要给出一个朕已阅的信号。这就是 Toast。在 API 20 中，系统将这类交互统一收敛到了 promptAction 模块下。我们不再像以前那样去寻找 Window 实例，而是直接调用 promptAction.showToast。这个 API 非常纯粹，它接受一个显示时长、一条消息文本，以及一个可选的位置参数。但在实战中，建议尽量保持 Toast 的简洁，不要试图在里面塞入过多的文字。它应该像一阵风，来过，被看到，然后消失。如果说 Toast 是全局的广播，那么 Popup 气泡就是点对点的悄悄话。CustomDialog 是一种模态交互，它会给背景加上遮罩，强迫用户聚焦。但有时候，我们并不想打断用户的操作流，只是想对界面上的某个元素做一点补充说明。比如一个帮助的小问号图标，或者一个“新功能”的引导提示。这时候，ArkUI 提供的 bindPopup 属性是最优雅的选择。这意味着任何组件——一个按钮、一张图片甚至一段文字，都可以绑定一个气泡。系统会自动计算目标组件在屏幕上的位置，然后决定气泡是出现在上方、下方还是侧边，并自动生成一个小箭头指向目标。我们作为开发者，几乎不需要关心坐标计算的问题，只需要关注气泡里的内容构建即可。@Entry @Component struct PopupExample { // 控制气泡显示的开关状态 @State showPopup: boolean = false; // 定义气泡内部的 UI 结构 @Builder PopupContent() { Column() { Text('功能说明') .fontSize(14) .fontWeight(FontWeight.Bold) .fontColor(Color.White) .margin({ bottom: 4 }) Text('这里是详细的补充文案，系统会自动根据位置计算箭头指向。') .fontSize(12) .fontColor('#E6E6E6') } .padding(12) .backgroundColor('#4D4D4D') // 气泡背景通常与文字反色 .borderRadius(8) } build() { Column() { // 任何组件都可以绑定气泡，这里以一个问号图标为例 SymbolGlyph($r('sys.symbol.questionmark_circle')) .fontSize(24) .fontColor($r('sys.color.ohos_id_color_text_secondary')) // 1. 点击切换状态 .onClick(() => { this.showPopup = !this.showPopup; }) // 2. 绑定气泡属性 .bindPopup(this.showPopup, { builder: this.PopupContent, // 指向内容构建器 placement: Placement.Bottom, // 优先显示位置（系统会自动调整） mask: false, // false 表示非模态，不阻断用户操作其他区域 enableArrow: true, // 显示指向目标的小箭头 popupColor: '#4D4D4D', // 气泡背景色（需与 Builder 背景一致或透明） onStateChange: (e) => { // 3. 状态同步：当点击空白处气泡消失时，同步更新 boolean 变量 if (!e.isVisible) { this.showPopup = false; } } }) } .width('100…

鸿蒙 HarmonyOS 6 | ArkUI (07)：导航架构 Navigation 组件 (V2) 与路由栈管理最佳实践
type: item
date: 2026-01-20T11:29:19+08:00
description: 前言在鸿蒙应用的开发历程中，页面跳转一直是大家最先接触的功能之一。很长一段时间里，Router 模块都是我们手中的标配武器，那句 router.pushUrl 相信每一位开发者都烂熟于心。但在构建大型应用，尤其是面对平板、折叠屏这些复杂设备时，老旧的 Router 逐渐显露出了疲态。它是一个页面级别的全局单例，难以处理分屏、弹窗嵌套路由以及模块化的动态加载。这就像是用一把瑞士军刀去砍伐整片森林，虽然能用，但效率极低且手感生涩。在 HarmonyOS 6 的时代，官方明确推荐我们全面拥抱 Navigation 组件。这不仅仅是一个组件的更替，更是一次架构思维的升级。Navigation 不再是一个简单的 API 调用，它是一个容器，一个能够容纳完整路由栈、标题栏和工具栏的超级容器。它将路由的管理权从系统底层交还到了开发者手中，让我们能够像操作数组一样精准地控制页面的进出栈。今天，我们就把那个陈旧的 Router 放在一边，深入探讨如何利用 Navigation V2 架构和 NavPathStack 构建一个现代化、健壮的应用导航体系。一、 从 Router 到 Navigation：架构的范式转移要理解 Navigation 的强大，我们先得明白它解决了什么痛点。传统的 Router 是基于 Page（页面）的，每一个页面都是一个独立的 Ability 或者窗口层级。当我们想要在一个弹窗里再做一套局部导航，或者在平板的左侧菜单里嵌入一个独立的路由栈时，Router 就束手无策了。Navigation 组件的出现彻底改变了这一局面。它本质上是一个 UI 组件，这意味着它可以被放置在界面的任何位置。你可以把它放在根节点作为全屏导航，也可以把它放在一个 Dialog 内部，甚至可以嵌套使用。在 API 20 中，Navigation 采用了 组件级路由 的概念。每一个“页面”不再是 @Entry 修饰的独立文件，而是被 NavDestination 包裹的自定义组件。这种设计让页面变得极其轻量，页面的切换本质上就是组件的挂载与卸载，性能得到了巨大的提升。更重要的是，它配合 NavPathStack 实现了路由栈的可编程化，我们终于可以像操作数据一样去操作界面了。二、 核心大脑：NavPathStack 路由栈管理如果说 Navigation 是躯壳，那么 NavPathStack 就是它的灵魂。在 V2 版本中，我们不再直接调用组件的方法来跳转，而是创建一个 NavPathStack 的实例，并将其绑定到 Navigation 组件的 pathStack 属性上。这个栈对象就是我们操控界面的遥控器。你需要实现一个复杂的登录流程：用户点击购买 -> 跳转登录 -> 跳转注册 -> 注册成功 -> 直接返回购买页（跳过登录页）。在旧的 Router 模式下，你需要计算 delta 索引或者使用 replace 模式小心翼翼地堆叠。而在 NavPathStack 中，就方便多了。你可以随时调用 popToName 直接回到指定的路由锚点，或者操作栈数组，精准地移除中间的某几个页面。数据的传递也变得优雅。当我们调用 pushPath 时，可以直接传入一个 param 对象。而在目标页面中，我们不需要再写繁琐的 router.getParams()，而是直接在 NavDestination 的 onShown 生命周期或者组件初始化时，从栈中获取参数。这种参数传递是类型安全的，且完全受控。此外，NavPathStack 还提供了强大的拦截器机制（Interception），让我们可以在路由跳转发生前进行鉴权拦截，比如用户未登录时直接重定向到登录页，这一切都在路由层面被优雅地拦截处理了。三、 页面构造：NavDestination 与路由表设计在 Navigation 架构下，我们的一级页面（根页面）通常直接写在 Navigation 的闭包里，而二级、三级页面则通过 NavDestination 来定义。这里有一个关键的概念转变：我们需要构建一个 路由映射表。我们不再是通过文件路径去跳转，而是通过 路由名称（Name）。我们需要在 Navigation 组件中配置 navDestination 属性，它接收一个 @Builder 构建函数。当 NavPathStack 请求跳转到 "DetailPage" 时，这个构建函数就会被触发，我们需要在这个函数里根据传入的 name 返回对应的 NavDestination 包裹的组件。这种设计模式天然支持模块化开发。我们可以把不同模块的路由表分散在各自的 HAR 包中，最后在主工程中进行聚合。每个 NavDestination 都是一个独立的沙箱，它拥有自己的标题栏、菜单栏和生命周期（onShown…