HarmonyOS 入门实战:从 ArkUI 页面、Navigation 路由到 ArkData 存储
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.ets、index.ets、history.ets 和 handle.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-10、entry/src/main/module.json5:1-50、build-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.ets、history.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。当前 targetSdkVersion 与 compatibleSdkVersion 都是 6.1.0(23);同文件 28-40 把 entry 加入构建。
entry/build-profile.json5:2 把模块声明为 stageMode,并在 targets 中提供 default 与 ohosTest。页面代码使用 Stage 模型的 UIAbility、Context 和 Want。
entry/src/main/module.json5 管模块能力。它声明 entry 模块、手机设备、EntryAbility、备份扩展和页面清单入口。新增 Ability 或修改桌面启动方式时,主要编辑这个文件。
entry/src/main/resources/base/profile/main_pages.json 管可由 loadContent() 加载的页面。当前注册 pages/index 和 pages/test。historyPage 是 Navigation 内部目的页,不需要作为窗口首屏注册。
一个模块配置多个 UIAbility
当应用需要两个独立窗口入口,例如「计算器」和「设置中心」,需要完成两步:先创建第二个 UIAbility 类,再在 module.json5 的 abilities 数组中注册。只创建 ArkTS 文件,系统不会自动发现它。
示例文件: entry/src/main/module.json5 的 abilities 节点。
"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-16 的 onCreate() 设置应用颜色模式并记录日志。它拿到的 want 描述本次启动请求,launchParam 描述启动原因和模式。
EntryAbility.ets:22-45 的 onWindowStageCreate() 获取主窗口,设置全屏并隐藏系统栏,然后调用 windowStage.loadContent('pages/index')。页面代码只有在窗口舞台创建后才能装载。
index.ets:84-86 在 aboutToAppear() 中读取历史记录。这个选择把读取时机放在首页即将显示时。index.ets:160-173 的 aboutToDisappear() 清理动画和光标计时器,页面离开后不会继续执行旧回调。
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 的页面由 Navigation、Column、Row、Grid、Button、TextInput、Text 和 Image 组成。Column 纵向组织菜单、显示区和键盘;Grid 使用 4 列 5 行放置 20 个按键;TextInput 显示表达式,Text 显示实时结果。
入门阶段常见装饰器
| 装饰器 | 数据归属 | 修改后是否更新 UI | 当前项目位置 |
|---|---|---|---|
@Entry | 页面入口标记 | 不保存数据 | index.ets:57 |
@Component | 自定义组件标记 | 不保存数据 | index.ets:58、history.ets:3 |
@State | 当前组件私有 | 是 | 路由栈、按钮动画、光标状态 |
@Observed | 可观察类实例 | 是,需配合观察引用 | handle.ets:10-11 的 KeyButton |
@StorageLink | AppStorage 中的共享键 | 是,双向同步 | 首页与历史页的 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-251、278-309 和 311-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,能力较基础 |
| 启动另一个 UIAbility | UIAbilityContext.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:67 和 history.ets:5 都声明:
文件: entry/src/main/ets/pages/index.ets:67、history.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 | 保存长期业务状态 |
| 页面跳转时携带 ID | Navigation 参数 | 持续同步可变对象 |
项目把长期保留的历史交给 Preferences,把页面实时读取的 KeyButton 交给 @StorageLink。这种组合体现了两个层次:内存状态负责 UI 响应,持久化存储负责恢复。
六、ArkData:从 Preferences 到关系型数据库
@kit.ArkData 是 HarmonyOS 数据管理 Kit 的统一导入入口。本机 SDK 的 @kit.ArkData.d.ts:39 导出 preferences、relationalStore、distributedKVStore、distributedDataObject 等模块。入门项目最常先接触 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,返回 RdbStore;insert() 输入表名与 ValuesBucket,返回新行 ID。
注意点: 表结构需要版本管理。字段增加后,应在统一的数据库初始化或升级逻辑中执行迁移,不能把建表 SQL 分散到页面组件。
Preferences 与 RDB 的选择
| 判断条件 | Preferences | relationalStore |
|---|---|---|
| 数据结构 | 少量键值 | 多行、多列、有关系的数据 |
| 读取方式 | 通常按键整体读取 | 条件、排序、分页、聚合 |
| 更新粒度 | 替换一个键的值 | 更新指定行和列 |
| 典型用途 | 设置、标记、小列表 | 历史、账单、收藏、业务记录 |
| 当前计算器 | JSON 保存历史列表 | 可替代为逐条保存记录 |
distributedKVStore 和 distributedDataObject 面向多设备协同数据。只有需求明确包含设备间同步时才需要引入;单设备计算器先把本地数据模型、错误处理和迁移策略写清楚。
AppStorage 不是 ArkData 持久化
AppStorage、LocalStorage 属于 ArkUI 状态管理,目标是让组件在状态变化后刷新。Preferences、RDB 属于 ArkData 持久化,目标是让数据在进程结束后仍能恢复。
一次计算完成后,项目先把记录放入 historyList,历史页可以马上显示;随后 saveHistory() 把列表写入 Preferences。这个顺序把「当前界面响应」和「下次启动恢复」分别交给对应机制。
七、计算器页面如何把这些知识组合起来
用数据生成 20 个按键
index.ets:32-53 使用 Key_date[] 描述按键值、标签、无障碍文本、内容类型和颜色。index.ets:311-320 用 ForEach 把数组转换为 20 个 GridItem。
数据驱动的好处是按键规则集中。数字键、运算符键、删除图标和正负号图标仍由同一个 @Builder calculatorKey() 生成,尺寸、动画和点击入口保持一致。
输入状态如何进入业务类
点击按钮后,index.ets:138-158 先更新按压动画,再调用 this.precess.baseClick(value)。baseClick() 根据按键映射分派到数字输入、删除、正负号、百分号、运算符、清空或计算。
页面组件只知道「用户按了什么」,KeyButton 决定「输入应该怎样变化」。这条边界让页面布局调整时无需改表达式规则,也让计算逻辑能够脱离按钮组件单独测试。
实时预览与状态刷新
handle.ets:74-90 的 preview() 在非等号输入后执行。表达式末尾是运算符时,它先临时移除末尾字符,再计算可用前缀。输入 12+ 时,预览区仍显示 12。
input_s 是表达式,output_s 是预览或最终结果。index.ets:278-305 分别把它们绑定到 TextInput 和 Text。KeyButton 被 @Observed 标记,又通过 @StorageLink 被页面持有,因此属性变化能够反映到组件树。
表达式优先级如何实现
handle.ets:244-417 使用数字栈和运算符栈计算表达式。扫描到数字时压入数字栈;扫描到新运算符时,先执行栈顶优先级更高或相同的运算;括号控制归约范围;一元负号 u- 拥有最高优先级。
输入 2 + 3 × 4
数字栈: [2, 3, 4]
运算符栈: [+, *]
先执行 *: [2, 12]
再执行 +: [14]
项目在数字解析时限制小数点数量和有效位数,在执行除法时检查除数是否为 0,在结束时确认数字栈只剩一个结果。异常统一转换为 Error 或 Precision Error,页面无需理解每一种解析异常。
历史页如何得到数据
点击等号后,handle.ets:237-241 把 [表达式, 结果] 放入 historyList 并调用 saveHistory()。打开历史页时,Navigation 把 historyPage 压入页面栈;历史页通过相同的 @StorageLink('precess') 读取列表;应用重新启动后,首页的 aboutToAppear() 再从 Preferences 恢复记录。
等号点击
|
+--> historyList.push() --> @StorageLink --> 历史页立即可见
|
`--> saveHistory() -----> Preferences --> 下次启动可恢复
这条数据流同时使用组件通信和 ArkData。@StorageLink 解决当前进程内的响应更新,Preferences 解决跨重启保存,Navigation 解决页面前进与返回。
八、两周学习顺序如何对应项目
第一周先完成工程与页面骨架。读懂 AppScope、module.json5、EntryAbility 和 main_pages.json,随后用 Column、Row、Grid、Button、TextInput 搭出计算器。状态先限定在 @State,重复 UI 用 @Builder 提取。
第二周把页面连接起来。用 Navigation 和 NavDestination 建立历史页路由,用 @Observed 与 @StorageLink 共享计算器状态,再通过 ApplicationContext 获取 Preferences 保存历史。Want 和 EventHub 可以分别用第二 Ability 启动、历史变更通知等小例子验证。
完成每一层后再进入下一层:
- Ability 能加载首页。
- 首页能根据状态刷新。
- Navigation 能进入并返回历史页。
- 首页与历史页能看到同一份内存状态。
- 结束进程后,Preferences 能恢复历史。
- 记录需要查询和分页时,再把数据访问迁移到 RDB Repository。
更多推荐

所有评论(0)