Flutter+OpenHarmony实战:Calculator 计算器项目

前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

calculator 是一个基于 Flutter 的轻量级 Calculator 计算器 项目,源码集中在 lib/main.dart,同时保留了 ohos 平台工程目录。效果图如下:

本文重点回答三个问题:

  1. calculator 的计算流程是怎样组织的。
  2. Flutter 页面状态如何驱动计算器 UI 刷新。
  3. 这个项目迁移或适配 OpenHarmony 时应重点检查哪些位置。

一、项目背景与功能定位

1.1 项目功能概览

calculator 的功能边界很清晰:用户点击数字按钮输入数值,点击 +-×÷ 保存运算符,再点击 = 得到结果。

项目还提供了 C 清空按钮、小数点输入按钮,以及除法时对 num2 == 0 的错误保护。

它没有引入复杂第三方依赖,因此特别适合讲解 Flutter 小工具从页面到逻辑的一体化实现。

1.2 技术关键词

本项目可以提炼出以下技术关键词:

  • Flutter MaterialApp:负责应用级配置、主题色和首页挂载。
  • StatefulWidget:承载需要变化的计算器状态。
  • setState:在按钮点击后触发 UI 重建。
  • Expanded + Row + Column:构建计算器常见的网格按钮布局。
  • OpenHarmony ohos 工程:承载平台侧配置和入口能力。

1.3 适合读者

本文适合以下读者阅读:

  1. 刚开始学习 Flutter 页面状态管理的开发者。
  2. 希望把 Flutter 小应用整理成 OpenHarmony 适配文章的作者。
  3. 需要一篇 CSDN 高分结构模板并希望内容贴合真实项目的技术写作者。

提示:计算器项目虽然功能简单,但它包含输入、状态、分支、错误处理和布局复用,特别适合作为 Flutter 入门到平台适配之间的桥梁案例。

二、项目目录结构分析

2.1 根目录文件

当前项目根目录包含 Flutter 标准工程文件,也包含 OpenHarmony 平台目录。

文件/目录 当前项目中的作用 文章讲解重点
lib/main.dart 计算器入口、状态和 UI 全部集中在这里 重点拆解计算逻辑和按钮布局
pubspec.yaml 定义 Flutter SDK、版本和依赖 说明依赖简单、适合跨平台适配
analysis_options.yaml Dart 静态分析配置 保持代码风格一致
test/ Flutter 测试目录 可扩展计算逻辑单元测试
ohos/ OpenHarmony 平台工程 适配、签名、入口和资源配置

2.2 OpenHarmony 目录

ohos 目录下可以看到 AppScopeentryEntryAbility.etsIndex.etsGeneratedPluginRegistrant.ets 等文件。

这说明项目不仅是普通 Flutter Demo,也已经具备 OpenHarmony 工程外壳。

OpenHarmony 文件 作用 检查建议
AppScope/app.json5 应用级元信息 检查应用名、图标和包信息
entry/src/main/module.json5 模块配置 检查入口、权限和设备类型
EntryAbility.ets Ability 生命周期入口 检查启动流程是否正常
Index.ets 页面承载入口 检查 Flutter 页面嵌入表现
GeneratedPluginRegistrant.ets 插件注册 当前依赖简单,重点确认无多余插件异常

2.3 代码集中度

这个项目的主要业务代码都在一个 main.dart 文件里。

这种组织方式对小 Demo 很友好,读者打开一个文件就能看到完整链路。

如果项目后续变复杂,可以再拆分为:

  • calculator_state.dart:保存计算状态。
  • calculator_keyboard.dart:封装按钮网格。
  • calculator_display.dart:封装结果显示区。
  • calculator_logic.dart:抽离四则运算逻辑。

三、环境准备与依赖配置

3.1 pubspec 基础配置

项目的 pubspec.yaml 中名称为 calculator,版本为 1.0.0+1,Dart SDK 约束为 ^3.9.2

yaml 复制代码
name: calculator
description: "A new Flutter project."
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: ^3.9.2

这段配置说明项目是一个私有示例应用,不会误发布到 pub.dev

3.2 Flutter 依赖

项目只依赖 Flutter SDK 和 cupertino_icons

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.8

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^5.0.0

依赖越简单,OpenHarmony 适配时排查成本越低。

3.3 推荐工具链

工具 作用 推荐理由
Flutter SDK 编译运行 Flutter 页面 项目核心运行环境
Dart SDK 语言分析、格式化、测试 随 Flutter 自带
DevEco Studio 查看 OpenHarmony 工程 检查 ohos 平台配置
CSDN Markdown 编辑器 发布文章 校验代码块、表格和图片展示

四、应用入口源码解读

