HarmonyOS 入门实战:从 ArkUI 页面、Navigation 路由到 ArkData 存储

摘要

本文面向刚开始使用 ArkTS 和 ArkUI 编写 Stage 模型应用的开发者。文章以项目中的计算器页面为例,先解释工程目录、UIAbility 和声明式 UI,再重点拆解 Navigation 页面路由、组件通信与 @kit.ArkData 数据存储。每个概念后都给出项目中的对应位置,让读者能从 API 含义走到一份可构建的实现。

先建立一张运行地图

这个项目的启动、交互和数据保存可以画成 4 层:

[EntryAbility]
      |
      | loadContent("pages/index")
      v
[Index 页面] -- NavPathStack --> [historyPage]
      |
      | @StorageLink("precess")
      v
[KeyButton 状态与计算逻辑]
      |
      | Preferences.put / get
      v
[应用沙箱中的 History 数据]

这 4 层分别回答 4 个入门问题:应用由谁启动,页面如何跳转,组件如何共享状态,数据如何跨重启保存。项目对应文件是 EntryAbility.etsindex.etshistory.etshandle.ets

一、工程目录:先知道代码应该放在哪里

Stage 模型项目通常按「应用、模块、源码、资源」组织。当前工程只有一个 entry 模块,日常开发最常进入的是 entry/src/main

machine/
|-- AppScope/                     # 整个应用共用的配置和资源
|   |-- app.json5                 # 包名、版本、应用图标
|   `-- resources/                # 应用级资源
|-- entry/                        # 一个业务模块
|   |-- build-profile.json5       # 模块构建方式和目标
|   `-- src/main/
|       |-- ets/
|       |   |-- entryability/     # UIAbility 实现
|       |   |-- entrybackupability/
|       |   `-- pages/            # 页面、组件和当前项目的业务类
|       |-- resources/            # 模块使用的图片、颜色、字符串、profile
|       `-- module.json5          # Ability、设备类型、skills、页面清单
|-- build-profile.json5           # 产品、SDK 和模块列表
`-- oh-package.json5              # 工程依赖

文件范围: AppScope/app.json5:1-10entry/src/main/module.json5:1-50build-profile.json5:1-42

作用: 目录树区分应用级配置、模块级配置、ArkTS 源码和资源。

原因: 页面增加后,如果所有类都堆在 pages,路由、数据访问和业务逻辑会互相引用。目录先按职责拆开,后续新增页面时只需判断代码属于 UI、状态、数据还是公共工具。

输入与输出: DevEco Studio 从这些目录读取 ArkTS、JSON5 和资源文件,构建出可安装模块。

注意点: resources 中的文件通过 $r('app.media.xxx')$r('app.string.xxx') 等资源引用访问。文件名和资源名改动后,ArkTS 中的引用也要同步更新。

实际项目常用的源码目录

当页面从 2 个增长到十几个时,可以在 entry/src/main/ets 下按职责增加目录。HarmonyOS 不强制下面的名称,但这种分法便于查找和复用。

目录放置内容计算器中的例子
pages/页面入口和 NavDestination 页面index.etshistory.ets
components/多个页面复用的 ArkUI 组件可复用的圆形计算器按键
viewmodel/页面状态、事件处理和展示逻辑KeyButton 的输入状态与预览逻辑
model/领域对象和数据结构HistoryItem、按键配置类型
repository/Preferences、RDB、网络等数据访问历史记录的保存与读取
utils/无页面状态的通用函数数字格式化、表达式 token 辅助函数
entryability/应用窗口入口EntryAbility.ets
common/常量、枚举、主题和通用类型路由名称、颜色与尺寸常量

当前项目把 KeyButton 放在 pages/handle.ets。理解入门机制时这样更容易追踪;业务继续增加后,可以把它拆到 viewmodel/,把 Preferences 操作拆到 repository/

5 个常用配置文件

AppScope/app.json5 管整个应用。当前文件声明包名 com.example.machine 和版本 1.0.0。应用名称、图标和版本升级通常从这里修改。

根目录 build-profile.json5:6-16 管产品和 SDK。当前 targetSdkVersioncompatibleSdkVersion 都是 6.1.0(23);同文件 28-40entry 加入构建。

entry/build-profile.json5:2 把模块声明为 stageMode,并在 targets 中提供 defaultohosTest。页面代码使用 Stage 模型的 UIAbilityContextWant

entry/src/main/module.json5 管模块能力。它声明 entry 模块、手机设备、EntryAbility、备份扩展和页面清单入口。新增 Ability 或修改桌面启动方式时,主要编辑这个文件。

entry/src/main/resources/base/profile/main_pages.json 管可由 loadContent() 加载的页面。当前注册 pages/indexpages/testhistoryPageNavigation 内部目的页,不需要作为窗口首屏注册。

一个模块配置多个 UIAbility

当应用需要两个独立窗口入口,例如「计算器」和「设置中心」,需要完成两步:先创建第二个 UIAbility 类,再在 module.json5abilities 数组中注册。只创建 ArkTS 文件,系统不会自动发现它。

示例文件: entry/src/main/module.json5abilities 节点。

"abilities": [
  {
    "name": "EntryAbility",
    "srcEntry": "./ets/entryability/EntryAbility.ets",
    "exported": true,
    "skills": [{
      "entities": ["entity.system.home"],
      "actions": ["ohos.want.action.home"]
    }]
  },
  {
    "name": "SettingsAbility",
    "srcEntry": "./ets/settingsability/SettingsAbility.ets",
    "exported": false
  }
]

作用: 配置把 SettingsAbility 注册为同一模块中的第二个 UIAbility。

原因: UIAbility 是系统调度单元,系统需要从 module.json5 得到类路径、导出规则和启动匹配规则。

输入与输出: name 是 Ability 名称,srcEntry 指向 ArkTS 类;配置生效后,可以通过显式 Want 启动它。

注意点: 只有需要被其他应用启动的 Ability 才应设置 exported: true。内部设置页通常用 false,并且多数普通页面使用 Navigation 即可,不必每页创建一个 Ability。

二、UIAbility、页面和组件的生命周期

EntryAbility 继承自 UIAbility。它负责应用窗口,不负责直接绘制每一个按钮。页面中的 @Component 负责声明组件树。

UIAbility.onCreate(want, launchParam)
        |
        v
onWindowStageCreate(windowStage)
        |
        +--> 配置主窗口
        `--> loadContent("pages/index")
                      |
                      v
               Index.aboutToAppear()

