作为一名长期从事移动应用开发的工程师,最近我使用DevEco Studio 6.0.1完成了一个基于ArkUI框架的计算器应用开发。这个项目虽然功能看似简单,但完整呈现了鸿蒙应用开发的核心技术栈和设计理念。计算器作为基础工具类应用,对UI交互、状态管理和算法逻辑都有较高要求,是检验开发能力的绝佳练手项目。
这个计算器采用经典的单页面垂直布局,支持完整的四则运算、括号优先级、负数计算等功能。在技术实现上,我选择了ArkTS作为开发语言,充分利用了鸿蒙系统的声明式UI特性和响应式编程模型。项目最大的亮点在于表达式解析算法的实现,通过双栈结构处理运算符优先级问题,这在后续章节会详细展开。对于刚接触鸿蒙开发的同行来说,这个项目涵盖了从环境搭建到发布上架的全流程,具有很好的参考价值。
开发工具选择华为官方推荐的DevEco Studio 6.0.1 Release版本,这是目前最稳定的鸿蒙开发IDE。安装时需要注意几个关键点:
提示:安装完成后务必运行
ohpm init命令初始化鸿蒙包管理环境,这是很多新手容易忽略的关键步骤
采用标准的鸿蒙应用目录结构:
code复制calculator/
├── entry/src/main/
│ ├── ets/ # ArkTS代码目录
│ │ ├── pages/ # 页面组件
│ │ ├── components/ # 公共组件
│ │ └── model/ # 业务逻辑
│ ├── resources/ # 资源文件
│ └── module.json5 # 模块配置
└── oh-package.json5 # 依赖管理
这种结构将UI、业务逻辑和资源清晰分离,便于团队协作和后期维护。特别建议将计算器核心算法单独放在model目录,与界面逻辑解耦。
计算器采用经典的Grid网格布局,通过ArkUI的<Grid>和<GridItem>组件实现。关键布局代码如下:
typescript复制Grid() {
// 第一行:结果显示区
GridItem({ row: 0, column: 0 }) {
Column() {
Text(this.formula) // 公式显示
.fontSize(20)
Text(this.result) // 结果显示
.fontSize(40)
}
}.columnStart(0).columnEnd(3)
// 数字和运算符按钮
ForEach(this.buttons, (item: ButtonItem) => {
GridItem({ row: item.row, column: item.col }) {
Button(item.label)
.onClick(() => this.onButtonClick(item))
}
})
}
按钮组件采用自定义封装,实现统一的样式和行为:
typescript复制@Component
struct CalcButton {
label: string
onClick: () => void
build() {
Button(this.label)
.width('100%')
.height('100%')
.fontSize(24)
.backgroundColor(this.getBgColor())
.onClick(() => this.onClick())
}
private getBgColor(): ResourceColor {
// 根据按钮类型返回不同背景色
}
}
计算器的核心在于表达式解析和计算,我采用了经典的双栈算法(Shunting-yard算法变种):
typescript复制class CalculatorEngine {
private numberStack: number[] = []
private operatorStack: string[] = []
evaluate(expression: string): number {
const tokens = this.tokenize(expression)
tokens.forEach(token => {
if (this.isNumber(token)) {
this.numberStack.push(parseFloat(token))
} else if (this.isOperator(token)) {
while (this.shouldProcessOperator(token)) {
this.processTopOperator()
}
this.operatorStack.push(token)
} else if (token === '(') {
this.operatorStack.push(token)
} else if (token === ')') {
while (this.operatorStack[this.operatorStack.length - 1] !== '(') {
this.processTopOperator()
}
this.operatorStack.pop() // 弹出左括号
}
})
while (this.operatorStack.length > 0) {
this.processTopOperator()
}
return this.numberStack.pop() || 0
}
private processTopOperator() {
// 实现运算符处理逻辑
}
}
该算法能正确处理运算符优先级和括号嵌套,时间复杂度为O(n),满足计算器场景的性能需求。针对鸿蒙环境,我做了以下优化:
采用ArkUI的响应式状态管理机制,核心状态变量使用@State装饰器:
typescript复制@Entry
@Component
struct CalculatorPage {
@State formula: string = ''
@State result: string = '0'
private engine: CalculatorEngine = new CalculatorEngine()
onButtonClick(item: ButtonItem) {
switch (item.type) {
case 'digit':
this.formula += item.value
break
case 'operator':
this.formula += ` ${item.value} `
break
case 'equals':
this.result = this.engine.evaluate(this.formula).toString()
break
case 'clear':
this.formula = ''
this.result = '0'
break
}
}
}
这种设计保证了UI与数据的自动同步,当formula或result变化时,相关组件会自动更新。
在实现计算逻辑时,运算符优先级是最容易出错的部分。我的解决方案是定义优先级映射表:
typescript复制private readonly precedence: Record<string, number> = {
'+': 1,
'-': 1,
'*': 2,
'/': 2,
'%': 2
}
private shouldProcessOperator(newOp: string): boolean {
if (this.operatorStack.length === 0) return false
const topOp = this.operatorStack[this.operatorStack.length - 1]
if (topOp === '(') return false
return this.precedence[topOp] >= this.precedence[newOp]
}
这种实现方式比传统的if-else判断更清晰,也便于扩展新的运算符。
处理用户输入时,小数点和负数需要特殊处理:
typescript复制// 在tokenize方法中添加特殊处理
private tokenize(expr: string): string[] {
// 将连续的"-"转换为负数标识
expr = expr.replace(/(?<=\s|^|\(|\/|\*|\+|-)-(\d)/g, ' -$1')
// 处理小数点前的隐式零
expr = expr.replace(/(?<=\s|^|\(|\/|\*|\+|-|%)\.(\d)/g, '0.$1')
return expr.split(/\s+/).filter(token => token !== '')
}
在开发过程中,我发现长时间使用后应用内存会缓慢增长。通过DevEco Studio的性能分析工具定位到问题:
解决方案是复用引擎实例,并在页面销毁时清理资源:
typescript复制aboutToDisappear() {
this.engine.cleanup()
}
鸿蒙应用发布必须使用正式签名,配置步骤如下:
build-profile.json5中配置签名信息:json复制"signingConfigs": [{
"name": "release",
"material": {
"certpath": "signing/release.p12",
"storePassword": "your_password",
"keyAlias": "release",
"keyPassword": "your_password",
"profile": "signing/your_profile.p7b",
"signAlg": "SHA256withECDSA"
}
}]
通过命令行或IDE界面执行构建:
bash复制./gradlew assembleRelease
生成的HAP包位于build/outputs/entry/release/目录。
整个流程大约需要3-5个工作日,建议提前准备以下材料:
计算器界面虽然简单,但频繁的按钮交互对渲染性能仍有要求。我采用了以下优化措施:
@Reusable装饰器标记按钮组件,实现实例复用cachedCount属性预缓存build()方法中进行复杂计算typescript复制@Reusable
@Component
struct CalcButton {
// ...
}
针对长表达式的计算,实现了以下优化:
worker线程处理复杂计算,避免阻塞UItypescript复制const worker = new worker.ThreadWorker('entry/ets/workers/CalculatorWorker.ts')
worker.onmessage = (msg: MessageEvents) => {
this.result = msg.data
}
通过以下方式降低内存占用:
WeakRef管理临时对象typescript复制private largeData: WeakRef<LargeObject> = new WeakRef(new LargeObject())
为计算引擎编写了全面的单元测试:
typescript复制describe('CalculatorEngine', () => {
it('should handle basic addition', () => {
const engine = new CalculatorEngine()
expect(engine.evaluate('1 + 2')).toEqual(3)
})
it('should respect operator precedence', () => {
const engine = new CalculatorEngine()
expect(engine.evaluate('2 + 3 * 4')).toEqual(14)
})
})
使用鸿蒙的UITest框架编写界面测试:
typescript复制it('test_number_input', async () => {
await driver.assertComponentExist(ON.text('0'))
await driver.click(ON.text('1'))
await driver.click(ON.text('2'))
await driver.assertComponentExist(ON.text('12'))
})
使用DevEco Studio的Profiler工具进行:
在完成这个鸿蒙计算器项目的过程中,我积累了一些值得分享的经验:
状态管理:对于计算器这类状态密集的应用,合理划分状态粒度非常重要。我最初将所有状态放在一个类中,导致更新效率低下。后来改为按功能模块拆分状态,性能明显提升。
算法选择:表达式解析算法有多种实现方式,经过对比测试,双栈算法在代码可读性和性能之间取得了最佳平衡。对于更复杂的科学计算需求,可以考虑引入ANTLR等解析器生成工具。
测试覆盖:计算器应用的边界条件特别多(如除零、括号匹配等),编写全面的测试用例可以节省大量调试时间。建议采用测试驱动开发(TDD)模式。
国际化:如果计划支持多语言,建议从一开始就使用资源引用而非硬编码字符串。鸿蒙的资源管理系统能很好地支持这一需求。
这个项目虽然规模不大,但涵盖了鸿蒙应用开发的完整流程。对于想学习鸿蒙开发的同行,我有两个建议:一是从这类工具类应用入手,逐步掌握核心概念;二是多参考华为官方提供的Sample代码,里面包含很多最佳实践。