在这里插入图片描述
在这里插入图片描述

一、前言
在移动应用开发中,计算器是最经典、最具教学意义的入门项目之一。它看似简单,却涵盖了 UI 布局、状态管理、事件处理、业务逻辑分层等几乎所有前端开发的核心概念。本文将以 HarmonyOS 原生语言 ArkTS 为载体,从零开始构建一个功能完备的四则运算计算器,并深入剖析每一步的设计思路与技术细节。

本文适合以下读者:

初学 HarmonyOS / ArkTS 的开发者
希望系统了解 ArkUI 声明式编程范式的前端工程师
对鸿蒙生态感兴趣的技术爱好者
全文约 10000 字,建议配合 DevEco Studio 实际编码阅读。

二、项目背景与技术选型
2.1 为什么选择计算器作为教学案例
计算器应用天然具有以下教学优势:

交互闭环完整:用户输入 → 状态变更 → UI 刷新 → 结果输出,构成了一个完整的数据流循环。
状态管理典型:需要维护当前输入值、历史运算值、运算符、输入状态等多个状态变量,非常适合讲解 @State 装饰器的使用场景。
UI 布局多样:包含按钮网格、结果显示区、标题栏等多种布局需求,可以展示 Row、Column、Grid、ForEach 等多种布局组件的用法。
逻辑复杂度适中:运算逻辑本身不复杂,但涉及边界条件处理(除零、连续运算、小数点处理等),适合讲解防御性编程思维。
2.2 HarmonyOS 与 ArkTS 简介
HarmonyOS(鸿蒙操作系统)是华为自主研发的分布式操作系统,自 2019 年发布以来,经历了多次重大迭代。到 2026 年的 HarmonyOS NEXT 版本,系统已全面去除了 Android 兼容层,所有应用均基于鸿蒙原生框架开发。

ArkTS 是鸿蒙原生应用开发语言,它基于 TypeScript 语法进行了扩展,引入了声明式 UI 编程范式。与 React/React Native 的 JSX 不同,ArkTS 的 UI 描述采用纯 TypeScript 语法,通过 @Component、@State、@Builder 等装饰器来组织界面和逻辑。

ArkTS 的核心特性包括:

声明式 UI:通过组合函数描述界面结构
状态驱动:状态变化自动触发 UI 刷新
强类型系统:基于 TypeScript 的类型安全
组件化开发:通过 @Component 定义可复用单元
三、计算器的需求分析
在动手编码之前,我们先明确计算器的功能需求。

3.1 功能需求
需求编号 需求描述 优先级
F-001 支持数字 0-9 和小数点输入 P0
F-002 支持加、减、乘、除四则运算 P0
F-003 支持连续运算(例如 2+3×4) P0
F-004 显示当前输入和执行结果 P0
F-005 支持清空(C)和退格(⌫) P1
F-006 支持等号计算结果 P0
F-007 界面美观、按钮响应灵敏 P1
3.2 非功能需求
响应时间:按钮点击到界面刷新 < 16ms
容错处理:除零时返回友好提示而非崩溃
状态一致性:计算过程中任何时候清空都能回到初始状态
四、架构设计与数据流
4.1 组件结构
计算器应用采用单组件架构(Single Component Architecture),所有状态集中于 Calculator 组件内部。这种设计对于小型应用最为简洁高效。

Calculator (单一组件)
├── 标题栏 (Row)
├── 显示区域 (Text)
└── 按钮网格 (Column > Row × N)
├── 数字按钮 0-9
├── 运算符按钮 + - × ÷
├── 功能按钮 C, ⌫, .
└── 等号按钮 =
4.2 状态设计
我们定义了以下五个 @State 变量来管理计算器的全部状态:

状态变量 类型 初始值 作用
display string ‘0’ 显示区域的文本内容
currentInput string ‘’ 当前正在输入的数字
operator string ‘’ 上次选择的运算符
previousValue number 0 运算符左侧的操作数
isNewInput boolean true 是否开始新一次输入
4.3 数据流图
整个计算器的数据流可以用以下状态机描述:

初始状态

├──→ 输入数字 → 更新 currentInput → 更新 display

├──→ 输入运算符 → 保存 previousValue 和 operator → 重置 currentInput

├──→ 输入等号 → 计算 previousValue operator currentInput → 更新 display

└──→ 清空 → 重置所有状态到初始值
这是一个典型的有穷状态机(Finite State Machine),状态数量虽然不多,但状态转移的逻辑需要精确处理。

4.4 连续运算的实现
连续运算是计算器最核心也最容易出错的逻辑。所谓连续运算,指的是用户依次输入:

2 + 3 × 4 =
期望的计算方式是:先计算 2 + 3 = 5,再计算 5 × 4 = 20。