进入后台:onBackground()
回到前台:onForeground()
页面离开:Index.aboutToDisappear()
窗口销毁:onWindowStageDestroy() -> onDestroy()

EntryAbility.ets:9-16onCreate() 设置应用颜色模式并记录日志。它拿到的 want 描述本次启动请求,launchParam 描述启动原因和模式。

EntryAbility.ets:22-45onWindowStageCreate() 获取主窗口,设置全屏并隐藏系统栏,然后调用 windowStage.loadContent('pages/index')。页面代码只有在窗口舞台创建后才能装载。

index.ets:84-86aboutToAppear() 中读取历史记录。这个选择把读取时机放在首页即将显示时。index.ets:160-173aboutToDisappear() 清理动画和光标计时器,页面离开后不会继续执行旧回调。

Want:Ability 之间的启动与传参

显式 Want 直接指定目标应用、模块和 Ability;隐式 Want 提供 action、entities 或 URI,由系统按 module.json5 中的 skills 匹配目标。

如果前面的 SettingsAbility 与当前应用同包,显式启动的关键字段如下:

示例位置: 任意持有 UIAbilityContext 的页面或组件。

import { common } from '@kit.AbilityKit'

const context = getContext(this) as common.UIAbilityContext
context.startAbility({
  bundleName: 'com.example.machine',
  moduleName: 'entry',
  abilityName: 'SettingsAbility',
  parameters: { source: 'calculator' }
})

作用: 代码启动同一应用中的 SettingsAbility,并附带来源参数。

