📌 所属专栏:HarmonyOS NEXT 零基础实战教程
🎯 适配版本:HarmonyOS 6.1.0(API23)/ DevEco Studio 最新版 / Stage模型
✅ 难度等级:新手进阶 · 架构思想实战项目
💡 前言
对于鸿蒙NEXT初学者而言,单纯背诵API、写静态页面很难真正掌握开发核心。想要吃透ArkUI声明式开发、状态管理、业务逻辑分层,计算器是性价比最高的实战项目。
它无需网络请求、无需复杂动画、无第三方依赖,核心聚焦UI布局、用户交互、逻辑运算、状态管理四大核心能力。同时涵盖新手必学的MVVM分层思想,彻底告别「UI和业务逻辑混写」的新手陋习。
本文将从零带大家开发一款功能完整、界面精致、兼容多场景、修复全部原生坑点的商用级计算器,包含基础四则运算、小数计算、正负切换、百分比、除零容错、连续运算、精度修复等全套功能,全程干货无废话,零基础可直接复刻!

一、项目核心亮点与学习价值
1.1 项目核心功能
本次开发的计算器摒弃简陋Demo效果,对标系统原生计算器,实现全场景可用功能:
•基础运算:加减乘除四则运算,支持连续链式运算
•精准小数计算:修复JS经典浮点精度问题,0.1+0.2精准等于0.3
•便捷辅助功能:一键清空、正负号切换、百分比转换
•智能容错机制:除零报错、超长数字限制、重复小数点拦截
•运算符替换:输入算式后可直接切换运算符,无需重新输入
•全新计算逻辑:等号结算后,点击数字自动开启新一轮计算
•可视化预览:实时展示完整运算表达式,操作更直观
1.2 独家学习价值
区别于网上杂乱的混写代码教程,本项目严格遵循MVVM分层架构,帮新手建立规范的开发思维:
•视图层(View):只负责UI渲染和用户点击交互,不掺杂任何运算逻辑
•视图模型(ViewModel):独立封装计算器核心引擎,统一管理所有状态与运算逻辑
•彻底解耦:UI与业务逻辑完全分离,代码易维护、易拓展、可单独测试

二、MVVM架构分层详解(核心思想)
很多新手写代码习惯「UI布局+运算逻辑全部写在一个页面」,代码臃肿、后期难以修改。本项目采用标准MVVM分层,结构清晰一目了然。
2.1 整体架构分层
整个项目分为两大核心模块,职责完全隔离:

  1. View层(Index.ets 页面视图)
    只做两件事:渲染页面UI、接收用户按钮点击事件。完全不参与数值计算、状态判断,不知道「3+5等于几」,只负责调用引擎方法、同步页面状态。
  2. ViewModel层(CalcEngine.ets 计算引擎)
    计算器的核心大脑,独立存储所有运算状态、封装全部运算逻辑、处理所有边界容错。不关心UI样式、按钮颜色,只专注数据处理与逻辑计算。
    2.2 架构优势
    •逻辑复用:计算引擎可直接复用在其他鸿蒙页面,无需重复编写代码
    •便于调试:运算报错可单独排查引擎逻辑,无需关联UI代码
    •方便拓展:后续新增科学计算、历史记录等功能,无需改动原有UI结构

三、项目创建与工程配置
3.1 新建项目
1.打开DevEco Studio,选择 Create HarmonyOS Project
2.选择 Empty Ability 空白纯净模板
3.自定义项目名称,API版本选择 API23
4.工程模型选择 Stage模型,语言选择 ArkTS
5.等待项目初始化完成,清理默认冗余代码
3.2 页面文件结构
在pages目录下创建两个核心文件,实现分层开发:
•CalcEngine.ets:运算引擎、状态管理、核心逻辑(ViewModel层)
•Index.ets:页面布局、按钮渲染、事件分发(View层)

四、核心引擎层开发(CalcEngine.ets)
引擎层是计算器的核心,包含枚举定义、状态存储、数字输入、运算逻辑、容错处理、结果格式化全套能力。
4.1 运算符枚举定义
采用规范枚举管理运算符,替换原生杂乱字符,UI展示更专业,避免逻辑判断混乱:
typescript
// 运算运算符枚举(使用标准数学符号,优化展示效果)
export enum CalcOperator {
NONE = ‘’,
ADD = ‘+’,
SUBTRACT = ‘−’,
MULTIPLY = ‘×’,
DIVIDE = ‘÷’
}