但在实际实现中,有两种不同的策略:

策略一:立即计算模式
每当用户输入一个新运算符时,立即用前一个运算符计算结果,并将结果作为下一次运算的起点。

输入 2 → 输入 + → 输入 3 → 输入 × → (先算 2+3=5) → 输入 4 → 输入 = → (算 5×4=20)
策略二:延迟计算模式
将所有操作数和运算符暂存,等用户按下等号时才一并计算。

输入 2 → 输入 + → 输入 3 → 输入 × → 输入 4 → 输入 = → (算 2+3×4=14)
策略二更符合数学运算优先级规则,但实现复杂度大幅增加。本文的计算器采用策略一(立即计算模式),因为它实现简单,逻辑清晰,适合教学演示。

五、编码实现详解
5.1 项目结构与文件组织
在 HarmonyOS 项目中,页面文件统一存放在 entry/src/main/ets/pages/ 目录下。每个页面是一个独立的 .ets 文件,通过 @Entry 装饰器标记为页面入口。

entry/src/main/ets/pages/
├── Index.ets # 主页面(导航页)
└── Calculator.ets # 计算器页面
计算器页面的基础骨架代码如下:

import { router } from ‘@kit.ArkUI’;

@Entry
@Component
struct Calculator {
@State display: string = ‘0’;
@State currentInput: string = ‘’;
@State operator: string = ‘’;
@State previousValue: number = 0;
@State isNewInput: boolean = true;

build() {
// UI 构建代码
}

onKeyClick(key: string) {
// 按键处理逻辑
}

calculate(a: number, b: number, op: string): number {
// 四则运算逻辑
}
}
5.2 UI 布局分析
ArkTS 的 UI 构建采用嵌套函数调用方式。计算器的 UI 结构可以分层解析:

第一层:根容器