原因: Ability 是系统级入口,必须通过 Context 请求系统调度。页面栈内部跳转则交给 Navigation

输入与输出: 输入是 Want 对象;startAbility() 返回 Promise,失败时会抛出 BusinessError。

注意点: abilityName 必须和 module.json5 中的 name 一致。页面内导航不要使用 Want,否则会把普通页面切换升级为新的 Ability 生命周期。

三、ArkUI 声明式开发:状态决定页面

ArkUI 的声明式开发可以概括为「读取状态,描述组件树」。开发者修改状态,框架更新引用该状态的组件属性。项目中按钮按下时,代码修改颜色、透明度和缩放状态;Button 的属性绑定这些值,因此界面随状态刷新。

index.ets:259-329 的页面由 NavigationColumnRowGridButtonTextInputTextImage 组成。Column 纵向组织菜单、显示区和键盘;Grid 使用 4 列 5 行放置 20 个按键;TextInput 显示表达式,Text 显示实时结果。

入门阶段常见装饰器

装饰器数据归属修改后是否更新 UI当前项目位置
@Entry页面入口标记不保存数据index.ets:57
@Component自定义组件标记不保存数据index.ets:58history.ets:3
@State当前组件私有路由栈、按钮动画、光标状态
@Observed可观察类实例是,需配合观察引用handle.ets:10-11KeyButton
@StorageLinkAppStorage 中的共享键是,双向同步首页与历史页的 precess
@Builder一段 UI 构建逻辑读取其依赖状态菜单、按键、目的页映射

@BuilderParam 适合把一段 UI 构建逻辑传给子组件,例如把不同图标传给统一的按键外壳。wrapBuilder 可以把全局 Builder 包成可传递对象。@Styles 用于复用宽高、圆角、背景色等通用样式。当前项目用局部 @Builder calculatorKey() 统一生成 20 个按键,已经覆盖最直接的重复部分。

用 ArkUI Inspector 对照声明和运行结果

ArkUI Inspector 展示设备上的组件树与运行时属性。计算器页面可以从 4 个值开始检查:Grid 是 4 列 5 行,按钮是 80 × 80 vp,按钮圆角是 40 vp,显示区高度是 150 vp。这些声明分别位于 index.ets:241-251278-309311-320

Inspector 还能确认 TextInput 是否获得焦点、软键盘是否隐藏、GridItem 是否占据预期轨道。布局异常时,先定位运行时尺寸,再回到对应 ArkTS 属性,比只看截图更容易找到约束来源。

四、Navigation 页面路由:一个页面栈如何工作

普通应用通常只有少数 UIAbility,却包含许多页面。Navigation 管理同一 Ability 窗口中的页面跳转,NavDestination 描述目的页,NavPathStack 保存访问历史。

Navigation                 页面容器
    |
    +-- 首页内容            当前根页面
    |
    `-- NavPathStack
          |-- detail       第一次 push
          |-- settings     第二次 push
          `-- about        当前栈顶

pop() 后:detail -> settings
clear() 后:回到 Navigation 根页面

1. Navigation 是容器

项目在 index.ets:260 创建 Navigation(this.pathStack)。传入的 pathStack 是页面栈控制器。Navigation 花括号中的 Column 是根页面内容,历史记录页不会替换这个组件定义,而是作为新的 NavDestination 压到栈顶。

Navigation 还可以管理标题栏、工具栏、分栏模式和返回行为。项目调用 .hideToolBar(true) 隐藏工具栏,并通过 .navDestination(this.pageMap) 注册目的页构建函数。

2. NavPathStack 保存路由状态

index.ets:60 创建 @State pathStack: NavPathStack = new NavPathStack()。把它放在 @State 中,可以让页面持有同一个栈实例,并在组件内调用栈操作。

常用方法可以按动作记忆:

方法栈变化适用场景
pushPathByName(name, param)在顶部增加一个目的页从列表进入详情
pushDestinationByName(name, param)增加页面并通过 Promise 报告构建错误需要捕获路由失败
replacePathByName(name, param)用新页面替换当前顶部登录完成后进入首页
pop()移除顶部页面返回上一页
pop(result)返回并把结果交给上页回调选择器返回所选值
removeByName(name)删除指定名称的页面清理重复中间页
clear()清空目的页栈退出业务流程回到根页
getAllPathName()读取当前所有页面名称调试页面栈

栈操作比「显示或隐藏一个组件」多了历史语义。连续进入详情、设置和关于页后,系统返回键可以按相反顺序退出;布尔状态需要开发者自己重建这套顺序。

3. navDestination 把名称映射到组件

项目的路由名称是 detail。点击菜单中的「历史」后,index.ets:207 调用 pushPathByName('detail', '')pageMap() 收到名称后构建 historyPage()

文件: entry/src/main/ets/pages/index.ets:77-82、205-208、259-329

@Builder
pageMap(name: string) {
  if (name === 'detail') {
    historyPage()
  }
}

// 菜单点击时
this.pathStack.pushPathByName('detail', '')

// Navigation 属性
.navDestination(this.pageMap)

作用: 路由名 detail 被压入栈后,Builder 返回历史记录页面。

原因: 路由控制与目的页构建分离。点击处只表达「去 detail」,页面定义仍由 pageMap() 管理。

输入与输出: pushPathByName() 输入名称和参数;pageMap() 输入名称并构建对应组件。

注意点: 名称是普通字符串,拼写错误不会被类型系统提前发现。实际项目可在 common/RouteNames.ets 中集中声明路由常量。

4. NavDestination 是目的页外壳

history.ets:8-40 使用 NavDestination() 包住 List,并设置标题「历史记录」。NavDestination 会接入 Navigation 的标题栏、返回行为和页面生命周期。

目的页适合承载完整页面。可复用的小区域仍应写成 @Component,由页面组合。把每个按钮或列表项都做成 NavDestination 会让组件复用和页面栈语义混在一起。

5. 页面参数与返回结果

pushPathByName() 的第二个参数可以传对象,例如 { recordId: 42 }。目的页可以通过当前 NavPathStack 或页面上下文读取参数。返回时调用 pop({ selectedId: 42 }),上页注册的 onPop 回调接收结果。

参数适合携带页面定位信息,例如记录 ID、筛选条件或展示模式。多个页面共同维护并实时变化的数据,更适合 AppStorage、LocalStorage 或 ViewModel;把整个可变业务对象复制进路由参数,容易出现两份状态。

6. Navigation、router 和 Want 的选择

需求推荐入口生命周期边界
同一页面栈内前进、返回、传参Navigation + NavPathStack同一 UIAbility
维护旧项目中的简单页面路由router同一 UIAbility,能力较基础
启动另一个 UIAbilityUIAbilityContext.startAbility(Want)新的或复用的 Ability 实例
让系统按 action/URI 匹配目标隐式 Want由系统解析 skills

当前项目从计算器首页进入历史页,Navigation 正好覆盖「同一窗口内前进和返回」。只有历史功能需要独立窗口、独立任务或被外部应用启动时,才需要单独的 UIAbility。

五、组件通信:先判断数据归谁

组件通信容易混乱,根源通常是数据所有者没有确定。先回答「谁创建数据,谁能修改,数据要活多久」,再选择装饰器或事件机制。