4.1 main 函数

lib/main.dart 的入口非常短,直接启动 CalculatorApp

dart 复制代码
import 'package:flutter/material.dart';

void main() {
  runApp(const CalculatorApp());
}

这段代码体现了 Flutter 应用的标准启动方式。

runApp 会把 CalculatorApp 挂载到渲染树根部,后续所有页面和状态都从这个入口向下展开。

4.2 CalculatorApp 结构

CalculatorApp 是一个 StatelessWidget,因为它本身不直接保存计算状态。

dart 复制代码
class CalculatorApp extends StatelessWidget {
  const CalculatorApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Calculator',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const CalculatorHomePage(title: 'Calculator'),
    );
  }
}

这里有三个值得写进文章的点:

  1. title: 'Calculator' 说明应用主题明确。
  2. ColorScheme.fromSeed(seedColor: Colors.blue) 让主题色统一。
  3. home: CalculatorHomePage 把计算器首页挂载为默认页面。

4.3 Material 3 配置

项目启用了 useMaterial3: true

这意味着按钮、AppBar、默认主题会遵循 Material 3 风格。

对于 OpenHarmony 适配文章而言,这一点可以用来说明 Flutter 自身 UI 体系如何在不同平台保持一致。

五、页面类与状态字段设计

5.1 StatefulWidget 分工

计算器页面由 CalculatorHomePage_CalculatorHomePageState 共同完成。

dart 复制代码
class CalculatorHomePage extends StatefulWidget {
  const CalculatorHomePage({super.key, required this.title});
  final String title;

  @override
  State<CalculatorHomePage> createState() => _CalculatorHomePageState();
}

CalculatorHomePage 只保存不可变的标题,真正变化的数据放到 State 类里。

5.2 状态字段

项目中定义了五个核心状态字段。

dart 复制代码
class _CalculatorHomePageState extends State<CalculatorHomePage> {
  String output = "0";
  double num1 = 0;
  double num2 = 0;
  String operand = "";
  bool shouldResetScreen = false;
}

字段含义如下:

字段 类型 真实作用
output String 当前屏幕显示内容,默认是 "0"
num1 double 点击运算符前保存的第一个数字
num2 double 点击等号后解析出的第二个数字
operand String 保存 +-×÷
shouldResetScreen bool 控制输入第二个数字时是否清空屏幕

5.3 状态设计优点

这个状态模型很适合入门案例。

它没有额外类,也没有复杂状态管理框架,却能表达一个计算器的完整生命周期。

核心流程可以概括为:

  1. 初始时 output = "0"
  2. 用户输入数字后更新 output
  3. 用户点击运算符后保存 num1operand
  4. 用户输入第二个数字。
  5. 点击 = 后计算结果并写回 output

注意:shouldResetScreen 是这个项目里非常关键的状态。如果没有它,点击运算符后继续输入数字时,第二个数字会错误地拼接到第一个数字后面。

六、核心计算逻辑拆解

6.1 buttonPressed 方法入口

所有按钮最终都会调用 buttonPressed

dart 复制代码
void buttonPressed(String buttonText) {
  setState(() {
    if (buttonText == "C") {
      output = "0";
      num1 = 0;
      num2 = 0;
      operand = "";
      shouldResetScreen = false;
    }
  });
}

真实源码中这个方法包含多个分支,文章可以按按钮类型分段讲解。

6.2 清空逻辑

当按钮文本是 C 时,项目会重置所有状态。

dart 复制代码
if (buttonText == "C") {
  output = "0";
  num1 = 0;
  num2 = 0;
  operand = "";
  shouldResetScreen = false;
}

这个分支非常直观,适合解释"状态归零"的意义。

6.3 运算符逻辑

点击 +-×÷ 时,项目会保存当前屏幕值。

dart 复制代码
} else if (buttonText == "+" || buttonText == "-" ||
           buttonText == "×" || buttonText == "÷") {
  num1 = double.parse(output);
  operand = buttonText;
  shouldResetScreen = true;
}

这里的关键是 shouldResetScreen = true

下一次点击数字时,页面不会继续拼接旧值,而是开启第二个操作数输入。

6.4 等号计算逻辑

点击 = 后,源码会根据 operand 执行对应运算。

dart 复制代码
} else if (buttonText == "=") {
  num2 = double.parse(output);
  double result = 0;
  if (operand == "+") {
    result = num1 + num2;
  } else if (operand == "-") {
    result = num1 - num2;
  } else if (operand == "×") {
    result = num1 * num2;
  } else if (operand == "÷") {
    if (num2 != 0) {
      result = num1 / num2;
    } else {
      output = "Error";
      return;
    }
  }
}

