记账本是移动端最经典的“入门+实战”项目,但一旦加上“同时兼容鸿蒙”这个前缀,工程的隐蔽复杂度立刻翻了几倍。我前阵子刚好把一款基于 Flutter 的记账应用从 Android/iOS 扩展到了鸿蒙设备,从环境配置、工程结构、核心账本功能实现,到原生能力桥接、HAR 封装、真机调试,完整路径走下来之后有不少值得沉淀的东西。这篇文章不聊虚的,只讲我在项目里做了什么、为什么这么做、踩了哪些坑、最后怎么解决。如果你正打算用 Flutter 统一三端,或者已经在给 Flutter 应用做鸿蒙适配,这篇的内容应该能帮你省下不少试错时间。
1. 项目选型复盘:记账本这类工具应用为什么适合 Flutter + 鸿蒙
1.1 记账应用的真实技术诉求不是炫技,而是低成本多端交付
记账类 App 的页面其实不复杂,无非是账单列表、记账弹窗、分类管理、统计报表、设置页这几个核心模块,但业务细节极其琐碎。真正折腾人的地方在于金额键盘的交互、分类数据的维护、不同时间维度的统计口径、周期性账单等。一套像样的记账产品,开发重心往往不是 UI 动效,而是长期的迭代稳定性与数据一致性。
如果这时候只有 Android 和 iOS 两端,Flutter 的优势早就被验证过了。真正需要决策的是“鸿蒙到底要不要做、怎么做”。我当时看了一圈,市面上很多团队的选择是再招一拨 ArkTS 的人重新写一版原生应用。可以理解,毕竟鸿蒙原生适配最干净,但代价也最直白:数据层、状态管理、页面交互全部重来第二遍。而记账本这种工具,用户非常在意历史账单不能错乱、跨端数据要一致,如果两套代码的逻辑分叉,后续维护绝对是一场灾难。
我最终选择 Flutter 作为底座,目标是一个代码库同时出 Android、iOS、鸿蒙三端。业务层尽量复用,只针对鸿蒙的系统能力做一层薄薄的适配层。从结果看,这个策略是成立的,因为我日常 90% 的改动仍然发生在 Dart 业务层,鸿蒙特判逻辑只集中在一个目录里。
1.2 Flutter、ArkTS、RN 与 KMP 的横向取舍
我把当时认真评估过的四条路线放在一起做了个对比,方便你自己判断:
| 候选方案 | UI 复用 | 逻辑复用 | 鸿蒙适配成熟度 | 我当时最大的顾虑 |
|---|---|---|---|---|
| ArkTS 原生单独开发 | 低 | 低 | 高 | 等于重新养一个技术栈,长期维护成本拉满 |
| React Native + 鸿蒙适配 | 中 | 中 | 中 | 第三方组件在鸿蒙上的兼容参差不齐 |
| Kotlin Multiplatform | 无 | 高 | 中 | UI 依然要维护两套,收益只覆盖一小半 |
| Flutter + ohos 支持 | 高 | 高 | 中高 | 部分原生插件无法直接使用,需要做桥接层 |
这四个方案里,ArkTS 的工程质量上限最高,但它是“单点最优”;Flutter 的方案不是任何单一平台上的“最优解”,却是全局综合成本最低的选择。React Native 和 KMP 更适合有特定生态绑定的团队,比如团队已经深度使用 RN,或者主要语言是 Kotlin。我们这个项目本身已经有 Flutter 代码资产,继续押注 Flutter + ohos 分支就是顺理成章的事。
这里多说一句:技术选型最怕只看“现在能不能跑”,更要看“以后谁来维护”。鸿蒙的普及速度不管怎么变化,一个能覆盖三端的小团队才是记账类项目的常态配置。
1.3 Flutter 跑鸿蒙的底层逻辑,理解清楚才不会瞎调
很多人以为 Flutter 跨鸿蒙是通过编译器把 Dart 代码转换成 ArkTS,其实不是。Flutter 能移植到鸿蒙,核心原因是它自绘引擎的架构设计:UI 渲染发生在 Flutter Engine 内部,和操作系统的原生控件树没有直接关系。Dart 代码跑在自己的运行时里,只需要底层把窗口创建、输入事件、纹理渲染、字体管理等系统能力接过来。
所以在 OpenHarmony 体系里,适配的关键是把 Flutter Engine 编译成鸿蒙能加载的动态库,让渲染环境对接方舟的能力接口。社区维护的 flutter_flutter 分支就是围绕这条链路持续迭代的。理解这个原理之后,有三个推论能直接指导你的工作:
第一,Flutter 版本不能随便升级。ohos 分支通常会滞后于 Flutter 官方版本,遇到新版本发布不要急着跟,先看分支是否同步。
第二,纯 Dart 的 pub 包大概率能在鸿蒙上直接用,但凡是调用了 Android/iOS 原生能力(MethodChannel、EventChannel、插件注册表)的包,几乎都需要找 ohos 适配版本,否则运行时就会报 MissingPluginException。
第三,调试时必须分清“Dart 代码逻辑问题”和“鸿蒙端桥接问题”,不要盲目怀疑跨平台框架本身,这能省下大量排查时间。
2. 环境搭建实操:Flutter 工具链与鸿蒙侧工程的连接方式
2.1 开发鸿蒙 Flutter 应用,三套环境缺一不可
很多人第一次上手就被环境搞到想放弃,这很正常。要跑 Flutter + 鸿蒙,你至少需要把下面三样东西对齐:
Flutter SDK:要使用带 ohos 支持的分支,而不是普通的 Flutter 主分支。主流方式是从 OpenHarmony SIG 的仓库拉取对应版本分支,或使用官方整合后的工具链。
HarmonyOS SDK:一般随 DevEco Studio 一起安装。安装后要在系统环境变量里显式指出来,例如配置
DEVECO_SDK_HOME指向 SDK 目录,否则后续构建会一直找不到 target。DevEco Studio:它和 Android Studio 的职责类似,负责管理鸿蒙侧工程、模拟器和签名信息。如果你只依赖命令行,很多调试入口会缺失。
以我实际项目为例,Flutter SDK 是通过 git 拉取的分支固定下来的。用命令行配置完环境变量后,一定要记得新开终端窗口,不然flutter doctor看到的还是旧环境。这个细节特别容易坑到刚装完环境的人:明明已经改了 path,当前终端还残留旧会话,于是怎么执行都报找不到命令。
建议环境准备好后用flutter doctor -v验证一遍,至少能看到 flutter、DevEco、OHOS SDK 的识别情况。如果 doctor 里没有 ohos 相关项,那大概率是 SDK 分支或环境变量没有匹配上。
2.2 工程结构准备:不要把鸿蒙代码混进 Dart 业务里
创建 Flutter 工程时,仍然用常规命令生成:
flutter create --org com.example --project-name flutter_ledger ledger_app这个命令默认只会生成 android、ios 等平台目录,鸿蒙侧工程通常需要手动集成或通过对应模板工具补齐。我不会把鸿蒙模板文件硬塞进 lib 目录,而是保持清晰分界。最终工程结构大概长这样:
ledger_app/ lib/ core/ db/ theme/ platform/ features/ home/ bill/ stats/ settings/ app.dart android/ ios/ ohos/其中 ohos 目录是鸿蒙侧的壳工程与桥接代码,lib 里单独建一个 platform 目录,专门存放会和鸿蒙原生交互的封装。给业务层提供的接口是纯 Dart 的抽象类,底层具体是走 MethodChannel 还是 EventChannel,业务代码完全不需要知道。这样以后如果插件库升级了,只需要替换 platform 目录内部实现,不会牵连页面层。
在开发节奏上,强烈建议先把项目在 Android 模拟器上跑通,再切到鸿蒙 target。不要一上来就搞三端并行,Flutter 业务逻辑本来就一次编写到处运行,最大的不确定性只在原生桥接层。Android 能跑通,至少说明 Dart 层没问题,聚焦去攻鸿蒙桥接即可。
2.3 构建与依赖配置:镜像、插件兼容清单要提前建立
依赖下载和普通 Flutter 项目没有本质区别,但国内网络环境下,pub 源和 Flutter 存储的访问速度会影响体验。我一般会在项目初始化时配置镜像环境变量:
export PUB_HOSTED_URL=https://pub.flutter-io.cn export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn这只是让依赖下载更快一些,不改变任何代码逻辑。真正要重点处理的是插件兼容清单。建议第一天就做一个表格,把项目用到的所有依赖列出来,标注“纯 Dart 包”还是“含原生代码包”。纯 Dart 的,比如 intl、path、collection 这类,闭眼用;含原生代码的,比如 sqflite、shared_preferences、image_picker、flutter_wechat 这类,就要逐个验证 ohos 支持情况。
我第一版就踩过一次:在 Android 侧用得好好的某个统计图表库,其实下面挂了一个原生性能检测模块,切到鸿蒙之后运行直接闪退。后来把它换成基于 Canvas 自绘的纯 Dart 版本才解决问题。所以在依赖选型阶段提前识别原生依赖,能帮你绕开大量隐蔽崩溃。
3. 记账本核心模块实现:从账本表结构到统计图表的完整落地
3.1 数据模型与本地存储:账本应用的地基是表设计
记账本这个场景,数据量一般不会大到需要上服务端数据库,但表结构设计仍然不能马虎。我设计的是最常见也最稳妥的三张表:分类表、账单表、预算表。分类表负责收入和支出的分类定义;账单表记录每笔消费流水;预算表按月保存预算金额。
以下是实际使用的核心建表语句:
CREATE TABLE category ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, icon TEXT, type INTEGER NOT NULL, -- 0 支出,1 收入 sort INTEGER DEFAULT 0 ); CREATE TABLE bill ( id INTEGER PRIMARY KEY AUTOINCREMENT, category_id INTEGER NOT NULL, amount REAL NOT NULL, type INTEGER NOT NULL, happened_at INTEGER NOT NULL, -- 毫秒时间戳 note TEXT, created_at INTEGER NOT NULL ); CREATE TABLE budget ( id INTEGER PRIMARY KEY AUTOINCREMENT, month TEXT NOT NULL, -- 格式 yyyy-MM amount REAL NOT NULL, UNIQUE(month) );数据库插件这块需要多说一句:Android/iOS 上 sqflite 非常顺手,但鸿蒙侧的 SQLite 支持目前更稳妥的做法是使用社区适配的方案,或者在 Dart 层通过 FFI 调用鸿蒙环境中的 sqlite 库。为了避免后续因为存储层卡住整个项目,我在 Repository 之上定义了数据访问抽象,底层无论用 sqflite 还是其他实现,上层代码都不需要改动。
比如账单查询抽象:
abstract class BillRepository { Future<Bill> create(Bill bill); Future<void> delete(int id); Future<List<Bill>> queryByMonth(DateTime month); Future<Map<String, double>> queryMonthlySummary(DateTime month); }这套抽象层的价值在后期适配时会被明显放大:你绝不会希望“把数据库框架从 A 换成 B”这种操作,导致几十个页面跟着改。
3.2 状态管理与路由组织:让“记一笔”之后全端自动刷新
记账应用使用频率最高的路径是:打开 App-记一笔-回到列表-看统计。其中隐藏着一个很容易被忽略的细节:当用户在记账页插入一条账单后,首页列表要更新、当月总支出要更新、统计图表要重新计算。如果用传统 setState 去手动控制,涉及跨页面的数据联动时很容易漏刷新。
我选了 Riverpod 来做全局状态管理。账单列表、月度汇总这类状态都通过 Provider 暴露,任何一笔新增或删除操作执行成功后,只需要让对应的 Provider 失效或调用刷新方法,全端所有监听该状态的页面都会同步更新。这样做的好处是,当页面越来越多时,你不需要去维护一张“修改后需要通知哪些页面”的清单,状态源是唯一的。
路由部分用了 go_router,因为记账本后续一定会增加更多二级页面,比如账单详情、分类编辑、预算设置。声明式路由比手动 Navigator.push 更好维护,尤其在需要深链跳转到某笔账单详情时,路径参数直接定义在路由表里,清晰得多。
3.3 账单录入页的关键交互:别让用户输入三次才完成一笔账
记账 App 的录入效率直接决定用户留存。我见过不少记账应用,用户记一笔账要点五六个控件、输入三个字段,极其繁琐。所以在实现账单录入页时,我做了几个刻意的设计。
第一,金额是默认焦点。进入页面后系统键盘自动弹出,用户可以直接输入数字,节省一次点击。第二,分类选择用“网格+最近使用优先”的排列方式,常用分类排在前面。第三,备注不是必填项,只有一个低调的输入入口,不打断记账主流程。
分类选择这里,我知道很多 Flutter 新手会用 CheckboxListTile 做多选分类,这其实是个伪需求:一笔账单只能属于一个分类,不应该做多选。但这个组件本身也值得提一下,因为在某种设置页上你可能确实需要多选能力。我在做设置页的预算提醒分类时用到过 CheckboxListTile,发现默认排版经常控制不住复选按钮和文字之间的距离。我的经验是,想微调文字与按钮间距,不要直接硬调组件内部,可以设置contentPadding或dense属性,如果还不能满足,就直接用 Row 包一个 Checkbox 加一个 Expanded Text 自定义,能控得更细腻。
金额输入还有一个容易忽略的边界:小数点只能出现一次,且输入法必须在某些机型上自动弹出数字键盘。这个我用输入格式化器统一处理,用户输入任何非法字符都会在上面拦截。
3.4 统计报表模块:用图表让用户感知“钱去哪了”
统计报表是记账本区别于普通“花销记录本”的核心功能。很多用户记账的目的是月底复盘,如果只能看到一条条流水,而不是按分类、按月份聚合出来的趋势,产品的价值会大打折扣。
在图表层次,我用了 fl_chart 的折线图和饼图。月度支出趋势用折线图展示,分类占比用饼图展示。关键数据不能只靠 Flutter widget 前端算,而是要在 SQL 层用聚合查询算好,道理很简单:如果当月有上千笔账单,把原始数据全部 load 到内存再逐条遍历,会带来无谓的性能消耗,对移动端很不友好。
实际用到的按月汇总查询大概是:
Future<List<MonthlySummary>> monthlySummary() async { final result = await db.rawQuery(''' SELECT strftime('%Y-%m', happened_at / 1000, 'unixepoch') AS month, SUM(CASE WHEN type = 0 THEN amount ELSE 0 END) AS expense, SUM(CASE WHEN type = 1 THEN amount ELSE 0 END) AS income FROM bill GROUP BY month ORDER BY month DESC LIMIT 12 '''); return result.map(MonthlySummary.fromMap).toList(); }拿到这些聚合数据后,前端图表只需要负责渲染,性能负担很轻。我实测过 5 年、近 2 万条账单的数据集,月度汇总页面秒开,这在原生 App 里也是很不错的体验。
3.5 设置页与主题细节:系统组件主题色也要统一管理
设置页通常会用到关于页面、开源许可列表等系统级页面。Flutter 的showLicensePage在开发阶段很实用,但它自身的主题颜色经常和 App 主色调不一致,尤其是当你的应用用了自定义 ColorScheme 时,默认推测的页面颜色可能显得突兀。
这里给一个省事方案:在设置页跳转时自定义主题作用域,给许可页包一层 Theme:
Theme( data: Theme.of(context).copyWith( colorScheme: Theme.of(context).colorScheme.copyWith( primary: AppColors.brandGreen, ), appBarTheme: const AppBarTheme( backgroundColor: AppColors.brandGreen, foregroundColor: Colors.white, ), ), child: const LicensePage(), )这样能保证所有跳转过去的系统页面和整体品牌色一致。不要总觉得“反正是系统页面,丑一点没关系”,用户对记账类应用的信任感很大程度来自细节的一致性。
4. 鸿蒙适配实战:从权限声明到 HAR 封装的完整步骤
4.1 鸿蒙工程的权限声明与基础配置
Flutter 应用在鸿蒙上最终会编译成 HAP 包,权限声明逻辑和 Android 的 AndroidManifest 类似,但写在module.json5里。比如如果记账本需要读取系统图片,用来做账单凭据,那么要事先在模块配置中声明:
{ module: { name: "entry", type: "entry", requestPermissions: [ { name: "ohos.permission.READ_MEDIA" }, { name: "ohos.permission.INTERNET" } ] } }这个步骤经常被忽略,因为在 Android 上有运行时权限弹窗,iOS 上也有使用说明。鸿蒙生态的一些权限策略也遵循类似的“最小化申请”原则。在开发阶段如果你发现系统服务调用没有反应或直接返回错误,第一反应应该去查权限有没有声明,而不是怀疑代码逻辑。
有些接口如果使用系统提供的系统选择器(比如拉起系统的图片选择器),反而不需要声明存储权限。所以最佳实践是:能用系统选择器的就用选择器,尽量避免直接读整个媒体库。
4.2 通过 MethodChannel 桥接鸿蒙系统能力
记账本里我实际用到的一个原生能力是“从系统图库选择一张凭证图”。Flutter 侧通过 MethodChannel 发起调用,鸿蒙原生侧在对应的 Ability 或 Extension 中实现方法。
核心流程是:Dart 侧声明 channel name 与方法名,原生侧注册同一个 channel,收到调用后使用 PhotoViewPicker 拉起图库选择,选择结果拿到 URI 后复制到应用沙箱目录,再把新路径返回给 Flutter。这样 Flutter 侧可以把返回路径直接当普通文件处理。
这里有三个容易被坑的地方:
第一,channel name 必须完全一致。Android 和鸿蒙侧如果复用了同一套 Dart 代码,原生端的 channel name 一点都不能差。
第二,从图库选出的 URI 不一定能直接读取。要先把源文件拷贝到应用自己的沙箱目录下,再返回给 Flutter 用 File 读取,否则可能因为持久化权限问题导致图片显示失败。
第三,如果原生方法没有注册成功,Flutter 侧会报 MissingPluginException,而不是返回一个空结果。捕获异常并弹出一个友好提示,比让用户面对红屏要好很多。
const platformChannel = MethodChannel('com.example.ledger/picker'); Future<String?> pickImage() async { try { final String? path = await platformChannel.invokeMethod('pickImage'); return path; } on MissingPluginException { debugPrint('picker plugin not available on this platform'); return null; } }4.3 微信登录等第三方登录在鸿蒙侧的注意事项
第三方登录是很多商业应用的刚需。针对热词里有人提到的“flutter 微信登录”,我多说一点自己在鸿蒙侧的体会。
如果你之前只是在 Android 上做过微信登录,千万不要把 Android 的接入逻辑直接平移到鸿蒙。微信在鸿蒙平台有自己独立的 SDK 和接入流程,需要去微信开放平台为鸿蒙应用单独申请 AppID 和对应的签名信息,AppID 和 Android 侧通常不能复用。如果你的 Flutter 侧选了现成的微信登录插件,要确认它是否已经做了 ohos 桥接,如果没有,就需要自己包一层 MethodChannel。
登录成功后的流程和传统平台类似,拿 code、换 token、再拿到 unionId 或 openId。潜在风险点在于回调地址和 scheme 配置。鸿蒙侧的拉起与回调机制和 Android 的 intent scheme 有一定差异,建议把回调处理都收敛到一个原生模块里,而不要让 Dart 层直接管 App 被拉起后的路由逻辑,否则排查问题时你会同时面对 Flutter、鸿蒙接入、微信平台三层迷雾。
4.4 HAR 封装 so:给 Flutter 提供带底层库能力的方法
如果你的鸿蒙应用要接入某些专有算法、安全键盘或第三方 SDK,通常厂家只会给一个 so 文件,这时候就需要在鸿蒙侧封装成 HAR(HarmonyOS Archive),相当于 Android 的 AAR。封装思路是:
第一步,在 DevEco Studio 中新建一个 Library 类型的 Module,作为动态库的宿主。
第二步,把 so 文件按 CPU 架构放进对应目录,常见的是libs/arm64-v8a,并在构建配置里声明 ABI 支持。
第三步,在 ets 或 ts 代码里调用该 so 暴露的接口,再包一层给 Flutter 调用的 MethodChannel。
第四步,在 Entry 模块的依赖里加上这个 HAR,打包时 so 会被自动带进 HAP。
我第一版做的时候直接踩了一个坑:只把 so 放到了 Module 工程目录,但忘了检查它是否带入了产物。结果真机上运行一调用就崩,查了半天是 so 没有打进去。后来验证产物的最简单办法,是把打出的 HAP 包解压,直接看libs/arm64-v8a下有没有对应的 so 文件。这个检查习惯我一直保留到现在。
4.5 模拟器调试与真机验证的节奏
开发阶段用鸿蒙模拟器很高效。DevEco Studio 自带模拟器管理,可以创建手机、平板等形态。Flutter 侧的 Dart 代码仍可以在模拟器里打断点、看变量、看 Debug 输出。鸿蒙原生侧的代码断点则需要在 DevEco Studio 里执行。这两个断点体系目前还不是同一套 UI,新手可能不适应,但用熟了之后心里要清楚:你当前断的是 Dart VM 还是方舟侧代码。
命令行验证也很有用。构建完 debug 包后,可以用 hdc 安装:
hdc install -r entry-default-signed.hap hdc shell aa start -a EntryAbility -b com.example.ledger有一条比较重要的开发经验:如果发现安装的包没有包含最新的 Dart 改动,先确认构建模式。Release 模式默认不启用 Dart debug 支持,很多改动可能没被完整编进去。开发尽量用 debug 构建,性能测试再切 release。
5. 高频问题排查与避坑技巧实录
5.1 版本地狱:Flutter SDK、依赖包、鸿蒙分支三方不匹配
我遇到过的最典型场景是:某天升级了某个第三方依赖,然后flutter pub get就一直失败,报错信息指向 Dart SDK 版本不满足。因为我的 Flutter SDK 是 ohos 分支,版本落后于官方主线,新依赖可能要求更高版本。
解决思路不是硬 upgrade,而是引入 FVM 做多版本管理,并把当前分支版本固定在 pubspec 的 environment 和 CI 配置里。依赖升级要谨慎,不要逐个去升级,优先保证主干依赖处于兼容状态。
热词里提到的“flutter 各个版本不对导致依赖包下不下来”,本质上也是这一类问题。不要光顾着改 pubspec,先看一眼你当前 Flutter 版本,很多下载失败是 SDK 版本太低导致解析器直接放弃。
5.2 Windows 上构建时报 CMake 找不到 Visual Studio generator
如果你在 Windows 上构建含原生代码的插件或鸿蒙适配模块,会遇到类似这样的报错:
CMake Error at CMakeLists.txt:3 (project): Generator Visual Studio 16 2017 could not find any instance of Visual Studio.这个报错的大意是 CMake 尝试用 Visual Studio 作为生成器,但在机器上没找到匹配的实例。常见原因是你装了 VS,但 C++ 桌面开发组件没有安装完整,或者当前工程配置期望的是旧版本 VS。
我的建议是:优先确认本机是否具备对应的 C++ 工具链;如果不需要用 MSVC 编译,也可以检查当前构建使用的生成器类型,涉及 native 模块用 ninja 更简洁。这种问题强依赖于具体插件,只能用“先定位是哪个模块触发、再看它要求什么工具链”的思路顺藤摸瓜。
5.3 热重载后界面没有刷新的几个原因
有朋友问“Flutter 热重载后浏览器没更新”,尤其是跑 Web 调试目标时,这个问题更常见。现象是改了标题字体颜色,点击热重载,浏览器页面纹丝不动。
先区分两个概念:热重载(Hot Reload)和热重启(Hot Restart)。热重载保留应用状态,只刷新当前页面的 widget;但如果你改的是 main 函数、全局变量、路由表顶层结构,或者改了 pubspec 和原生代码,热重载无效,必须热重启才能生效。
如果执行了热重启浏览器还是没更新,那大概率是 DevTools 附着的调试实例不对,或浏览器缓存了旧的 JS bundle。可以把调试 session 断开一次,再重新启动。这条经验在 debug 模式下排查 UI 问题特别有用。
5.4 UI 细节:CheckboxListTile 间距与列表项高度问题合集
设置页里一旦用到 CheckboxListTile,最容易碰到的问题是复选按钮和文字的间距、右侧留给辅助控件的位置。以下是我实测比较有效的一组调整属性:
CheckboxListTile( dense: true, controlAffinity: ListTileControlAffinity.leading, contentPadding: EdgeInsets.only(left: 12, right: 12), title: Text(分类名称), value: selected, onChanged: (v) { ... }, )如果对间距有像素级要求,可以不用 CheckboxListTile,而是自定义 ListTile 的 leading/trailing:
ListTile( leading: Checkbox(value: selected, onChanged: onChanged), title: Text(name), trailing: Text(monthlyAmount), onTap: () => onChanged!(!selected), )这种方式的好处是完全掌握布局,坏处是要自己处理点击区域。不要怕绕开组件自己拼,Flutter 的组件体系本来就鼓励你组合。
5.5 高频问题速查表
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| flutter 命令找不到 | path 未刷新 | 重开终端,确认 SDK 路径 |
| 依赖下载失败 | SDK 版本与依赖不兼容 | 用 FVM 锁定版本 |
| 原生调用报 MissingPluginException | 插件未做 ohos 桥接 | 检查插件版本或自写 MethodChannel |
| 图片选择后无法显示 | 直接拿系统 URI 读取 | 先拷贝到应用沙箱目录 |
| 热重载无效 | 改到了非 widget 层代码 | 用 Hot Restart 而不是 Hot Reload |
| 打出的 HAP 调用 so 崩溃 | so 没有打入产物 | 解压 HAP,检查 libs 目录 |
| showLicensePage 颜色突兀 | 未应用自定义主题 | 用 Theme 包裹 LicensePage |
| CheckboxListTile 间距异常 | 默认控件排版限制 | 用 contentPadding/dense 或自定义布局 |
6. 关于这笔技术债,我最后想分享的几句体会
项目收尾后,我回头审视这套 Flutter + 鸿蒙的记账本方案,最大的体感是:真正的成本不在“写业务代码”,而在“适应碎片化的系统适配生态”。你始终要面对 Flutter 官方主干、ohos 分支、各 pub 插件维护方、鸿蒙 SDK 版本这几个变量,它们不是同步演进的,所以版本管理和兼容清单必须从第一天就建立起来。
如果你打算做一个尝试性 Demo,随时可以上手;但如果你要做一个需要上架、长期迭代的产品,我建议先花半天时间把所有依赖的鸿蒙兼容性排查清楚。这部分前期规划的投入是最值钱的。后续我会考虑把账本的数据导出、云同步、多设备协同按同样的思路扩展到鸿蒙生态,毕竟 Flutter 把三端 UI 拉到了同一起跑线,剩下的空间就交给业务想象力了。