鸿蒙计算器应用开发实战
鸿蒙计算器应用开发实战:基于 HarmonyOS NEXT 6.1.1(API 24)与 ArkTS 的完整实现

一、前言
1.1 关于 HarmonyOS NEXT
HarmonyOS NEXT 是华为自主研发的新一代操作系统,它彻底摆脱了对 Android 的依赖,采用了完全自研的鸿蒙内核和系统架构。HarmonyOS NEXT 6.1.1 对应的是 API 24 版本,这一版本标志着鸿蒙生态进入了全新的成熟阶段。开发者在构建应用时,使用的是华为自研的 ArkTS 语言——一种基于 TypeScript 的超集,并且采用声明式 UI 框架 ArkUI,提供了高效、流畅的跨设备开发体验。
1.2 本文目标
本文将以一个完整的计算器应用为实例,带领大家深入了解 HarmonyOS NEXT 环境下使用 ArkTS + ArkUI 开发应用的全流程。你将学到以下内容:
- 理解 HarmonyOS NEXT 的项目结构和配置体系
- 掌握 ArkTS 声明式 UI 的基本语法和组件使用
- 学习 Grid 网格布局实现按钮面板
- 掌握状态管理(@State)的使用方法
- 理解计算器的核心交互逻辑
- 掌握应用的打包构建与调试
本文不依赖任何外部链接或第三方库,所有代码均在 HarmonyOS NEXT 6.1.1(API 24)环境下编写运行,确保读者可以原样构建。
二、项目结构解析
2.1 整体项目目录
一个标准的 HarmonyOS NEXT 项目由多个模块组成,下面是我们的计算器项目的目录结构:
jisuanji2/
├── AppScope/ # 应用全局配置
│ ├── app.json5 # 应用级配置(包名、版本等)
│ └── resources/ # 全局资源文件
│ ├── base/element/string.json # 全局字符串资源
│ └── base/media/ # 全局媒体资源(启动图标等)
├── entry/ # 应用主模块
│ ├── src/main/ets/ # ArkTS 源代码
│ │ ├── entryability/ # UIAbility(生命周期)
│ │ ├── entrybackupability/ # 备份扩展能力
│ │ └── pages/ # 页面文件
│ ├── src/main/resources/ # 模块级资源
│ ├── src/main/module.json5 # 模块配置
│ ├── build-profile.json5 # 模块构建配置
│ └── oh-package.json5 # 模块包依赖
├── hvigor/ # 构建工具配置
├── oh_modules/ # 依赖包目录
├── build-profile.json5 # 全局构建配置
├── oh-package.json5 # 全局包依赖
└── hvigorfile.ts # 构建入口脚本
2.2 关键文件职责说明
| 文件路径 | 功能说明 |
|---|---|
AppScope/app.json5 |
定义应用的包名(bundleName)、版本号、应用图标和标签 |
entry/src/main/module.json5 |
注册 EntryAbility、配置页面路由、声明设备类型 |
entry/src/main/ets/pages/Index.ets |
计算器的主要UI和逻辑——核心代码所在 |
entry/src/main/ets/entryability/EntryAbility.ets |
应用的入口 Ability,负责页面加载和生命周期管理 |
entry/src/main/resources/base/profile/main_pages.json |
页面路由表,声明了所有的页面路径 |
oh-package.json5 |
声明项目模型版本(modelVersion)和开发依赖 |
2.3 模型版本说明
在 oh-package.json5 中,我们看到:
{
"modelVersion": "6.1.1",
"description": "Please describe the basic information.",
"dependencies": {},
"devDependencies": {
"@ohos/hypium": "1.0.25",
"@ohos/hamock": "1.0.0"
}
}
modelVersion: "6.1.1" 标识了当前项目所基于的 HarmonyOS SDK 版本为 6.1.1,对应 API 24。开发依赖中的 hypium 是鸿蒙的单元测试框架,hamock 是模拟测试框架,两者配合实现对应用逻辑的自动化测试。
三、从零搭建计算器页面
3.1 页面入口与路由配置
每个 HarmonyOS 应用都需要通过 main_pages.json 来配置页面路由。对于我们的计算器来说,只需要一个主页面,配置如下:
// entry/src/main/resources/base/profile/main_pages.json
{
"src": [
"pages/Index"
]
}
这里 src 数组中的每一个元素代表一个页面文件的路径(相对于 ets/ 目录),框架会自动将 pages/Index 映射到 ets/pages/Index.ets。
3.2 Ability 生命周期管理
EntryAbility 是应用的入口,它继承自 UIAbility。在 HarmonyOS 中,UIAbility 负责管理页面的生命周期,其生命周期方法包括:
onCreate—— Ability 创建时调用onDestroy—— Ability 销毁时调用onWindowStageCreate—— 窗口舞台创建时调用,此处加载页面内容onWindowStageDestroy—— 窗口舞台销毁时调用onForeground—— 应用进入前台时调用onBackground—— 应用进入后台时调用
核心实现代码如下:
// EntryAbility.ets
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
const DOMAIN = 0x0000;
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
try {
// 设置颜色模式为跟随系统
this.context.getApplicationContext()
.setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
} catch (err) {
hilog.error(DOMAIN, 'testTag',
'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
}
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
}
onDestroy(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
// 加载首页页面
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag',
'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
onWindowStageDestroy(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}
onForeground(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
}
onBackground(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
}
}
关键代码解析:
-
setColorMode(COLOR_MODE_NOT_SET):让应用的颜色模式跟随系统设置,实现浅色/深色模式的自动适配。这是 HarmonyOS 提供的系统级能力,无需开发者手动监听。 -
windowStage.loadContent:在窗口舞台创建后,通过loadContent方法加载指定的页面。该方法是异步的,通过回调函数获取加载结果。如果失败,通过hilog记录错误信息,便于调试。 -
hilog日志系统:HarmonyOS 提供的高性能日志系统,DOMAIN是应用自定义的领域标识(十六进制),第二个参数是日志标签,第三个是格式化字符串。%{public}s表示公开字符串,日志中可见。
3.3 模块配置详解
在 module.json5 中,我们除了注册 EntryAbility 外,还注册了一个 EntryBackupAbility:
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": ["phone"],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["ohos.want.action.home"]
}
]
}
],
"extensionAbilities": [
{
"name": "EntryBackupAbility",
"srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
"type": "backup",
"exported": false,
"metadata": [
{
"name": "ohos.extension.backup",
"resource": "$profile:backup_config"
}
]
}
]
}
}
需要特别说明的几个配置项:
-
skills:表示该 Ability 支持的意图(Want)能力。entity.system.home和ohos.want.action.home表示该应用是桌面应用,会出现在鸿蒙系统的桌面启动器上。 -
extensionAbilities:注册了一个备份恢复扩展能力。EntryBackupAbility负责在应用数据需要备份或恢复时处理相应逻辑,这在云备份场景中非常关键。 -
startWindowBackground:引用$color:start_window_background,这个颜色值定义在resources/base/element/color.json中,值为#FFFFFF(白色)。该属性决定了应用启动时窗口的背景色,防止启动阶段出现白屏闪烁。
四、计算器核心逻辑实现
4.1 ArkTS 组件结构
在 HarmonyOS NEXT 的 ArkUI 框架中,一个页面的最小单位是组件(Component)。组件通过 @Entry 装饰器标记为页面入口,通过 @Component 装饰器声明为一个自定义组件。计算器应用的全部代码都位于 Index.ets 中,这是一个单页面的简约实现。
4.2 状态变量设计
@Entry
@Component
struct Index {
@State displayText: string = '0' // 显示屏当前显示的文本
firstNum: number = 0 // 运算的第一个操作数
operator: string = '' // 当前运算符(+、-、*、/)
isNewInput: boolean = true // 是否为新输入(重置标记)
四个变量的职责:
| 变量名 | 类型 | 初始值 | 作用 |
|---|---|---|---|
displayText |
string |
'0' |
显示屏内容,使用 @State 装饰——当其变化时自动触发 UI 更新 |
firstNum |
number |
0 |
存储运算符左边的数值 |
operator |
string |
'' |
当前选中的运算符,在 calcResult() 中被 switch 使用 |
isNewInput |
boolean |
true |
标记是否需要清空显示屏接受新输入 |
为什么 displayText 需要使用 @State?
在 ArkTS 的声明式 UI 中,@State 装饰的变量与视图之间建立了数据绑定。当 displayText 发生变化时(例如点击数字按钮后追加字符),框架会自动重新渲染与该变量绑定的 Text 组件,不需要开发者手动操作 DOM。这是声明式编程的核心优势。
4.3 数字输入逻辑
numClick(num: string): void {
if (this.isNewInput) {
// 新输入模式下,直接替换显示文本
this.displayText = num === '.' ? '0.' : num
this.isNewInput = false
} else {
// 连续输入模式,追加字符
if (num === '.' && this.displayText.includes('.')) return // 防重复小数点
this.displayText += num
}
}
设计要点分析:
(1)新输入模式(isNewInput === true)
当用户刚刚点击了一个运算符(如 +),或者刚刚点击了等号得到计算结果后,下一次输入数字时,应当清空屏幕开始新的输入。此时 isNewInput 为 true,函数会直接将 displayText 设置为点击的数字。
这里有一个细节处理:如果用户点击的是 .(小数点),则显示为 '0.' 而不是 '.',因为单独一个小数点作为起始是非法的数值表示,0. 才是合理的。
(2)连续输入模式(isNewInput === false)
在已经输入数字的基础上继续追加。这里需要防止重复输入小数点:
if (num === '.' && this.displayText.includes('.')) return
如果当前显示文本中已经包含了小数点,再次点击小数点按钮时直接返回,不做任何处理。这种做法符合常规计算器的行为:每个数字只能有一个小数点,位于最右侧连续追加。
4.4 运算符处理逻辑
opClick(op: string): void {
this.firstNum = parseFloat(this.displayText) // 将当前显示值保存为第一个操作数
this.operator = op // 记录运算符
this.isNewInput = true // 标记下次输入为新输入
}
当用户点击 +、-、*、/ 中的任意一个时:
- 保存第一个操作数:通过
parseFloat(this.displayText)将当前显示屏上的字符串转换为浮点数,存入firstNum。 - 记录运算符:将点击的运算符存入
operator变量。 - 重置输入标记:将
isNewInput设置为true,这样下一次用户点击数字时,显示屏将显示新输入的数字,而不是追加。
这种设计是典型的两个操作数嵌套计算模式,即:(第一个数 + 运算符)= 等待第二个数。
4.5 计算结果逻辑
calcResult(): void {
const secondNum = parseFloat(this.displayText) // 获取第二个操作数
let res = 0
switch (this.operator) {
case '+': res = this.firstNum + secondNum; break
case '-': res = this.firstNum - secondNum; break
case '*': res = this.firstNum * secondNum; break
case '/':
res = secondNum === 0 ? 0 : this.firstNum / secondNum // 除零保护
break
}
this.displayText = Number(res.toFixed(10)).toString() // 精度处理
this.isNewInput = true // 计算结果后标记为新输入
this.operator = '' // 清除运算符
}
关键设计细节:
(1)除零保护
case '/':
res = secondNum === 0 ? 0 : this.firstNum / secondNum
break
数学上除零是未定义的。常见的做法有:显示"Error"、显示"Infinity"、或者返回 0。这里选择了返回 0 的策略,计算器仍然可以继续使用,适合面向普通用户的场景。
(2)精度控制
this.displayText = Number(res.toFixed(10)).toString()
这是整个计算器最有必要详细解释的一行代码。JavaScript(以及 ArkTS)中的浮点数运算遵循 IEEE 754 标准,会引入经典的浮点数精度问题。例如:
0.1 + 0.2在 JS 中等于0.300000000000000042.35 * 100等于234.99999999999997
toFixed(10) 将结果保留 10 位小数,然后通过 Number(...) 去掉多余的尾随零,再用 .toString() 转回字符串。这样就解决了大部分浮点数精度问题。例如:
输入: 0.1 + 0.2
实际值: 0.30000000000000004
toFixed(10): "0.3000000000"
Number(...) + toString(): "0.3"
4.6 清除功能
clearAll(): void {
this.displayText = '0' // 显示屏归零
this.firstNum = 0 // 重置第一个操作数
this.operator = '' // 清除运算符
this.isNewInput = true // 标记为新输入
}
点击 C 按钮后,所有状态恢复到初始值。这是一个"全清"(Clear All)操作,相当于现实计算器上的 AC 键。
五、UI 界面构建详解
5.1 整体布局结构
页面的整体布局采用 Column 垂直容器,内部包含两个主要部分:
Column(根容器,全宽全高,内边距 10)
├── Text(显示屏,全宽,灰色背景)
└── Grid(按钮面板,4列网格)
├── 第1行: C, /, *, -
├── 第2行: 7, 8, 9, +
├── 第3行: 4, 5, 6, =
├── 第4行: 1, 2, 3, 0
└── 第5行: ., (空), (空)
5.2 显示屏组件
Text(this.displayText)
.width('100%')
.fontSize(48)
.textAlign(TextAlign.End)
.padding(20)
.backgroundColor('#f1f1f1')
.margin({ bottom: 10 })
属性链式调用是 ArkUI 声明式语法的一大特点。对这个显示屏的分析:
this.displayText:绑定@State变量,数据驱动的核心。fontSize(48):字号设为 48,在手机上视觉上够大够清晰。textAlign(TextAlign.End):右对齐,符合计算器显示屏数字从右往左显示的习惯。backgroundColor('#f1f1f1'):浅灰色背景,与白色主体形成视觉区分,模拟真实计算器的 LCD 屏效果。margin({ bottom: 10 }):与下方 Grid 按钮面板保持间距。
5.3 网格按钮面板
Grid() {
// 第一行:运算符与清除
GridItem() { Button('C').fontSize(24).width('100%').height(80)
.backgroundColor('#ff9500').fontColor(Color.White)
.onClick(() => this.clearAll()) }
GridItem() { Button('/').fontSize(24).width('100%').height(80)
.backgroundColor('#777').fontColor(Color.White)
.onClick(() => this.opClick('/')) }
GridItem() { Button('*').fontSize(24).width('100%').height(80)
.backgroundColor('#777').fontColor(Color.White)
.onClick(() => this.opClick('*')) }
GridItem() { Button('-').fontSize(24).width('100%').height(80)
.backgroundColor('#777').fontColor(Color.White)
.onClick(() => this.opClick('-')) }
// 第二行:数字 + 运算符
GridItem() { Button('7') ... }
GridItem() { Button('8') ... }
GridItem() { Button('9') ... }
GridItem() { Button('+').backgroundColor('#777')
.fontColor(Color.White) ... }
// 第三行
GridItem() { Button('4') ... }
GridItem() { Button('5') ... }
GridItem() { Button('6') ... }
GridItem() { Button('=').backgroundColor('#007aff')
.fontColor(Color.White) ... }
// 第四行
GridItem() { Button('1') ... }
GridItem() { Button('2') ... }
GridItem() { Button('3') ... }
GridItem() { Button('0') ... }
// 第五行
GridItem() { Button('.') ... }
GridItem() { Text('') } // 占位
GridItem() { Text('') } // 占位
}
.columnsTemplate('1fr 1fr 1fr 1fr') // 四等分列
.width('100%')
5.3.1 Grid 布局核心
Grid 组件是鸿蒙 ArkUI 提供的网格布局容器,它比传统的行/列更灵活。关键属性:
-
columnsTemplate('1fr 1fr 1fr 1fr'):声明 4 列,每列宽度均为1fr(等比例分配剩余空间)。fr是 Grid 布局中的弹性单位,类似于 CSS Grid 中的fr单位。4 个1fr意味着 4 列均分容器宽度。 -
GridItem:Grid 的子元素容器,每个 GridItem 默认占据一个单元格,按从左到右、从上到下的顺序填充。
5.3.2 按钮样式分类
按钮的视觉设计分为三类,通过颜色区分功能:
| 按钮类别 | 背景色 | 文字颜色 | 包含按钮 | 功能含义 |
|---|---|---|---|---|
| 数字按钮 | #eee(浅灰) |
默认(黑色) | 0-9 和 . | 数值输入 |
| 运算符按钮 | #777(深灰) |
Color.White |
+、-、*、/ | 选择运算 |
| 功能按钮 | #ff9500(橙色) |
Color.White |
C | 清除 |
| 等号按钮 | #007aff(蓝色) |
Color.White |
= | 计算结果 |
这种配色方案借鉴了 iOS 计算器的风格,橙色代表危险操作(清除),蓝色代表执行操作(计算),深灰色运算符视觉上比数字按钮更深,便于用户快速定位。
5.3.3 占位 GridItem 的处理
细心的读者可能注意到第五行只有两个实际按钮(. 和两个空的 Text('')),这是因为 4 列网格需要每行有 4 个 GridItem。而我们的计算器布局在第五行只需要 1 个小数点按钮,剩余 3 个位置用空白 Text('') 占位,保持网格结构完整。
六、HarmonyOS NEXT 特性应用详解
6.1 声明式 UI 的双向绑定机制
在传统的 Android 开发(XML + Java/Kotlin)中,UI 和数据是分离的,更新 UI 需要手动调用 findViewById 获取控件引用,再调用 setText 方法。而在 ArkTS 中,声明式 UI 通过 @State 装饰器建立了数据与视图的自动绑定。
工作流程如下:
用户点击按钮
↓
触发 onClick 回调
↓
修改 @State 变量(displayText)
↓
框架自动重新渲染 Text 组件
↓
屏幕显示新内容
这种模式大幅减少了 UI 更新代码量,也避免了手动操作 DOM 可能产生的状态不一致问题。
6.2 链式属性配置
ArkUI 的回调函数配置采用链式语法:
Button('9')
.fontSize(24)
.width('100%')
.height(80)
.backgroundColor('#eee')
.onClick(() => this.numClick('9'))
这种写法清晰流畅,每个属性方法都返回组件本身,可以继续调用下一个属性。相比于在配置对象中写一堆属性,链式调用的可读性更佳。
6.3 资源引用机制
HarmonyOS NEXT 提供了一套强大的资源引用机制。在 module.json5 中我们看到了大量的 $string:xxx、$media:xxx、$color:xxx、$profile:xxx 引用。这些引用在编译时会被替换为 resources/ 目录下对应的实际值:
$string:module_desc → resources/base/element/string.json → "module description"
$media:startIcon → resources/base/media/startIcon.png
$color:start_window_background → resources/base/element/color.json → "#FFFFFF"
$profile:main_pages → resources/base/profile/main_pages.json
这种机制的好处是:
- 多语言适配:可以通过在
resources/en_US/目录下放置同名string.json来实现国际化。 - 主题切换:在
resources/dark/目录下配置暗色主题资源,系统自动切换。 - 资源复用:同一资源可在多处引用,修改一处全局生效。
6.4 窗口启动优化
在 module.json5 中配置的 startWindowIcon 和 startWindowBackground 对于提升应用启动体验至关重要:
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
当用户点击应用图标时,系统会立即显示启动窗口(start window),其背景色就是 start_window_background(白色),图标为 startIcon。这给用户一种即时响应的感觉。随后系统加载 Ability、初始化页面,当页面渲染完成后,启动窗口被页面内容替换。如果未配置这些属性,启动阶段可能出现黑屏或白屏,影响用户体验。
七、完整代码汇总
7.1 Index.ets 完整源码
这是整个计算器的全部代码,位于 entry/src/main/ets/pages/Index.ets:
@Entry
@Component
struct Index {
@State displayText: string = '0'
firstNum: number = 0
operator: string = ''
isNewInput: boolean = true
numClick(num: string): void {
if (this.isNewInput) {
this.displayText = num === '.' ? '0.' : num
this.isNewInput = false
} else {
if (num === '.' && this.displayText.includes('.')) return
this.displayText += num
}
}
opClick(op: string): void {
this.firstNum = parseFloat(this.displayText)
this.operator = op
this.isNewInput = true
}
calcResult(): void {
const secondNum = parseFloat(this.displayText)
let res = 0
switch (this.operator) {
case '+': res = this.firstNum + secondNum; break
case '-': res = this.firstNum - secondNum; break
case '*': res = this.firstNum * secondNum; break
case '/':
res = secondNum === 0 ? 0 : this.firstNum / secondNum
break
}
this.displayText = Number(res.toFixed(10)).toString()
this.isNewInput = true
this.operator = ''
}
clearAll(): void {
this.displayText = '0'
this.firstNum = 0
this.operator = ''
this.isNewInput = true
}
build() {
Column() {
// 显示屏
Text(this.displayText)
.width('100%')
.fontSize(48)
.textAlign(TextAlign.End)
.padding(20)
.backgroundColor('#f1f1f1')
.margin({ bottom: 10 })
// 4列网格按钮面板
Grid() {
GridItem() { Button('C').fontSize(24).width('100%').height(80)
.backgroundColor('#ff9500').fontColor(Color.White)
.onClick(() => this.clearAll()) }
GridItem() { Button('/').fontSize(24).width('100%').height(80)
.backgroundColor('#777').fontColor(Color.White)
.onClick(() => this.opClick('/')) }
GridItem() { Button('*').fontSize(24).width('100%').height(80)
.backgroundColor('#777').fontColor(Color.White)
.onClick(() => this.opClick('*')) }
GridItem() { Button('-').fontSize(24).width('100%').height(80)
.backgroundColor('#777').fontColor(Color.White)
.onClick(() => this.opClick('-')) }
GridItem() { Button('7').fontSize(24).width('100%').height(80)
.backgroundColor('#eee')
.onClick(() => this.numClick('7')) }
GridItem() { Button('8').fontSize(24).width('100%').height(80)
.backgroundColor('#eee')
.onClick(() => this.numClick('8')) }
GridItem() { Button('9').fontSize(24).width('100%').height(80)
.backgroundColor('#eee')
.onClick(() => this.numClick('9')) }
GridItem() { Button('+').fontSize(24).width('100%').height(80)
.backgroundColor('#777').fontColor(Color.White)
.onClick(() => this.opClick('+')) }
GridItem() { Button('4').fontSize(24).width('100%').height(80)
.backgroundColor('#eee')
.onClick(() => this.numClick('4')) }
GridItem() { Button('5').fontSize(24).width('100%').height(80)
.backgroundColor('#eee')
.onClick(() => this.numClick('5')) }
GridItem() { Button('6').fontSize(24).width('100%').height(80)
.backgroundColor('#eee')
.onClick(() => this.numClick('6')) }
GridItem() { Button('=').fontSize(24).width('100%').height(80)
.backgroundColor('#007aff').fontColor(Color.White)
.onClick(() => this.calcResult()) }
GridItem() { Button('1').fontSize(24).width('100%').height(80)
.backgroundColor('#eee')
.onClick(() => this.numClick('1')) }
GridItem() { Button('2').fontSize(24).width('100%').height(80)
.backgroundColor('#eee')
.onClick(() => this.numClick('2')) }
GridItem() { Button('3').fontSize(24).width('100%').height(80)
.backgroundColor('#eee')
.onClick(() => this.numClick('3')) }
GridItem() { Button('0').fontSize(24).width('100%').height(80)
.backgroundColor('#eee')
.onClick(() => this.numClick('0')) }
GridItem() { Button('.').fontSize(24).width('100%').height(80)
.backgroundColor('#eee')
.onClick(() => this.numClick('.')) }
GridItem() { Text('') }
GridItem() { Text('') }
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.width('100%')
}
.width('100%')
.height('100%')
.padding(10)
.backgroundColor(Color.White)
}
}
7.2 EntryAbility.ets 完整源码
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
const DOMAIN = 0x0000;
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
try {
this.context.getApplicationContext()
.setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
} catch (err) {
hilog.error(DOMAIN, 'testTag',
'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
}
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
}
onDestroy(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag',
'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
onWindowStageDestroy(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}
onForeground(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
}
onBackground(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
}
}
八、功能测试用例
8.1 基本四则运算测试
编写测试用例是确保应用质量的关键。以下是在鸿蒙 Hypium 测试框架下的测试思路:
| 测试用例 | 操作步骤 | 预期结果 |
|---|---|---|
| 加法 | 输入 12 + 34 = | 显示 46 |
| 减法 | 输入 100 - 45 = | 显示 55 |
| 乘法 | 输入 6 × 7 = | 显示 42 |
| 除法 | 输入 81 ÷ 9 = | 显示 9 |
| 连续运算 | 2 + 3 = 5, 再 × 4 = | 显示 20 |
8.2 边界条件测试
| 测试用例 | 操作步骤 | 预期结果 |
|---|---|---|
| 除零 | 5 ÷ 0 = | 显示 0(容错设计) |
| 小数点输入 | 输入 . | 显示 0. |
| 重复小数点 | 输入 3.14. | 显示 3.14(忽略第二个小数点) |
| 大数运算 | 9999999999 + 1 = | 显示 10000000000 |
| 浮点精度 | 0.1 + 0.2 = | 显示 0.3(而非 0.30000000000000004) |
| 清除后继续 | 输入 5+3= 得 8,再点 C,再输入 2+2= | 显示 4 |
8.3 测试代码示例
在 entry/src/test/ 目录下的测试文件可以这样编写:
// List.test.ets
import { describe, it, expect } from '@ohos/hypium';
export default function calculatorTest() {
describe('CalculatorBasicTest', () => {
it('test_addition', 0, () => {
// 加法逻辑验证(纯逻辑层测试)
const firstNum = 12
const secondNum = 34
const result = firstNum + secondNum
expect(result).assertEqual(46)
})
it('test_float_precision', 0, () => {
const firstNum = 0.1
const secondNum = 0.2
const raw = firstNum + secondNum
const formatted = Number(raw.toFixed(10)).toString()
expect(formatted).assertEqual('0.3')
})
it('test_divide_by_zero', 0, () => {
const firstNum = 5
const secondNum = 0
const result = secondNum === 0 ? 0 : firstNum / secondNum
expect(result).assertEqual(0)
})
})
}
九、性能与优化思考
9.1 渲染性能
在当前计算器应用中,所有按钮的点击处理都是同步的,没有异步操作或复杂计算,所以渲染性能非常优秀。HarmonyOS 的 ArkUI 引擎采用了自研的渲染管线,对于这种轻量级页面,帧率可以稳定保持在 120fps。
9.2 内存管理
本应用只包含一个页面,状态变量仅有 4 个,不存在内存泄漏风险。鸿蒙 NEXT 提供了自动的垃圾回收机制,当一个页面被销毁时,其关联的组件树和状态变量会被自动回收。
9.3 可扩展性思考
当前计算器是一个最小可行产品(MVP),后续可以扩展的方向包括:
- 科学计算功能:增加 sin、cos、log、平方根等函数按钮,需要引入数学库支持。
- 运算历史记录:使用
@State数组保存每一步的运算记录,并用List组件展示。 - 深色模式适配:在
resources/dark/下配置暗色主题的颜色值,系统自动切换。 - 横竖屏适配:使用
Grid的rowsTemplate动态调整行列布局。 - 键盘支持:监听物理键盘输入,提高输入效率。
- 数据持久化:使用
PreferencesKit存储计算历史,应用重启后仍然保留。
十、构建与部署
10.1 开发环境要求
| 环境 | 要求 |
|---|---|
| 操作系统 | Windows 10 / 11、macOS、Ubuntu |
| 开发工具 | DevEco Studio NEXT |
| SDK 版本 | HarmonyOS NEXT 6.1.1(API 24) |
| 模型版本 | oh-package.json5 中 modelVersion 设为 “6.1.1” |
| 调试设备 | 鸿蒙 NEXT 真机或本地模拟器 |
10.2 构建流程
在 DevEco Studio 中,构建流程非常简洁:
- 打开项目,DevEco Studio 会自动识别
oh-package.json5中声明的模型版本和依赖。 - 点击
Build → Build HAP(s)开始构建,构建产物为.hap安装包文件。 - 也可以通过命令行构建:在项目根目录执行
hvigorw assembleHap。 - 构建完成后,HAP 包位于
entry/build/default/outputs/目录下。
10.3 应用签名与安装
HarmonyOS NEXT 对应用签名有严格要求:
- 在 DevEco Studio 中配置自动签名(需要华为开发者账号)。
- 签名后的 HAP 包可以通过
hdc install命令安装到真机或模拟器。 - 安装命令:
hdc install entry/build/default/outputs/default/entry-default-signed.hap
十一、总结
11.1 项目回顾
通过本文,我们从一个完整的 HarmonyOS NEXT 计算器项目出发,系统性地学习了:
- 项目结构:理解了 AppScope、entry 模块、资源体系、路由配置的分层设计思想。
- ArkTS 语法:掌握了
@Entry、@Component、@State装饰器的使用,以及声明式 UI 的数据驱动机制。 - ArkUI 组件:深入使用了
Column、Grid、GridItem、Text、Button等核心组件。 - 交互逻辑:实现了包含数字输入、运算符选择、计算结果、精度处理和容错保护在内的完整计算器逻辑。
- 生命周期:理解了 UIAbility 的生命周期方法以及页面加载流程。
- 资源管理:掌握了字符串、颜色、媒体、配置文件的声明与引用机制。
11.2 计算器业务逻辑全景
整个计算器的核心状态机可以用以下流程概括:
[初始状态: 显示=0, 无运算符]
│
├── 点击数字 ──→ 追加/替换显示数字
│
├── 点击运算符 ──→ 保存当前值到 firstNum
│ 记录运算符
│ 等待第二个数
│
├── 点击等号 ──→ 计算 firstNum 运算符 secondNum
│ 显示结果(精度处理)
│ 重置所有状态
│
└── 点击 C ────→ 全部重置为初始状态
11.3 对 HarmonyOS NEXT 的展望
HarmonyOS NEXT 6.1.1(API 24)代表了华为构建独立生态的决心。从开发者的角度来看:
- ArkTS 语言将 TypeScript 的类型安全和声明式 UI 带到了系统级开发领域,学习曲线平滑。
- ArkUI 框架在渲染性能、跨端适配方面表现出色,一次开发即可适配手机、平板、车机、智慧屏等多设备。
- 工具链(DevEco Studio + hvigor)提供了从代码编写、调试、性能分析到发布上架的完整闭环。
作为开发者,现在正是深入鸿蒙生态开发的最佳时机。希望本文的计算器实战案例能够帮助你快速入门,在此基础上开发出更多优秀的鸿蒙原生应用。
本文中的所有代码均在 HarmonyOS NEXT 6.1.1(API 24)环境下编写并验证通过。项目源码完整可用,读者可以直接依据本文内容构建并运行。
更多推荐




所有评论(0)