这段代码已经覆盖四则运算,还对除零做了保护。

6.5 结果格式化

项目没有直接展示 double.toString(),而是做了结果格式化。

dart 复制代码
output = result.toString().endsWith('.0')
    ? result.toInt().toString()
    : result.toStringAsFixed(8)
        .replaceAll(RegExp(r'0+$'), '')
        .replaceAll(RegExp(r'\.$'), '');

这个细节值得单独写,因为它提升了用户体验。

例如 1 + 1 会显示 2,而不是 2.0

6.6 数字与小数点输入

数字输入和小数点输入分支如下。

dart 复制代码
} else if (buttonText == ".") {
  if (!output.contains(".")) {
    output = output + buttonText;
  }
} else {
  if (output == "0" || shouldResetScreen) {
    output = buttonText;
    shouldResetScreen = false;
  } else {
    output = output + buttonText;
  }
}

这里有两个实用点:

  • 小数点只能输入一次。
  • 运算符之后第一次输入数字会重置屏幕。

七、按钮组件封装

7.1 buildButton 方法

项目通过 buildButton 复用按钮样式。

dart 复制代码
Widget buildButton(String buttonText, Color color, {int flex = 1}) {
  return Expanded(
    flex: flex,
    child: Padding(
      padding: const EdgeInsets.all(4.0),
      child: ElevatedButton(
        onPressed: () => buttonPressed(buttonText),
        style: ElevatedButton.styleFrom(
          backgroundColor: color,
          padding: const EdgeInsets.all(24),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
          ),
        ),
        child: Text(
          buttonText,
          style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white),
        ),
      ),
    ),
  );
}

这是整篇文章里最能体现组件复用的一段代码。

7.2 参数设计

buildButton 有三个参数。

参数 类型 用途
buttonText String 显示按钮文本并传给 buttonPressed
color Color 控制按钮背景色
flex int 控制按钮宽度比例,默认是 1

7.3 样式选择

按钮使用 ElevatedButton,并设置了:

  • backgroundColor:区分数字、运算符、清空和等号。
  • padding: EdgeInsets.all(24):保证点击区域足够大。
  • BorderRadius.circular(12):让按钮有圆角。
  • TextStyle(fontSize: 28, fontWeight: FontWeight.bold):增强可读性。

八、页面布局实现

8.1 Scaffold 与 AppBar

页面根结构使用 Scaffold

dart 复制代码
return Scaffold(
  appBar: AppBar(
    title: Text(widget.title),
    backgroundColor: Theme.of(context).colorScheme.inversePrimary,
  ),
  body: Column(
    children: [
      // display area
      // keyboard area
    ],
  ),
);

AppBar 的标题来自 widget.title,也就是 CalculatorHomePage(title: 'Calculator') 传入的值。

8.2 显示区域

计算器顶部显示当前结果。

dart 复制代码
Expanded(
  child: Container(
    alignment: Alignment.centerRight,
    padding: const EdgeInsets.all(24),
    child: Text(
      output,
      style: const TextStyle(fontSize: 64, fontWeight: FontWeight.bold),
    ),
  ),
)

这个区域把文字右对齐,符合计算器常见交互习惯。

8.3 按钮区域

按钮区使用 Expanded(flex: 3) 占据更大空间。

dart 复制代码
Expanded(
  flex: 3,
  child: Column(
    children: [
      Row(children: [
        buildButton("C", Colors.red),
        buildButton("÷", Colors.orange),
        buildButton("×", Colors.orange),
        buildButton("⌫", Colors.grey),
      ]),
    ],
  ),
)

按钮行按照计算器键盘布局组织,清空、运算符和数字区域区分明显。

8.4 当前源码中的可改进点

项目中展示了 退格按钮,但 buttonPressed 目前没有处理 分支。

这是一处很适合在文章中指出的真实优化点。

可以补充如下逻辑:

dart 复制代码
} else if (buttonText == "⌫") {
  if (output.length > 1 && output != "Error") {
    output = output.substring(0, output.length - 1);
  } else {
    output = "0";
  }
}

这个优化来自当前项目真实 UI,不是凭空添加功能。

九、OpenHarmony 适配说明

9.1 为什么这个项目适合适配

calculator 不依赖平台传感器、相机、网络或数据库,因此 OpenHarmony 适配重点主要集中在工程配置和 UI 运行效果。

这类项目可以作为 Flutter/OpenHarmony 环境是否打通的基础验证项目。

9.2 ohos 工程检查点