4.2 全局状态定义
集中存储计算器所有核心状态,精准记录每一步操作状态,解决连续运算、状态错乱问题:
typescript
export class CalcEngine {
// 屏幕展示数值
private _display: string = ‘0’;
// 上一个操作数
private _previousOperand: number = 0;
// 当前正在输入的操作数
private _currentOperand: number = 0;
// 待执行运算符
private _pendingOperator: CalcOperator = CalcOperator.NONE;
// 是否为新数字输入状态
private _isNewEntry: boolean = false;
// 是否刚刚完成一次运算(用于重置新一轮计算)
private _justEvaluated: boolean = false;
// 表达式预览文本
private _expressionPreview: string = ‘’;
// 是否出现运算错误
private _hasError: boolean = false;

// 只读对外暴露属性
get display(): string { return this._display; }
get expressionPreview(): string { return this._expressionPreview; }
get hasError(): boolean { return this._hasError; }
}

4.3 核心工具方法
统一格式化数值,解决浮点精度、大数展示、尾部零冗余问题:
typescript
// 数值格式化,修复浮点精度、大数溢出、尾部零问题
private formatResult(num: number): string {
if (!isFinite(num)) return ‘错误’;
if (Math.abs(num) < 1e-10) return ‘0’;
if (Number.isInteger(num) && Math.abs(num) < 1e15) return num.toString();
if (Math.abs(num) > 1e15) return num.toExponential(6);
// 保留10位小数,去除尾部无效零,解决0.1+0.2精度问题
return parseFloat(num.toFixed(10)).toString();
}

// 操作数格式化,优化预览展示效果
private formatOperand(num: number): string {
return this.formatResult(num);
}
4.4 数字输入逻辑
处理各类输入边界:前置零清除、重复小数点拦截、结算后重置输入、超长数字限制:
typescript
// 数字/小数点输入核心逻辑
inputDigit(digit: string): void {
// 错误状态禁止输入
if (this._hasError) return;

// 结算完成后,点击数字重置新计算
if (this._justEvaluated) {
  this.clearAll();
  this._justEvaluated = false;
}

if (this._isNewEntry) {
  // 新输入阶段,直接替换屏幕数值
  this._display = digit;
  this._isNewEntry = false;
} else {
  // 拦截重复小数点
  if (digit === '.' && this._display.includes('.')) return;
  
  // 清除前置零
  if (this._display === '0' && digit !== '.') {
    this._display = digit;
  } else {
    // 限制最大输入长度,防止数值溢出
    const pureNum = this._display.replace('-', '').replace('.', '');
    if (pureNum.length >= 15) return;
    this._display += digit;
  }
}
this._currentOperand = parseFloat(this._display);

}

4.5 运算符输入逻辑(支持连续运算+运算符替换)
typescript
// 运算符点击逻辑
inputOperator(op: CalcOperator): void {
if (this._hasError) return;

// 已有待执行运算符,先完成上一次运算,实现连续计算
if (this._pendingOperator !== CalcOperator.NONE && !this._isNewEntry) {
  this.evaluate();
} 
// 未输入新数字时,支持替换已有运算符
else if (this._isNewEntry && this._pendingOperator !== CalcOperator.NONE) {
  this._pendingOperator = op;
  this._expressionPreview = `${this.formatOperand(this._previousOperand)} ${op} `;
  return;
}

// 缓存当前数值,等待下一次输入
this._previousOperand = parseFloat(this._display);
this._pendingOperator = op;
this._isNewEntry = true;
this._justEvaluated = false;
this._expressionPreview = `${this.formatOperand(this._previousOperand)} ${op} `;

}

4.6 核心运算逻辑(容错+精度修复)
typescript
// 执行计算逻辑
evaluate(): string {
if (this._hasError || this._pendingOperator === CalcOperator.NONE) {
return this._display;
}

this._currentOperand = parseFloat(this._display);
const prev = this._previousOperand;
const curr = this._currentOperand;
let result: number = 0;

// 四则运算逻辑
switch (this._pendingOperator) {
  case CalcOperator.ADD:
    result = prev + curr;
    break;
  case CalcOperator.SUBTRACT:
    result = prev - curr;
    break;
  case CalcOperator.MULTIPLY:
    result = prev * curr;
    break;
  case CalcOperator.DIVIDE:
    // 除零容错
    if (curr === 0) {
      this._hasError = true;
      this._display = '错误';
      this._expressionPreview = '';
      this._pendingOperator = CalcOperator.NONE;
      return '错误';
    }
    result = prev / curr;
    break;
}

// 更新表达式预览
this._expressionPreview = `${this.formatOperand(prev)} ${this._pendingOperator} ${this.formatOperand(curr)} =`;
// 格式化结果并更新状态
this._display = this.formatResult(result);
this._previousOperand = result;
this._pendingOperator = CalcOperator.NONE;
this._isNewEntry = true;
this._justEvaluated = true;

return this._display;

}