父组件私有状态
   |-- 值下发 --------> 子组件       @Prop
   |-- 双向引用 ------> 子组件       @Link
   |-- 跨层提供 ------> 后代组件     @Provide / @Consume
   `-- 应用级共享 ----> 多个页面     AppStorage / @StorageLink

一次性动作通知:EventHub
页面定位信息:Navigation 参数
跨重启数据:Preferences / RDB

父子组件:参数下发与事件回传

父组件拥有状态时,可以用 @Prop 向子组件传只读值。子组件需要修改父状态时,可以用 @Link 建立双向引用,或者由父组件传入回调,让子组件报告「点击了删除」这类事件。

计算器的圆形按键如果拆成独立组件,按键文字、颜色和无障碍说明适合由父组件下发;点击事件通过回调返回按键值。这样子组件负责外观,Index 继续负责把输入交给 KeyButton.baseClick()

跨层组件:Provide 与 Consume

当祖先组件和深层后代之间隔着多层布局,逐层传递同一个主题或配置会增加中间组件参数。@Provide 在祖先提供数据,@Consume 在后代读取或修改。

这组装饰器适合页面树内部的主题、编辑上下文和表单状态。它的可见范围跟随组件树,不用于跨应用重启保存数据。

AppStorage 与 StorageLink:项目正在使用的共享方式

AppStorage 是应用进程中的可观察键值状态。@StorageLink('precess') 与指定键双向同步:任一绑定方修改对象,其他绑定方可以看到变化。

项目在 index.ets:67history.ets:5 都声明:

文件: entry/src/main/ets/pages/index.ets:67history.ets:5

@StorageLink('precess') precess: KeyButton = new KeyButton()

作用: 首页和历史页连接同一个 KeyButton 状态,其中包含表达式、结果和 historyList

原因: 两个页面属于不同组件,历史页又需要读取首页计算后产生的记录。应用级共享键避免逐层传参。

输入与输出: 输入是 AppStorage 键 precess 和默认对象;修改对象属性后,绑定组件读取更新后的状态。

注意点: AppStorage 位于内存中,应用进程结束后数据消失。项目另外使用 Preferences 保存 historyList,两者职责不同。

LocalStorage:给一棵页面树单独建状态空间

LocalStorage 的使用方式与 AppStorage 相似,但作用域可以绑定到指定页面树或 Ability。多窗口应用中,两个窗口各自需要「当前表达式」时,LocalStorage 比全局 AppStorage 更合适。

可以把 LocalStorage 理解成页面树的状态容器。它负责组件响应更新,不负责持久化。页面销毁或进程结束后,仍需从 Preferences 或 RDB 恢复数据。

EventHub:发布一次动作通知

每个 Ability Context 都提供 eventHub,支持 on()emit()off()。它适合表达「某件事发生了」,例如设置保存后通知页面刷新主题。

示例位置: 任意能够取得 Ability Context 的组件生命周期中。

const hub = getContext(this).eventHub
const listener = () => this.reloadHistory()

hub.on('historyChanged', listener)
hub.emit('historyChanged')
hub.off('historyChanged', listener)

作用: 订阅者在 historyChanged 发出时重新加载数据。

原因: 发送方只知道事件名称,不需要持有接收组件实例。

输入与输出: emit() 输入事件名和可选参数;所有已注册回调同步收到通知。

注意点: 订阅和取消订阅必须使用同一个函数引用。组件消失时调用 off(),否则旧组件可能继续响应事件。

5 种通信方式怎么选

场景合适方式不应承担的职责
父组件给子组件一个显示值@Prop跨重启保存
子组件与父组件共同编辑状态@Link 或回调应用级全局状态
祖先与深层后代共享上下文@Provide / @Consume无关页面之间广播
多个页面持续共享可观察状态AppStorage 或 LocalStorage数据落盘
松耦合的一次性通知EventHub保存长期业务状态
页面跳转时携带 IDNavigation 参数持续同步可变对象

项目把长期保留的历史交给 Preferences,把页面实时读取的 KeyButton 交给 @StorageLink。这种组合体现了两个层次:内存状态负责 UI 响应,持久化存储负责恢复。

六、ArkData:从 Preferences 到关系型数据库

@kit.ArkData 是 HarmonyOS 数据管理 Kit 的统一导入入口。本机 SDK 的 @kit.ArkData.d.ts:39 导出 preferencesrelationalStoredistributedKVStoredistributedDataObject 等模块。入门项目最常先接触 Preferences 和关系型数据库。

Context 为什么是数据 API 的入口

项目在 handle.ets:14 调用 getContext().getApplicationContext()。Context 标识当前应用或 Ability 的运行环境,ArkData 根据 Context 确定应用沙箱、数据目录和权限归属。

页面短暂存在,ApplicationContext 覆盖整个应用进程。把它交给 Preferences 后,首页和历史页访问的是同一应用空间内的数据文件。

Preferences:适合小规模键值配置

Preferences 以「文件名 + 键 + 值」组织数据。值可以是 string、number、boolean 及受支持的数组类型。它适合主题开关、首次启动标记、排序方式和小规模历史数据。

项目把 Preferences 文件名和键都设为 History,再把 historyList 序列化为 JSON 字符串。下面是基于 handle.ets:587-607 整理的推荐读写方式:

文件: entry/src/main/ets/pages/handle.ets:587-607

import { preferences } from '@kit.ArkData'

const prf = await preferences.getPreferences(
  this.appContext, 'History')

await prf.put('History', JSON.stringify(this.historyList))
await prf.flush()

const json = await prf.get('History', '[]') as string
this.historyList = JSON.parse(json) as historyItem[]

作用: put() 把历史数组写入内存中的 Preferences 实例,flush() 刷新到持久化文件,get() 在应用下次启动时读回字符串。

原因: 当前记录只有表达式与结果两个字段,一次读取整个列表即可满足历史页展示。

输入与输出: 保存输入是 historyItem[] 序列化后的 string;读取输出经过 JSON.parse() 恢复为元组数组。

注意点: 默认值应与后续类型一致。既然代码按 string 调用 JSON.parse(),默认值也应使用 '[]'。写入多个相关键时,再统一调用一次 flush(),可以减少不必要的落盘。

Preferences 的完整读写顺序

ApplicationContext
      |
      v
getPreferences(context, "History")
      |
      +--> get(key, defaultValue) --> 内存对象 --> JSON.parse
      |
      `--> put(key, value) --> flush() --> 应用沙箱文件

