
1. 项目背景与整体思路最近开始折腾鸿蒙应用开发发现跨端方案绕不开 Flutter。作为一个常年写原生 Android 的老开发我一开始对 Flutter 是有点抗拒的总觉得“又是套壳”但真正上手后发现Flutter 在鸿蒙上的适配远比想象中成熟。鸿蒙的 ArkUI 和 Flutter 的 Widget 体系虽然理念不同但核心的响应式思路是相通的尤其对于想要一套代码同时覆盖 Android、iOS、鸿蒙的团队来说Flutter 几乎是一个绕不开的选择。这篇是我自己学习 Flutter 的第一天记录主题锁定在“变量与基本类型”但我不想只讲语法而是直接拿“简易计算器逻辑”当靶子把变量、类型、运算、状态管理这些基础概念全部揉进一个可运行的小项目里。为什么选计算器因为计算器逻辑浓缩了编程语言最核心的几个要素输入、存储、运算、输出。Dart 的变量声明方式、数值类型、字符串处理、类型转换、条件分支在一台计算器里全都能用上。而且计算器的 UI 不复杂能让我把注意力放在 Dart 语法和 Flutter 的状态更新机制上不至于一上来就被复杂的布局拖垮。这篇内容适合两类人一类是完全没接触过 Flutter、想从鸿蒙跨端入门的开发者另一类是写过 React Native 或者 uni-app、想对比一下 Flutter 和它们差异的同学。我会从 Dart 的基础语法写起一直到计算器核心逻辑的完整实现最后附上我实际调试时踩过的坑。关于鸿蒙适配需要先明确一件事Flutter 官方目前对鸿蒙的支持是通过 OpenHarmony 社区的 flutter_flutter 分支来完成的并不是默认的 stable 分支。所以如果你在鸿蒙设备上直接跑官方 Flutter 项目大概率会遇到编译问题。我的做法是用社区的 flutter-sdk 版本配合 DevEco Studio 创建鸿蒙工程再把 Flutter 模块嵌进去。后面章节我会给出具体的环境配置步骤但先提醒一句别用默认的 Flutter 命令创建项目然后指望它在鸿蒙上直接跑除非你做足了 SDK 替换的功课。2. 环境搭建与工程结构2.1 鸿蒙侧需要准备什么在写任何代码之前先把环境理清楚。鸿蒙开发目前有两个方向一个是 HarmonyOS NEXT 的纯 ArkTS 开发另一个是兼容 Flutter 的跨端开发。我们要做的是后者所以需要安装的工具包括DevEco Studio用来创建鸿蒙工程、OpenHarmony 的 Flutter SDK带 flutter_flutter 分支、以及 Android SDKFlutter 引擎编译时会有部分依赖。这里有个容易混淆的点Flutter 的鸿蒙适配并不是直接用华为官方 SDK而是通过 OpenHarmony 社区的适配层把 Flutter 引擎编译成鸿蒙的 hap 包。因此你在配置环境变量时要把FLUTTER_STORAGE_BASE_URL和PUB_HOSTED_URL指向镜像源这一步对国内开发者来说基本是必须的否则拉依赖会慢到怀疑人生。我实际用的配置版本是Flutter 3.7.12OpenHarmony 分支、DevEco Studio 4.0、OpenHarmony SDK API 9。这三个版本搭配比较稳定网上有太多人用最新的 Flutter 3.10 结果编译报一堆 C 错其实不是代码问题是适配分支还没跟上。如果你第一次搭环境建议严格按照社区 README 里的版本对应关系来别追新。2.2 Flutter 工程与鸿蒙工程怎么融合Flutter 官方项目结构和鸿蒙工程是两个完全不同的体系。Flutter 项目根目录有pubspec.yaml、lib/目录鸿蒙工程则有entry/、oh-package.json5等。融合的方案是先用flutter create创建 Flutter 工程然后通过 DevEco Studio 的“导入 Flutter 模块”功能或者手动把 Flutter 工程作为依赖添加到鸿蒙工程的oh-package.json5里。我更推荐手工方式因为 IDE 的导入功能在版本不匹配时经常抽风。具体做法在鸿蒙工程的oh-package.json5的dependencies里加入 flutter 模块的本地路径类似这样{ dependencies: { flutter: file:./flutter_module } }然后把 Flutter 工程里的lib、pubspec.yaml、.dart_tool等关键文件/目录都放在flutter_module下。之后在鸿蒙的EntryAbility.ets里通过FlutterAbility加载 Flutter 页面。这一步不算复杂但有个大坑如果 Flutter 工程里用了flutter build生成产物鸿蒙工程并不会自动识别需要你先在 Flutter 目录下执行flutter build hap再把生成的 hap 文件放到鸿蒙工程的资源目录里。所以日常开发流程是先改 Flutter 代码然后构建再刷新鸿蒙工程最后运行到设备。听起来繁琐实际习惯以后还好。2.3 依赖与镜像配置的零碎经验我这里说的镜像源不是指网络代理而是指 Flutter 的软件仓库镜像。国内开发者必须要改两个环境变量FLUTTER_STORAGE_BASE_URL设为https://storage.flutter-io.cnPUB_HOSTED_URL设为https://pub.flutter-io.cn。不设置的话flutter pub get会卡在下载包那里。另外鸿蒙适配版的 Flutter SDK 中有一些原生编译产物需要从华为的仓库拉取你需要在鸿蒙工程的build-profile.json5中配置华为的 maven 仓库地址这个在官方 README 里有写但很多人会漏掉。踩过一次很深的坑我一开始用系统默认的 Flutter SDK没有切换到 OpenHarmony 分支结果在 DevEco Studio 里编译时提示找不到flutter.h折腾了半天才意识到是 SDK 选错了。所以最重要的一条经验先确认你的flutter doctor里显示的版本带不带-ohos后缀如果没有赶紧重装。3. Dart 变量与基本类型深度拆解3.1 变量声明var、final、const 的适用场景计算器逻辑里会不断用到临时值、常量、以及可变的运算结果所以第一天必须先搞懂 Dart 的三种变量声明方式。var是可变变量类型由首次赋值推断比如var currentInput 0之后可以改成123但不能重新赋值为数字类型因为 Dart 是强类型语言类型一旦推断就锁死了。final表示只能赋值一次适合声明像maxDisplayLength这种固定配置。const是编译期常量必须是可以在编译时确定的值比如const pi 3.14159不能用const声明一个需要运行时计算的结果。在计算器里典型的用法是这样的final ListString _history []表示历史记录列表内容可变但列表引用不可变var _display 0表示当前显示的数字随时要改。很多人一开始分不清final和const我自己的记忆方式final管“运行时只能赋值一次”const管“编译时就锁死”。如果你写const time DateTime.now()编译器会直接报错因为DateTime.now()不是编译期常量。3.2 基本类型num、int、double、String、bool 的边界Dart 和 Java 不太一样数字类型不是两个完全独立的类型而是有一个共同的父类num。int和double都是num的子类。这个设计带来一个便利你可以声明num x 1然后改成x 2.5因为num类型同时接受整数和浮点数。但在计算器逻辑中我不建议用num因为用户输入的小数点和运算结果对精度要求很高用num会让类型检查变得宽松容易出问题。稳妥的做法是整数运算用int涉及除法和小数一律用double或者干脆用Decimal库后面会讲。字符串在 Dart 里是String不可变对象每次拼接都会创建新字符串。计算器显示面板我用的就是String因为用户输入的过程天然是字符串拼接按“1”变成1再按“2”变成12按小数点变成12.。如果用数字类型去存储输入过程会很麻烦比如输入“0.1”时如果直接用 double 表示会丢失精度。所以计算器开发的一个关键技巧是在用户输入阶段用字符串存储在按下等号时再转换成数字计算。布尔类型bool在计算器中主要用来控制状态比如_isNewNumber表示“当前是否是新数字的起点”按完运算符后下一次按数字应该清空显示区这个标志位就是bool的核心作用。3.3 类型转换与运算符计算器逻辑的隐藏地雷计算器代码里最常踩的坑就是类型转换。用户按下等号时我需要把字符串12.5转成double用double.parse()。但如果用户输入的是12.double.parse()会直接抛异常。所以我在转换前必须先做校验比如用正则RegExp(r^\d\.?\d*$)来判断字符串是不是合法数字。另一个地雷是 Dart 的整数除法。比如5 ~/ 2结果是2取的是商的整数部分而5 / 2结果是2.5类型是double。如果你做整除运算却用了/然后期待结果是整数就会拿到一个 double显示时可能出现2.0而不是2。计算器里我们通常希望8 / 4显示2而不是2.0所以需要写一个格式化函数如果结果是整数就去掉小数点后面的零。运算符方面Dart 支持 - * / % ~/其中%是取余~/是整除。计算器逻辑里我暂时只用了前四个取余留给后续扩展。还有一个容易忽略的知识点Dart 中没有和--运算符虽然支持但建议别用因为可读性不好的代码在计算器这种逻辑密集的场景里会很难排查。我个人写计算器时用current current 1简单直白。4. 简易计算器逻辑实现4.1 整体状态设计与输入流程计算器界面只做一件事显示当前输入的表达式和结果。我设计的状态字段只有这几个String _display 0; // 当前显示内容 String _expression ; // 完整表达式例如 1234 bool _isNewNumber true; // 是否开始输入新数字 double _result 0; // 上次运算结果 String _lastOperator ; // 上次的运算符 bool _isCalculated false; // 是否刚按过等号整个输入流程可以分成三种动作数字输入、运算符输入、等号输入。数字输入时需要判断_isNewNumber是否为 true如果是就清空当前显示并填入新数字否则把数字拼接到显示字符串末尾。运算符输入时如果已经有一个运算符就先计算前一步的结果再把新运算符存下来等号输入时执行完整计算并显示结果。这套状态机的思路是所有计算器 App 的核心不管 UI 怎么变逻辑都是这个套路。4.2 数字输入与小数点处理数字输入函数我这样写void _onDigitPressed(String digit) { if (_isNewNumber) { _display digit; _isNewNumber false; } else { // 如果用户输入的是小数点要检查是否已经存在小数点 if (digit . _display.contains(.)) { return; } // 处理前导零如果当前显示只有 0并且输入的不是小数点则替换 if (_display 0 digit ! .) { _display digit; } else { _display _display digit; } } }这里最重要的判断是“重复小数点”。真实计算器里按3.14再次按小数点不会变成3.14.而是被忽略。我用_display.contains(.)判断即可。另一个隐藏细节是前导零如果当前显示是0用户再按5显示应该变成5而不是05。所以我加了if (_display 0)的判断。4.3 运算符计算与连续运算运算符输入时我选择维护一个_pending结构而不是每次都处理完整表达式。简化版逻辑如下void _onOperatorPressed(String op) { if (_isCalculated) { // 如果刚按过等号那么当前显示有结果直接用结果参与下轮运算 _expression _display; _isCalculated false; } else if (_lastOperator.isNotEmpty !_isNewNumber) { // 如果已有运算符并且当前数字不是新数字说明需要先算前一步 _calculateIntermediate(); } _lastOperator op; _expression _expression op; _isNewNumber true; } void _calculateIntermediate() { double currentNum double.parse(_display); double? storedNum _storedValue; if (storedNum ! null) { _result _applyOperator(storedNum, currentNum, _lastOperator); _display _formatResult(_result); _storedValue _result; } else { _storedValue currentNum; } }这里有个容易出错的地方用户输入12 34 56第一次按时应该把12存起来第二次按时应该先算12 34 46再把46存起来。所以我在_calculateIntermediate里用_storedValue存储中间结果。_applyOperator根据运算符类型执行对应的运算double _applyOperator(double a, double b, String op) { switch (op) { case : return a b; case -: return a - b; case *: return a * b; case /: if (b 0) { throw Exception(除数为零); } return a / b; default: return b; } }除数为零的判断必须放在 switch 里不提前拦的话Dart 的 double 除法不会抛异常而是返回Infinity在界面上会显示一个很奇怪的Infinity用户根本看不懂。4.4 等号计算与结果格式化等号是计算器的高潮。我选择维护一个“当前表达式字符串”的方法_expression里存了类似1234这样的内容。按等号时我用一个简单的四则运算解析器去计算它。第一天我没有引入复杂的 AST 解析只处理最简单的“数字 运算符 数字”结构所以等号函数是这样void _onEqualsPressed() { if (_lastOperator.isEmpty) return; double left _storedValue ?? double.parse(_expression.split(_lastOperator).first); double right double.parse(_display); double result _applyOperator(left, right, _lastOperator); _display _formatResult(result); _expression _display; _lastOperator ; _isCalculated true; _isNewNumber true; }这段逻辑对“连续等号”没有处理比如按2 3 第二下应该得到8但第一天我们先不扩展。_formatResult函数用来去掉多余的.0String _formatResult(double value) { if (value value.roundToDouble()) { return value.toInt().toString(); } return value.toString(); }这个函数很关键。2.0 3.0用 double 计算得到5.0toString()会返回5.0但用户期待的是5。所以判断一下如果值等于它的整数部分就转成int再输出。4.5 UI 层状态刷新Flutter 的 setState 与鸿蒙的联动计算器的逻辑写完后要渲染到 Flutter 的 UI 上。Flutter 是声明式 UI状态变化时调用setState触发重绘。我的CalculatorScreen是一个StatefulWidget里面维护上述所有状态每次数字或运算符按钮点击时更新状态后调用setState(() {})显示文本自然刷新。这里要说一个 Flutter 和鸿蒙 ArkUI 的差异ArkUI 使用的是State装饰器变量变化后 UI 自动更新Flutter 则需要手动调用setState很多从鸿蒙转过来的同学会忘记这一步导致界面看起来“没反应”。调试的时候可以加一行print(_display)确认逻辑更新了只是没刷新 UI再接上述缺的setState。在鸿蒙工程里嵌入 Flutter 页面时UI 的刷新机制不变还是 Flutter 那套。鸿蒙原生侧和 Flutter 页面的通信是通过MethodChannel来实现的但计算器这种纯前端逻辑完全用不到只要 Flutter 模块能正常显示就行。如果后续要把计算结果回传给鸿蒙侧才需要写通道。5. 常见问题与排查技巧实录5.1 编译问题版本不匹配导致的各种诡异报错我用的是 OpenHarmony 的 Flutter 分支第一天就遇到一个报错Error: The method isnt defined for the class Object。这个错特别迷惑因为我写的代码里根本没有比较。后来查了才知道是 Flutter SDK 分支里的dart:core版本和 DevEco Studio 自带的 Dart 插件版本不匹配。解决办法是去 OpenHarmony 的 flutter 仓库重新 checkout 对应版本的 SDK或者用同一个版本来源的 Dart SDK。我最后的配置是 Flutter 3.7.12 配 Dart 2.19.6再往上就容易出问题。另一个高频问题是构建时提示flutter build hap找不到hap命令。这是因为没有安装ohos插件。需要在 Flutter 工程下执行flutter pub add flutter_ohos而且flutter_ohos插件的版本必须和你用的 Flutter SDK 版本对应否则就算命令存在构建时也会报错Cannot find builder ohos。5.2 逻辑问题除数为零与精度丢失逻辑层面的坑比编译更隐蔽。我在_applyOperator里做了除数为零的异常抛出但第一次写的时候我选择在 UI 层捕获异常并显示“错误”。实际测试时发现除数为零用 double 做除法不会报错而是返回Infinity所以_applyOperator里必须显式检查。另外小数运算精度问题在计算器里非常严重比如0.1 0.2的结果是0.30000000000000004直接展示很尴尬。如果你要求严格的十进制精度需要引入decimal包final Decimal a Decimal.parse(0.1); final Decimal b Decimal.parse(0.2); final Decimal sum a b; // 0.3但用decimal包需要额外依赖而且所有运算符重载都变了代码侵入性较大。第一天我采用了一个折中方案计算完成后用_formatResult展示时保留十位小数并去掉尾部零String _formatResult(double value) { String s value.toStringAsFixed(10); s s.replaceFirst(RegExp(r0$), ); s s.replaceFirst(RegExp(r\.$), ); return s; }这样0.1 0.2会先变成0.3000000000000去掉尾部的零后是0.3效果基本能满足常见场景。5.3 UI 布局问题按钮溢出与字号自适应计算器的按钮是网格排列我用GridView.count(crossAxisCount: 4)来排列但测试过程中发现在横屏或小屏设备上按钮高度会被压扁。解决方法是把按钮放在AspectRatio(aspectRatio: 1)的容器里或者用Expanded让按钮总是占满等分的空间。另一个问题是显示区域当输入特别长的算式时文本会溢出屏幕边界。我给显示文本加了FittedBox(fit: BoxFit.scaleDown)来自动缩放字号实测下来很稳。如果你在鸿蒙设备上运行还要注意状态栏高度。鸿蒙的状态栏和 Android 不尽相同Flutter 默认使用MediaQuery.padding.top来计算安全区但如果鸿蒙适配层没有正确传递这个值页面顶部会被状态栏遮挡。我遇到过一次解决办法是在 Flutter 页面外层套一个SafeArea强制避开系统的安全区域。5.4 调试技巧用好debugPrint和 DevTools计算器状态多用debugPrint把每个按钮触发后的_display、_expression、_lastOperator打印出来可以非常清晰地看到状态机怎么流转。比如按1 2 我打印出来的日志应该是[onDigit] display1, exp [onOp] op, exp1, storednull [onDigit] display2, exp1, stored1 [onEquals] left1, right2, result3 [display] 3如果日志显示stored1但第二次按的时候没有更新stored说明_calculateIntermediate的分支走错了。DevTools 的 Flutter Inspector 也能看 Widget 树但计算器这种简单页面用不上主要是调试 UI 布局的时候可能需要。6. 实操心得与后续扩展6.1 第一天最容易走的弯路我最初的想法是把表达式1234*56存成字符串然后等号时用eval直接算。但是 Dart 没有eval函数这反而是一件好事所以我被迫去手动解析。这个“被迫”让我搞清楚了状态机的本质计算器不是一次解析所有表达式而是边输入边累积中间结果。所以建议你也别去找什么表达式引擎库先把最简单的前缀中缀逻辑手写一遍这对理解“状态管理”非常有帮助。另一个弯路是过度设计。我一开始想用枚举定义所有运算符还打算支持括号结果第一天光维护运算符的状态就花了三个小时。后来我把需求砍到只剩加减乘除代码立马清爽了。新手学 Flutter 千万不要一上来就求大而全计算器这种项目就适合一点点加功能。6.2 关于const与final的实操建议我在代码里用了很多final而不是const。因为计算器的按钮数据是在initState里生成的虽然内容不变但运行时才确定只能用final。只有像MAX_LENGTH 12这种才是const。这个区分在团队 Code Review 时会比较严格养成习惯以后写复杂项目不容易出错。6.3 后续扩展计划支持连续运算与括号今天的计算器只支持a op b 下一步我打算把解析器升级成支持“加减乘除优先级”的版本。方案是用一个简单的递归下降解析器把表达式拆成若干项每项由乘除符号连接整体由加减符号连接。这样12*3会正确得到7而不是9。这个扩展需要涉及更复杂的字符串解析但第一天不必着急。另外我还想加一个“退格”按钮它需要处理_display的子串删除substring操作以及当字符串只剩一位时的0回退这些都可以后续一点一点补。6.4 聊聊鸿蒙 Flutter 的实际体验最后说点个人感受。第一天在鸿蒙设备上跑 Flutter最大的感受是“社区适配的成熟度和官方版还有差距”主要体现在构建产物体积和启动速度上。我打出来的 hap 包比纯 ArkUI 的大不少启动时也能感觉到多了一层引擎加载。但开发体验确实香Dart 的强类型和 Flutter 的热重载让调试流畅很多改完代码几乎不要等待就能看到效果。如果你的项目需要覆盖鸿蒙和安卓Flutter 值得投入但建议先把 Dart 语法练扎实尤其是变量类型和状态管理这两块。计算器项目虽小但能把这两块的核心概念全部打通后面学 Provider、Riverpod 都会快很多。