4.7 辅助功能方法
typescript
// 清空所有状态,重置计算器
clearAll(): void {
this._display = ‘0’;
this._previousOperand = 0;
this._currentOperand = 0;
this._pendingOperator = CalcOperator.NONE;
this._isNewEntry = false;
this._justEvaluated = false;
this._expressionPreview = ‘’;
this._hasError = false;
}

// 正负号切换
toggleSign(): void {
if (this._hasError || this._display === ‘0’) return;
this._display = this._display.startsWith(‘-’) ? this._display.slice(1) : -${this._display};
this._currentOperand = parseFloat(this._display);
}

// 百分比转换
percentage(): void {
if (this._hasError) return;
const res = parseFloat(this._display) / 100;
this._display = this.formatResult(res);
this._currentOperand = res;
}

五、视图层开发(Index.ets)
视图层专注UI渲染和事件分发,通过@State同步引擎状态,采用Builder封装通用按钮,代码高度精简、样式统一、适配全屏幕。
typescript
import { CalcEngine, CalcOperator } from ‘./CalcEngine’

@Entry
@Component
struct Index {
// 页面响应式状态
@State display: string = ‘0’;
@State expression: string = ‘’;
@State hasError: boolean = false;

// 实例化计算引擎
private engine: CalcEngine = new CalcEngine();

// 同步引擎状态到页面UI
private refreshDisplay(): void {
this.display = this.engine.display;
this.expression = this.engine.expressionPreview;
this.hasError = this.engine.hasError;
}

// 通用按钮样式构建器
@Builder
CalcButton(label: string) {
Text(label)
.fontSize(26)
.fontColor(this.getBtnTextColor(label))
.fontWeight(this.getBtnFontWeight(label))
.width(‘calc((100% - 48px) / 4)’)
.height(64)
.textAlign(TextAlign.Center)
.backgroundColor(this.getBtnBg(label))
.borderRadius(32)
.onClick(() => this.onButtonClick(label))
}

// 按钮行布局构建器
@Builder
ButtonRow(labels: string[]) {
Row() {
ForEach(labels, (label: string) => {
this.CalcButton(label)
}, (label: string) => label)
}
.width(‘100%’)
.height(64)
.padding({ left: 16, right: 16 })
}

// 获取按钮背景色
private getBtnBg(label: string): ResourceColor {
if ([‘AC’, ‘+/-’, ‘%’].includes(label)) return ‘#E8E8E8’;
if ([‘÷’, ‘×’, ‘−’, ‘+’].includes(label)) return ‘#FF6B35’;
if (label === ‘=’) return ‘#1a1a2e’;
return ‘#3a3a4a’;
}

// 获取按钮文字颜色
private getBtnTextColor(label: string): ResourceColor {
return [‘AC’, ‘+/-’, ‘%’].includes(label) ? ‘#1a1a2e’ : ‘#FFFFFF’;
}

// 获取按钮字体权重
private getBtnFontWeight(label: string): FontWeight {
return label === ‘=’ ? FontWeight.Bold : FontWeight.Normal;
}

// 按钮点击事件分发
private onButtonClick(label: string): void {
switch (label) {
case ‘AC’:
this.engine.clearAll();
break;
case ‘+/-’:
this.engine.toggleSign();
break;
case ‘%’:
this.engine.percentage();
break;
case ‘÷’:
this.engine.inputOperator(CalcOperator.DIVIDE);
break;
case ‘×’:
this.engine.inputOperator(CalcOperator.MULTIPLY);
break;
case ‘−’:
this.engine.inputOperator(CalcOperator.SUBTRACT);
break;
case ‘+’:
this.engine.inputOperator(CalcOperator.ADD);
break;
case ‘=’:
this.engine.evaluate();
break;
case ‘.’:
default:
this.engine.inputDigit(label);
break;
}
// 刷新页面状态
this.refreshDisplay();
}

build() {
Column() {
// 计算显示区域
Column() {
// 表达式预览
Text(this.expression)
.fontSize(16)
.fontColor(‘#999’)
.width(‘100%’)
.textAlign(TextAlign.End)
.margin({ top: 20, bottom: 4 })

    // 结果展示
    Text(this.display)
      .fontSize(this.hasError ? 32 : 48)
      .fontWeight(FontWeight.Bold)
      .fontColor(this.hasError ? '#E74C3C' : '#1a1a2e')
      .width('100%')
      .textAlign(TextAlign.End)
  }
  .width('100%')
  .padding({ left: 24, right: 24, top: 40, bottom: 20 })
  .layoutWeight(1)
  .justifyContent(FlexAlign.End)

  Divider().width('92%').color('#eee')

  // 按钮操作区域
  Column({ space: 12 }) {
    this.ButtonRow(['AC', '+/-', '%', '÷'])
    this.ButtonRow(['7', '8', '9', '×'])
    this.ButtonRow(['4', '5', '6', '−'])
    this.ButtonRow(['1', '2', '3', '+'])

    // 特殊布局行:0、小数点、等号
    Row() {
      Text('0')
        .fontSize(26)
        .fontColor('#fff')
        .width(136).height(64)
        .textAlign(TextAlign.Center)
        .backgroundColor('#3a3a4a')
        .borderRadius(32)
        .onClick(() => this.onButtonClick('0'))

      Text('.')
        .fontSize(26)
        .fontColor('#fff')
        .width(64).height(64)
        .textAlign(TextAlign.Center)
        .backgroundColor('#3a3a4a')
        .borderRadius(32)
        .onClick(() => this.onButtonClick('.'))

      Text('=')
        .fontSize(26)
        .fontColor('#fff')
        .fontWeight(FontWeight.Bold)
        .width(64).height(64)
        .textAlign(TextAlign.Center)
        .backgroundColor('#1a1a2e')
        .borderRadius(32)
        .onClick(() => this.onButtonClick('='))
    }
    .width('100%')
    .height(64)
    .padding({ left: 16, right: 16 })
  }
  .width('100%')
  .padding({ bottom: 24 })
}
.width('100%')
.height('100%')
.backgroundColor('#F8F9FA')

}
}
在这里插入图片描述

六、开发踩坑总结(新手必看)
坑点1:原生字符展示不规范
常规 *、-、/ 符号展示粗糙,替换为标准Unicode数学符号 ×、−、÷,UI更专业,同时枚举统一适配,避免逻辑匹配失效。
坑点2:JS浮点精度丢失
原生 0.1+0.2=0.30000000000000004,通过 toFixed(10) 保留小数+ parseFloat 去零,完美修复精度问题。
坑点3:连续运算与运算符替换失效
未做状态判断会出现「3+×5」非法表达式,通过 _isNewEntry 状态判断,支持未输入数字时直接替换运算符,输入数字后自动执行连续运算。
坑点4:结算后数字输入错乱
结算8后点击7会变成87,通过 _justEvaluated 标记,结算后点击数字自动清空历史结果,开启新计算。
坑点5:固定按钮宽度适配失效
固定宽度在不同屏幕适配错乱,使用 calc 动态计算宽度,适配所有鸿蒙设备屏幕。

七、功能测试用例(全覆盖验证)
开发完成后可自行测试,所有场景均已适配:
•基础运算:3+5=8、9-2=7、6×4=24、8÷2=4
•连续运算:3+5+2=10、9×2-3=15
•小数精度:0.1+0.2=0.3、1.5×2=3
•容错处理:5÷0=错误、禁止重复小数点
•辅助功能:50%=0.5、5切换为-5
•运算符替换:5+×3=15

八、项目拓展方向
基础功能完成后,可自主迭代升级,打造完整版商用计算器:
•新增科学计算模式:三角函数、幂运算、开方、对数计算
•数据持久化:通过Preferences存储历史计算记录
•主题适配:新增深色/浅色模式切换
•交互优化:添加按钮点击动画、触感反馈
•拓展功能:退格删除、历史记录查询与复用

九、项目总结
这款MVVM架构计算器,是鸿蒙NEXT新手从基础UI开发进阶到业务逻辑开发的标杆项目。不同于简单的页面编写,本项目核心培养分层开发思维、状态管理思维、边界容错思维。
通过实战你将掌握:ArkUI声明式布局与Builder组件复用、@State响应式状态同步、MVVM分层解耦开发、复杂业务逻辑封装、前端浮点精度修复、全场景边界问题处理。
吃透本项目,基本可以搞定90%的鸿蒙基础交互类项目,为后续复杂应用开发筑牢基础!

Logo

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

更多推荐