getPreferences() 是异步操作,因此保存和读取函数使用 async/await。页面显示前需要数据时,可以在 aboutToAppear() 中调用异步读取,再由可观察状态触发列表刷新。项目正是在 index.ets:84-86 调用 readHistory()

如果改用关系型数据库,代码怎么组织

当历史记录需要按日期查询、分页、删除单条记录、统计运算类型或保存更多字段时,relationalStore 更适合。每条计算记录占一行,不必每次重写整个 JSON 数组。

替代示例文件: 可新建 entry/src/main/ets/repository/HistoryRepository.ets

import { relationalStore } from '@kit.ArkData'

const config: relationalStore.StoreConfig = {
  name: 'calculator.db',
  securityLevel: relationalStore.SecurityLevel.S1
}

const store = await relationalStore.getRdbStore(context, config)
await store.executeSql(
  'CREATE TABLE IF NOT EXISTS history(' +
  'id INTEGER PRIMARY KEY AUTOINCREMENT, expression TEXT, result TEXT)')

await store.insert('history', {
  expression: '2+3×4',
  result: '14'
})

作用: 代码创建 calculator.db,确保 history 表存在,并插入一条计算记录。

原因: 关系型数据库支持按列查询、排序、分页、事务和条件删除。记录数量增长后,不需要读取并解析全部历史。

输入与输出: getRdbStore() 输入 Context 和 StoreConfig,返回 RdbStoreinsert() 输入表名与 ValuesBucket,返回新行 ID。

注意点: 表结构需要版本管理。字段增加后,应在统一的数据库初始化或升级逻辑中执行迁移,不能把建表 SQL 分散到页面组件。

Preferences 与 RDB 的选择

判断条件PreferencesrelationalStore
数据结构少量键值多行、多列、有关系的数据
读取方式通常按键整体读取条件、排序、分页、聚合
更新粒度替换一个键的值更新指定行和列
典型用途设置、标记、小列表历史、账单、收藏、业务记录
当前计算器JSON 保存历史列表可替代为逐条保存记录

distributedKVStoredistributedDataObject 面向多设备协同数据。只有需求明确包含设备间同步时才需要引入;单设备计算器先把本地数据模型、错误处理和迁移策略写清楚。

AppStorage 不是 ArkData 持久化