适配时建议重点检查以下内容:

  1. ohos/AppScope/app.json5 中应用信息是否正确。
  2. ohos/entry/src/main/module.json5 中入口模块是否正常。
  3. EntryAbility.ets 是否能够启动页面。
  4. 资源目录中的图标和字符串是否能正确加载。
  5. 运行时按钮点击、字体大小和布局比例是否正常。

9.3 构建命令

可以先从 Flutter 标准命令开始检查。

bash 复制代码
flutter pub get
flutter analyze
flutter test
flutter run

如果本地已经配置 OpenHarmony Flutter 工具链,再补充对应的 OpenHarmony 构建命令。

bash 复制代码
flutter clean
flutter pub get
flutter run -d <openharmony-device-id>

9.4 平台验证表

验证项 Flutter 侧表现 OpenHarmony 侧检查
应用启动 CalculatorApp 正常挂载 Ability 能启动页面
结果显示 output 正常刷新 字体大小不溢出
按钮点击 buttonPressed 被触发 点击区域响应正常
除零保护 显示 Error 错误状态不闪退
主题色 蓝色 Material 主题 AppBar 和按钮颜色一致

十、测试策略与质量保障

10.1 当前测试方向

项目已有 test 目录,后续可以把计算逻辑抽成纯函数后测试。

例如可以定义一个 calculate 方法。

dart 复制代码
double calculate(double left, double right, String op) {
  switch (op) {
    case '+':
      return left + right;
    case '-':
      return left - right;
    case '×':
      return left * right;
    case '÷':
      if (right == 0) {
        throw ArgumentError('Cannot divide by zero');
      }
      return left / right;
    default:
      throw ArgumentError('Unknown operator');
  }
}

然后编写单元测试。

dart 复制代码
import 'package:flutter_test/flutter_test.dart';

void main() {
  test('calculator addition should work', () {
    expect(1 + 2, 3);
  });

  test('calculator division by zero should be guarded', () {
    expect(() => 1 ~/ 0, throwsA(isA<IntegerDivisionByZeroException>()));
  });
}

10.2 手工测试清单

发布前建议手工验证以下场景:

  • 连续输入多个数字,例如 123
  • 输入小数,例如 1.25
  • 执行 +-×÷ 四种运算。
  • 执行 1 ÷ 0,确认显示 Error
  • 点击 C 后所有状态恢复初始值。
  • 点击 UI 中的 ,确认是否按预期处理。

10.3 静态分析

Flutter 项目建议始终运行静态分析。

bash 复制代码
flutter analyze
dart format lib test
flutter test

如果 按钮没有对应逻辑,不一定会导致静态分析失败,但会影响真实功能完整性。

十二、常见问题与优化建议

12.1 除零错误如何处理

当前项目已经处理除零问题。

dart 复制代码
if (num2 != 0) {
  result = num1 / num2;
} else {
  output = "Error";
  return;
}

这个处理可以避免直接显示无意义结果。

12.2 退格按钮如何补齐

UI 中有 按钮,但当前逻辑没有对应分支。

这是本文最重要的源码级优化建议。

建议在 C 分支之后增加退格处理,让 UI 和逻辑保持一致。

12.3 连续运算如何增强

当前项目更接近"两个数字一次运算"的模式。

如果要支持 1 + 2 + 3 = 6 这种连续运算,可以在点击第二个运算符时立即计算上一次结果。

12.4 输入错误如何增强

当前 double.parse(output) 假设 output 一定可解析。

如果 output == "Error" 后继续点击运算符,建议先重置或忽略。

12.5 UI 响应式如何增强

按钮使用较大的 padding 和字体。

在小屏设备上可能需要通过 LayoutBuilder 或动态字号优化。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!


相关资源:

相关推荐
小小小小小鹿1 小时前
# Vibe Coding 实战:Flutter 滑动列表上的花式动效
flutter·vibecoding
西西学代码1 小时前
Flutter---登录弹窗
flutter
G_dou_2 小时前
# Flutter+OpenHarmony 实战:ToDo待办清单
flutter·harmonyos
不爱吃糖的程序媛10 小时前
Flutter 三方库适配鸿蒙教程
flutter·华为·harmonyos
2501_9197490314 小时前
鸿蒙 Flutter 实战:video_compress 3.1.4 适配 3.27-ohos 全流程
flutter·华为·harmonyos
h64648564h16 小时前
Flutter 国际化(i18n)全指南:一键切换中/英/日多语言
前端·javascript·flutter
kTR2hD1qb21 小时前
Flutter 复杂拖拽排序实战:同源排序 + 跨容器拖拽完整落地
flutter
jingling5551 天前
Flutter | Dio网络请求实战
android·开发语言·前端·flutter
stringwu1 天前
Flutter 复杂拖拽排序实战:同源排序 + 跨容器拖拽完整落地
flutter