Column()
.width(‘100%’)
.height(‘100%’)
.backgroundColor(‘#F5F5F5’)
最外层使用 Column 垂直布局,铺满整个屏幕。

第二层:标题栏

Row() {
Button(‘< 返回’)
Blank()
Text(‘🧮 计算器’)
Blank()
}
使用 Row 水平布局排列返回按钮、空白占位和标题文本。Blank() 组件自动填充剩余空间,实现两端对齐。

第三层:结果显示区域

Text(this.display)
.fontSize(40)
.fontWeight(FontWeight.Bold)
.textAlign(TextAlign.End)
.backgroundColor(‘#1A1A2E’)
.fontColor(Color.White)
结果显示区域使用深色背景和白色大字,数字右对齐模拟真实计算器的显示效果。fontSize(40) 保证了远距离可读性,textAlign(TextAlign.End) 实现右对齐。

第四层:按钮网格

按钮网格采用 Column 嵌套 Row 的结构实现 4×5 的布局:

Column({ space: 8 }) {
this.btnRow([‘7’, ‘8’, ‘9’, ‘÷’])
this.btnRow([‘4’, ‘5’, ‘6’, ‘×’])
this.btnRow([‘1’, ‘2’, ‘3’, ‘-’])
this.btnRow([‘0’, ‘.’, ‘=’, ‘+’])
this.btnRow([‘C’, ‘⌫’, ‘’, ‘’])
}
这里使用了 @Builder 来抽象按钮行,显著减少了重复代码:

@Builder btnRow(keys: string[]) {
Row({ space: 8 }) {
ForEach(keys, (key: string) => {
if (key !== ‘’) {
Button(key)
.width(70)
.height(60)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.borderRadius(12)
.backgroundColor(
key === ‘=’ ? ‘#4A90D9’ :
key === ‘C’ ? ‘#E74C3C’ : ‘#FFFFFF’
)
.fontColor(key === ‘=’ || key === ‘C’ ? Color.White : ‘#333333’)
.shadow({ radius: 2, color: ‘#10000000’, offsetY: 1 })
.onClick(() => this.onKeyClick(key))
} else {
Button(’ ').width(70).height(60).opacity(0).enabled(false)
}
})
}
.justifyContent(FlexAlign.Center)
}
@Builder 是 ArkTS 中非常强大的复用工具,它允许我们定义一段可复用的 UI 片段,并在 build() 方法中多次调用。这里我们将按钮行抽象为 btnRow,传入四个按键文本,ForEach 自动遍历生成按钮组件。

空字符串 ‘’ 对应一个透明占位按钮,用于保持网格对齐——例如第五行只需要两个功能按钮(C 和 ⌫),剩余两个位置用空白填充。

5.3 状态管理的精妙之处
@State 装饰器是 ArkTS 响应式系统的核心。当一个被 @State 标记的变量发生变化时,ArkUI 框架会自动重新渲染依赖于该变量的 UI 部分,无需开发者手动调用 setState() 或类似方法。

在我们的计算器中:

@State display: string = ‘0’;
当用户点击数字按钮,onKeyClick 方法修改 currentInput,然后更新 display:

this.currentInput += key;
this.display = this.currentInput;
一旦 display 发生变化,显示区域的 Text(this.display) 会自动更新文本内容。这就是声明式 UI 的魔力——你只需要管理状态,框架负责将状态映射到界面。

5.4 按键事件的完整处理逻辑
onKeyClick 方法是计算器的核心逻辑所在,它需要根据按键类型分发到不同的处理分支。我们先给出整体结构,然后逐段分析:

onKeyClick(key: string) {
if (key >= ‘0’ && key <= ‘9’) {
this.handleDigit(key);
} else if (key === ‘.’) {
this.handleDot();
} else if (key === ‘C’) {
this.handleClear();
} else if (key === ‘⌫’) {
this.handleBackspace();
} else if (key === ‘=’) {
this.handleEquals();
} else {
this.handleOperator(key);
}
}
5.4.1 数字处理

数字输入的关键在于 isNewInput 标志位:

handleDigit(key: string) {
if (this.isNewInput) {
this.currentInput = key; // 新输入,覆盖之前的内容
this.isNewInput = false;
} else {
this.currentInput += key; // 连续输入,追加数字
}
this.display = this.currentInput;
}
为什么需要 isNewInput?试想用户在计算完 2+3=5 之后,想继续输入 4。此时 currentInput 的值是上一次计算的结果 “5”。如果不重置,用户输入 4 会变成 “54”,这显然不是预期行为。所以每当用户输入运算符或等号后,isNewInput 被设为 true,下次数字输入会覆盖旧值。

5.4.2 小数点处理

小数点的处理有一个重要的边界条件——防重复:

handleDot() {
if (!this.currentInput.includes(‘.’)) {
this.currentInput += ‘.’;
this.display = this.currentInput;
this.isNewInput = false;
}
}
this.currentInput.includes(‘.’) 确保一个数字中最多只能有一个小数点。如果没有这个检查,用户可以输入 “3.14.15” 这样的非法数字。

5.4.3 清空与退格

清空操作需要将所有状态恢复到初始值:

handleClear() {
this.display = ‘0’;
this.currentInput = ‘’;
this.operator = ‘’;
this.previousValue = 0;
this.isNewInput = true;
}
退格操作需要处理删除到空的情况:

handleBackspace() {
if (this.currentInput.length > 1) {
this.currentInput = this.currentInput.slice(0, -1);
this.display = this.currentInput;
} else {
this.currentInput = ‘’;
this.display = ‘0’;
this.isNewInput = true;
}
}
值得注意的边界情况:如果当前输入长度为 1,退格后应该显示 “0” 而不是空字符串。这也符合真实计算器的行为。

5.5 连续运算的完整流程
连续运算是计算器最具挑战性的部分。让我们用一个完整的例子来追踪状态变化:

用户按顺序输入:5 + 3 × 2 =

步骤 1:输入 “5”

currentInput = “5”
isNewInput = false
display = “5”
步骤 2:输入 “+”

previousValue = 5(保存当前输入)
operator = “+”
isNewInput = true(等待下一个数字)
currentInput 被清空但 display 不变(visible 上仍显示 5)
步骤 3:输入 “3”

isNewInput = true,所以 currentInput = “3”(覆盖)
isNewInput = false
display = “3”
步骤 4:输入 “×”

发现已有运算符 “+”,先计算 5 + 3 = 8
previousValue = 8
operator = “×”
isNewInput = true
步骤 5:输入 “2”

currentInput = “2”
display = “2”
步骤 6:输入 “=”

计算 8 × 2 = 16
display = “16”
currentInput = “16”
isNewInput = true
最终结果 16。这里的核心处理逻辑在运算符输入分支:

handleOperator(key: string) {
if (this.currentInput) {
if (this.operator) {
// 已有运算符,先计算
let result = this.calculate(this.previousValue,
parseFloat(this.currentInput), this.operator);
this.display = String(result);
this.previousValue = result;
} else {
this.previousValue = parseFloat(this.currentInput);
}
this.operator = key;
this.isNewInput = true;
}
}
equals 的处理逻辑类似:

handleEquals() {
if (this.operator && this.currentInput) {
let result = this.calculate(this.previousValue,
parseFloat(this.currentInput), this.operator);
this.display = String(result);
this.currentInput = String(result);
this.operator = ‘’;
this.previousValue = result;
this.isNewInput = true;
}
}
5.6 四则运算的实现
运算函数本身是最简单的部分:

calculate(a: number, b: number, op: string): number {
switch (op) {
case ‘+’: return a + b;
case ‘-’: return a - b;
case ‘×’: return a * b;
case ‘÷’: return b !== 0 ? a / b : 0; // 防止除零
default: return b;
}
}
这里唯一需要注意的是除零保护。在 case ‘÷’ 分支中,我们检查除数是否为零。如果是,直接返回 0 而不是抛出异常。在实际生产应用中,可能需要显示 “Error” 或 “除数不能为零” 的提示信息。

六、UI 优化与设计细节
6.1 颜色与视觉设计
计算器的配色方案遵循以下原则:

元素 颜色方案 设计意图
背景 #F5F5F5 浅灰 减少视觉疲劳
显示区域 #1A1A2E 深色 + 白字 模拟真实计算器
数字按钮 白色背景 + 浅阴影 清晰可辨
运算符按钮 白色背景 与数字按钮统一
等号按钮 #4A90D9 蓝色 视觉突出
清空按钮 #E74C3C 红色 警示色,防误触
阴影设计:

.shadow({ radius: 2, color: ‘#10000000’, offsetY: 1 })
这里使用半透明黑色 (#10000000 表示 16 进制 RGBA,A=0x10 ≈ 6%) 模拟 Material Design 的阴影效果。偏移量 offsetY: 1 使阴影位于按钮下方,产生微妙的立体感。

6.2 交互反馈
虽然 ArkUI 提供了 animation API 来实现交互动画,但在计算器场景中,瞬时响应比动画更重要。因此我们没有添加按钮按下的缩放动画,而是通过以下方式优化交互体验:

即刻响应:onClick 事件直接转发到 onKeyClick 处理,无延迟
视觉区分:等号按钮使用不同颜色,让用户能快速定位"执行"按钮
清晰状态:显示区域始终展示当前输入或计算结果,用户不会迷失
6.3 无障碍访问
在 ArkUI 中,可以通过 accessibilityText 属性为按钮添加无障碍标签:

Button(‘+’)
.accessibilityText(‘加号’)
虽然没有在代码中显式添加,但对于生产环境的计算器应用,建议为每个按钮添加无障碍标签,帮助视障用户使用屏幕朗读功能。

七、常见问题与调试技巧
7.1 状态更新不及时
症状:点击按钮后界面没有立即刷新。
排查:检查是否使用了 @State 装饰器。在 ArkTS 中,只有被 @State、@Prop、@Link 等装饰器标记的变量才会触发 UI 刷新。普通成员变量(private)的修改不会驱动视图更新。

7.2 连续计算错误
症状:输入 2 + 3 × 4 = 得到错误结果。
排查:检查 isNewInput 标志位的设置。如果在运算符和等号处理分支中忘记设置 isNewInput = true,会导致后续数字追加到旧值后面。

7.3 小数点多个
症状:能输入 3.14.15 这样的非法数字。
排查:在 handleDot 方法中检查 currentInput.includes(‘.’)。注意这里使用的是 includes 方法(ES6),而不是 indexOf。

7.4 除零崩溃
症状:输入 5 ÷ 0 = 导致应用闪退。
排查:在 calculate 函数中增加除零保护。同时建议在 handleEquals 中也检查当前输入是否为零。

八、扩展与优化方向
本文实现的计算器虽然功能完整,但仍有很大的优化空间:

8.1 数学运算增强
支持百分比运算:200 × 10% = 20
支持平方根:√ 9 = 3
支持正负号切换:± 按钮
存储功能:MC/MR/M+/M- 等存储键
8.2 UI/UX 优化
按钮触感反馈:使用 hapticFeedback API 提供震动反馈
横向布局:在横屏模式下提供科学计算器布局
深色模式:适配系统的深色/浅色主题
字体自适应:当数字位数过多时自动缩小字号
8.3 性能优化
减少不必要的渲染:将静态 UI 部分提取为 @Builder
使用 LazyForEach:对于大量数据场景,使用懒加载列表
九、总结
本文详细讲解了使用 ArkTS 开发计算器应用的全过程,从需求分析、状态设计,到 UI 编码、事件处理,再到调试优化。通过这个经典案例,我们学习了以下核心 ArkTS 概念:

@Component 与 @Entry:声明组件和页面入口
@State:管理组件内部状态
@Builder:复用 UI 片段
ForEach:循环渲染组件
响应式数据流:状态变化自动驱动 UI 刷新
计算器虽小,但它完整地展现了声明式 UI 编程的"状态驱动视图"核心理念。掌握了计算器的开发,你就已经理解了 ArkTS 开发中最核心的思维模式。接下来,无论你是开发待办清单、天气应用还是更复杂的社交应用,底层的数据流思维都是相通的。

十、参考资源
HarmonyOS 开发者官网
ArkTS 语言规范
ArkUI 组件参考
DevEco Studio 使用指南

Logo

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

更多推荐