AppStorage、LocalStorage 属于 ArkUI 状态管理,目标是让组件在状态变化后刷新。Preferences、RDB 属于 ArkData 持久化,目标是让数据在进程结束后仍能恢复。

一次计算完成后,项目先把记录放入 historyList,历史页可以马上显示;随后 saveHistory() 把列表写入 Preferences。这个顺序把「当前界面响应」和「下次启动恢复」分别交给对应机制。

七、计算器页面如何把这些知识组合起来

用数据生成 20 个按键

index.ets:32-53 使用 Key_date[] 描述按键值、标签、无障碍文本、内容类型和颜色。index.ets:311-320ForEach 把数组转换为 20 个 GridItem

数据驱动的好处是按键规则集中。数字键、运算符键、删除图标和正负号图标仍由同一个 @Builder calculatorKey() 生成,尺寸、动画和点击入口保持一致。

输入状态如何进入业务类

点击按钮后,index.ets:138-158 先更新按压动画,再调用 this.precess.baseClick(value)baseClick() 根据按键映射分派到数字输入、删除、正负号、百分号、运算符、清空或计算。

页面组件只知道「用户按了什么」,KeyButton 决定「输入应该怎样变化」。这条边界让页面布局调整时无需改表达式规则,也让计算逻辑能够脱离按钮组件单独测试。

实时预览与状态刷新

handle.ets:74-90preview() 在非等号输入后执行。表达式末尾是运算符时,它先临时移除末尾字符,再计算可用前缀。输入 12+ 时,预览区仍显示 12

input_s 是表达式,output_s 是预览或最终结果。index.ets:278-305 分别把它们绑定到 TextInputTextKeyButton@Observed 标记,又通过 @StorageLink 被页面持有,因此属性变化能够反映到组件树。

表达式优先级如何实现

handle.ets:244-417 使用数字栈和运算符栈计算表达式。扫描到数字时压入数字栈;扫描到新运算符时,先执行栈顶优先级更高或相同的运算;括号控制归约范围;一元负号 u- 拥有最高优先级。

输入 2 + 3 × 4
数字栈:     [2, 3, 4]
运算符栈:  [+, *]

先执行 *:  [2, 12]
再执行 +:  [14]

项目在数字解析时限制小数点数量和有效位数,在执行除法时检查除数是否为 0,在结束时确认数字栈只剩一个结果。异常统一转换为 ErrorPrecision Error,页面无需理解每一种解析异常。

历史页如何得到数据

点击等号后,handle.ets:237-241[表达式, 结果] 放入 historyList 并调用 saveHistory()。打开历史页时,Navigation 把 historyPage 压入页面栈;历史页通过相同的 @StorageLink('precess') 读取列表;应用重新启动后,首页的 aboutToAppear() 再从 Preferences 恢复记录。

等号点击
  |
  +--> historyList.push() --> @StorageLink --> 历史页立即可见
  |
  `--> saveHistory() -----> Preferences --> 下次启动可恢复

这条数据流同时使用组件通信和 ArkData。@StorageLink 解决当前进程内的响应更新,Preferences 解决跨重启保存,Navigation 解决页面前进与返回。

八、两周学习顺序如何对应项目

第一周先完成工程与页面骨架。读懂 AppScopemodule.json5EntryAbilitymain_pages.json,随后用 ColumnRowGridButtonTextInput 搭出计算器。状态先限定在 @State,重复 UI 用 @Builder 提取。

第二周把页面连接起来。用 NavigationNavDestination 建立历史页路由,用 @Observed@StorageLink 共享计算器状态,再通过 ApplicationContext 获取 Preferences 保存历史。Want 和 EventHub 可以分别用第二 Ability 启动、历史变更通知等小例子验证。

完成每一层后再进入下一层:

  1. Ability 能加载首页。
  2. 首页能根据状态刷新。
  3. Navigation 能进入并返回历史页。
  4. 首页与历史页能看到同一份内存状态。
  5. 结束进程后,Preferences 能恢复历史。
  6. 记录需要查询和分页时,再把数据访问迁移到 RDB Repository。
Logo

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

更多推荐