做了一阵子 Flutter for OpenHarmony 开发最直观的感受就是这套组合完全能撑起一个日常向的 App。这个生活助手 App 的项目核心里做的是习惯打卡功能——每天早上打开设备看一眼今天的运动、阅读、喝水目标有没有完成点一下就记录下来晚上再瞄一眼这个月的打卡日历。一个顺手、稳定的打卡工具本身就是有价值的产品。这篇文章不是讲概念是我实际把一个 Flutter for OpenHarmony 习惯打卡功能从零做出来的全过程。会聊到为什么选这套技术栈、环境怎么搭、数据表怎么设计、打卡热力图怎么做、本地提醒怎么配最后还会把编译和运行阶段踩过的坑一次性列清楚。适合这几类人看想在 OpenHarmony 设备上用 Flutter 开发应用但不知道怎么入手的开发者已经在用 Flutter 想了解 openharmony 平台适配差异的人以及纯粹对跨端技术方案感兴趣、想看看这套组合真实战斗力如何的同学。1. 项目定位与方案选型为什么非要用 Flutter 做 OpenHarmony 应用1.1 原生 ArkUI 和 Flutter我为什么选了后者OpenHarmony 生态有自己的应用开发范式也就是常说的 ArkUI 声明式开发。它的组件体系和状态管理设计得确实不错如果只做一个只在 OpenHarmony 上跑的小工具直接用 ArkUI 没问题。但我这个项目有个前提同样的代码后续要覆盖移动端和其他平台团队里也已经有比较成熟的 Flutter 技术积累。用 Flutter for OpenHarmony等于把原来那套组件库、状态管理方案、第三方包生态整体搬过来而不是在另一个技术栈里重新踩一遍 UI 和交互的坑。这里有一个很实际的对比ArkUI 的工程结构、路由写法、组件生命周期和 Flutter 差异不小一旦做起来后续想要复用到别的平台几乎等于重写。而 Flutter 本身的跨端能力放在这里UI 表现层可以做到一套代码多处运行底层的 OpenHarmony 能力通过平台通道按需调用。对我这种先做一个核心功能验证可行性的项目来说选择非常明确。1.2 习惯打卡为什么是第一个落地功能生活助手 App 的完整形态其实是很大的待办清单、健康数据、记账、日程安排每个模块都是一片深水区。我刻意先把习惯打卡拎出来做原因有三点功能闭环完整新建习惯、每日打卡、历史记录、数据统计、周期提醒一条链路走下来能把 Flutter 在 OpenHarmony 上涉及的 UI、数据库、异步、通知这几块核心能力全部覆盖一遍。数据模型清晰习惯主表和打卡记录表的关系非常典型很适合用来验证本地数据库方案在 OpenHarmony 平台上的表现。交互复杂度适中列表、日历网格、状态切换、长按编辑这些交互在 Flutter 里都有成熟写法既能检验框架能力又不至于一上来就把自己淹没在过度复杂的需求里。把第一个版本的范围切到足够小才能把Flutter 跑在 OpenHarmony 上这件事本身验证清楚。项目跑通了后续再往其他模块扩展就有了稳定基座。1.3 整体架构设计模块边界越清楚适配成本越低整体架构我分成了四层表现层、状态层、数据层、平台层。表现层是纯 Flutter Widget负责页面渲染和交互状态层用 Provider 管理习惯列表、今日状态、日历数据数据层负责 SQLite 读写和统计逻辑平台层是唯一的 OpenHarmony 相关代码比如获取应用文档目录、发送本地通知都收拢在这一层。这么分层有个好处万一某个第三方包在 OpenHarmony 上不可用我只需要替换平台层对应的实现不需要动 UI 和状态层。实际开发中这个设计救了我好几次后面讲踩坑的时候会详细说。2. 环境搭建OpenHarmony 与 Flutter 双端配置要点2.1 OpenHarmony 侧的环境准备先说结论要在 OpenHarmony 上跑 Flutter 应用你得先有一个能编译 HAP 包的完整工具链环境。我用的流程是先从 OpenHarmony 官方渠道下载 SDK 包然后安装官方提供的 IDE 工具把 SDK 路径配置进 IDE 的工程设置里。这一步没什么捷径SDK 版本和 IDE 版本必须匹配否则后面编译时会出现一堆莫名其妙的错误。有一点要特别提醒OpenHarmony 的 SDK 下载下来之后环境变量和 IDE 配置要一起检查。我一开始只配了 IDE 里的 SDK 路径命令行环境变量没有同步导致 Flutter 端调用编译脚本时找不到 OpenHarmony 的编译工具卡了半天。建议在系统环境变量里把 SDK 路径和工具链路径都配上IDE 和命令行两边保持一致。2.2 Flutter for OpenHarmony SDK 的配置过程OpenHarmony 官方维护了专门的 Flutter SDK 分支它的用法和标准 Flutter SDK 类似但要把它当成一个独立的 SDK 来管理。做法是先把这个分支的代码拉下来比如放到一个独立的目录然后把 FLUTTER_ROOT 环境变量指向这个目录或者在使用时显式指定 SDK 路径。我当时的做法是下载官方维护分支的稳定版本然后执行 flutter doctor 确认识别到了 OpenHarmony 平台。这一步的输出里能看到类似 ohos 平台的工具链状态包括是否找到 OpenHarmony SDK、编译工具是否可用。如果 doctor 里没出现 OpenHarmony 相关的平台项大概率是环境变量没配对或者 SDK 版本不在支持列表里。这里有个经验不要同时把标准 Flutter SDK 和 OpenHarmony 分支混在一个项目里用容易把 .dart_tool 和 build 目录搞乱。我后来是建了一个专门的环境切换脚本用哪个 SDK 就切换对应的 PATH 和 FLUTTER_ROOT项目之间互不干扰。2.3 创建项目与依赖治理项目创建仍然走熟悉的 flutter create但需要显式声明平台flutter create --platforms ohos,android habit_app然后把偏好目录结构调整好打开 pubspec.yaml 添加依赖。依赖选择是整个项目最关键也最容易翻车的一环因为 OpenHarmony 的 Flutter 生态还不够成熟不是所有 pub.dev 上的包都有对应的 OpenHarmony 平台实现。我总结了一个依赖选型的优先级顺序优先选纯 Dart 实现的包其次选社区明确支持 OpenHarmony 的版本最后才考虑需要自己写平台通道的包。纯 Dart 包完全不依赖原生平台代码天然跨端在 OpenHarmony 上几乎没有适配问题。比如后面要用的数据库方案我没有选常见的 sqflite而是用了纯 Dart 实现的轻量级数据库直接操作文件绕开了平台通道的不确定性。依赖治理的经验就是每加一个第三方包之前先查它的平台支持列表确认有 OpenHarmony 实现或者纯 Dart 实现再动手。项目里宁可少用几个花哨的包也要保证编译和三方依赖的稳定性。3. 习惯打卡核心实现数据表设计、打卡交互与日历统计3.1 数据模型设计两张表打通全部需求习惯打卡的数据结构其实非常经典。我用了两张表一张存习惯本身的信息一张存每天的打卡状态CREATE TABLE habits ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, target_days INTEGER DEFAULT 21, created_at TEXT NOT NULL ); CREATE TABLE checkins ( habit_id INTEGER NOT NULL, date TEXT NOT NULL, status INTEGER DEFAULT 1, PRIMARY KEY (habit_id, date), FOREIGN KEY (habit_id) REFERENCES habits(id) ON DELETE CASCADE );habits 表负责习惯的基本信息比如名称、目标天数、创建时间。checkins 表是打卡记录以 habit_id 和 date 作为联合主键这个设计保证了同一天对同一个习惯只能有一条打卡记录天然去重。date 字段我用的是 YYYY-MM-DD 格式的字符串没有用时间戳。原因很简单打卡业务的粒度是天用字符串可以直接比较和索引做日历统计的时候也不需要做时区转换。跨时区这种场景在这个功能里不存在没必要引入额外的复杂度。target_days 是给用户设置习惯周期用的比如 21 天养成计划。实际统计的时候会用到这个字段来计算进度百分比也会在连续打卡达到目标天数时给一个完成提示。3.2 数据库访问层避免在 UI 线程里碰数据库数据层我用纯 Dart 的轻量级数据库方案逻辑上是标准 DAO 模式。核心思路是所有数据库操作都封装在单独的 Repository 类里UI 层和状态层只跟 Repository 打交道不直接拼 SQL。class HabitRepository { FutureListHabit getAllHabits() async { ... } Futureint insertHabit(Habit habit) async { ... } Futurevoid checkIn(int habitId, String date) async { ... } FutureMapString, bool getCheckinsByMonth(int habitId, String month) async { ... } }在 OpenHarmony 上有一个特别要注意的点数据库 IO 操作绝对不能直接在 UI isolate 里跑。Flutter 的标准做法是把数据库操作放到单独的 isolate 中避免阻塞 UI 线程。我在 OpenHarmony 上实测发现简单的本地读写如果量不大直接在 async 方法里跑问题也不明显但一旦按月查询打卡记录、做连续天数统计时就能感觉到掉帧。后来统一封装了一个带 isolate 调度的工具类把读写压力从 UI isolate 里挪走页面滑动才恢复正常。连续打卡天数的计算是打卡功能的灵魂。逻辑上不复杂从今天往回逐天检查只要发现某一天没有打卡记录就停止计数。int calculateStreak(SetString checkoutDates, DateTime today) { var streak 0; var cursor today; final formatter DateFormat(yyyy-MM-dd); while (checkoutDates.contains(formatter.format(cursor))) { streak; cursor cursor.subtract(const Duration(days: 1)); } return streak; }这里有个隐藏的坑DateTime.now() 拿到的值是带时分秒的直接用来做日期比较时Subtract Duration 计算可能出现跨天误差。我踩过一次晚上十一点打卡后计算连续天数结果因为时间分秒影响往回回溯的起点不对把连续天数算断了。解决方法是先做一次归一化只保留年月日final today DateTime(now.year, now.month, now.day);所有日期相关的计算都基于这个归一化后的对象统计结果才稳定。3.3 打卡主界面与交互实现主界面我采用的是今天优先的设计思路打开 App 直接看到的是今日打卡卡片和习惯列表。打卡交互上做了两个入口一个是每个习惯右侧的大圆点按钮点击即完成当天打卡状态立即切换另一个是点击整个卡片进入习惯详情页能看到历史记录和周期进度。打卡按钮的状态有三种未打卡、已打卡、已中断。已中断状态我用了灰色标记表示这个习惯在今天以前已经有断档用于提醒用户及时续上。状态切换时加了简短的动画反馈按钮从空心圆变成带对勾的实心圆这个过程用 Flutter 自带的 AnimatedContainer 就能实现不需要额外引入动画库。列表的滑动删除和长按编辑也是刚需。滑动删除用 Flutter 标准的 Dismissible 组件但要注意一个问题Dismissible 在滑动删除时如果直接操作数据源列表容易出现删除后列表状态不同步的问题。我的做法是先把数据库删除操作完成再把内存列表里的对应数据移除最后更新状态层顺序不能反。长按编辑我跳转到了一个独立的编辑页面习惯名称和目标天数都可以改。这个页面的表单校验也补上了比如习惯名称不能为空、目标天数不能小于 1这些基本功不能省。3.4 日历视图与连续打卡统计日历视图是这个功能里视觉上最出效果、也是性能上最容易出问题的地方。我需要在一个月视图上展示哪些天打卡了、哪些天没打、今天在哪、连续打卡多少天。我的实现方案是用 GridView 渲染一个 6x7 的网格对应一个完整的日历月。具体做法是先计算出当前月第一天的星期偏移量再补齐本月所有天数不足 42 格的部分用下个月的日期填充。每个格子根据当天的打卡状态显示不同的背景色和文字颜色打卡日用一个实心圆标记今天用一个描边圆标记未打卡的日期则淡化显示。性能上有一个关键点每个日历格子的布局不能太重。我一开始在格子里放了多个 Padding、Container、Text 嵌套结果月份切换时能明显感觉到卡顿。后来把格子内部的组件结构简化固定了每个格子的宽高比例再用 RepaintBoundary 把格子隔离成独立绘制层性能就稳定了。月份切换时用 PageView 左右滑动切换加载相邻一个月的数据后再刷新基本可以达到流畅跟手。日历下面的统计区放了两个核心指标累计打卡天数和当前连续打卡天数。累计天数直接查 checkins 表 count 就能拿到连续天数用前面写的回溯逻辑计算。这两个数字每天都会变动所以我在状态层里对它们做了缓存只有当天打卡状态变化时才重新计算避免频繁遍历数据库。4. 状态管理、数据流与本地提醒别让状态流在细节里崩掉4.1 Provider 为什么够用状态管理我选的是 Provider没有上更重型的方案。原因很简单这个 App 的状态流比较清晰核心就是习惯列表和打卡记录Provider 的 ChangeNotifier 机制完全能覆盖而且分层好理解。项目亚硬上更复杂的框架反而增加心智负担。我把状态拆分成了两个 NotifierHabitListModel 负责习惯列表的增删改查TodayCheckModel 负责今日打卡状态和日历数据。两个模型之间有交互比如在详情页修改了习惯名称返回列表页后列表数据要能刷新。这个我用的是共享 Repository 实例的机制两个模型持有同一个数据源访问对象数据变更后手动调用 notifyListeners 通知对应页面重建。class HabitListModel extends ChangeNotifier { final HabitRepository _repo; ListHabit _habits []; Futurevoid load() async { _habits await _repo.getAllHabits(); notifyListeners(); } }4.2 数据流时序异步操作容易引发的状态错乱这个模块里最容易出的隐性 Bug 是异步操作的时序问题。比如用户非常快地连续点击两个不同习惯的打卡按钮如果每次点击都触发一次异步数据库写入然后立即用返回结果更新内存状态两个操作的完成先后顺序无法保证可能出现界面显示和数据库不一致的情况。我的解法是把打卡操作设计成串行队列模式每次打卡请求先进入一个待处理队列上一个操作完全结束后再处理下一个。虽然看起来损失了一点并发效率但保证了数据一致性。实际体验下单机本地数据库的写入速度毫秒级串行化根本感知不到延迟。另一个关键是状态更新要基于数据库操作的成功回调而不是在发起操作时乐观更新。乐观更新在标准 Flutter 里很常见但在 OpenHarmony 的异步环境里如果写入失败会导致界面状态和真实数据长期不一致排查起来很痛苦。宁可多等十几毫秒也要确认写入成功再更新 UI。4.3 本地提醒通知的实现与权限坑习惯打卡如果没有提醒大概率三天后就忘了打开 App。所以本地通知是必须有的能力。我用了 Flutter 的本地通知方案来做每日定时提醒核心是配置每天固定时间触发一条本地通知文案类似今天的阅读目标还没完成记得打卡。配置通知时有个设备差异问题OpenHarmony 不同设备对通知权限的处理不完全一样。有的设备默认允许应用发送通知有的需要用户在设置里手动授权。我在首启引导页里加了请求通知权限的流程并且把权限被拒绝时的情况做了兜底处理——在打卡页显示一个开启提醒的横幅点击后跳转到系统设置页。定时通知的构建逻辑参考了本地通知插件的标准用法await localNotifications.periodicallyShow( id: 1001, title: 打卡提醒, body: 今天的习惯目标还没完成记得打卡, frequency: DailyPeriodicNotification.dailyAt(8, 30), );每天同一时间触发对于习惯打卡这种周期性场景非常合适。这里也有一个细节如果用户把设备时区改了定时通知的时间可能偏移。目前我的处理是监听时区变化事件并重新调度下一次通知避免用户跨时区后提醒时间错乱。5. 常见问题与排查技巧实录5.1 编译阶段依赖不匹配与 MissingPluginException第一类高频问题是编译时提示找不到某个原生符号或者 SDK 版本不匹配。这种问题绝大多数是因为 Flutter for OpenHarmony 分支和 OpenHarmony SDK 的版本对应关系没对齐。OpenHarmony 的 API level 在持续演进老的 Flutter 分支可能只支持到某个 API 等级换了更高版本 SDK 后编译就会报错。我的排查顺序是先看报错信息里提到的 API 符号再确认当前 Flutter 分支对应的支持版本最后调整 SDK 或分支版本到兼容位置。不要一上来就怀疑代码写错了环境匹配问题占了编译失败原因的六成以上。第二类高频问题是 MissingPluginException意思是在调某个 Flutter 插件的时候OpenHarmony 平台没有对应的原生实现。最典型的场景是拿了一个只支持 Android 和 iOS 的插件直接跑在 OpenHarmony 上。这个问题的解决思路我在选型部分就铺垫过了插件选型前先确认平台支持遇到不支持的插件就找纯 Dart 替代品或者自己在平台层补一个实现。5.2 运行时崩溃与文件路径问题习惯数据要持久化存储就需要拿到一个可靠的永久目录路径。普通 Flutter 里的 path_provider 插件在 OpenHarmony 上需要有对应的平台适配。如果直接调用没有适配的版本就会遇到空返回值或者路径获取失败。我最后的方案是走平台通道在 OpenHarmony 原生侧获取应用沙箱目录返回到 Dart 侧使用。虽然代码量多了几十行但换来了路径获取的确定性。这里有个小技巧把路径获取结果缓存到静态变量里只在应用启动时获取一次后续所有数据库和文件操作都复用这个路径避免频繁调用平台通道。运行时崩溃另一种常见原因是数据库文件句柄被多处打开。排查下来是多个 Repository 实例各自打开了一次数据库操作时互相竞争文件锁。统一改成单例模式之后就没再出现过。5.3 体验优化日历卡顿与中文字体显示日历卡顿在 3.4 里提过优化思路这里补充一个细节排查过程。我首版日历的每个格子是一个半透明的 Container叠了多层阴影和边框。在低端 OpenHarmony 设备上滑动切换月份时整页重建的格子太多掉帧明显。后来把所有静态样式抽出来用 const 构造函数固定不变量结合 RepaintBoundary 和 cacheExtent 参数把当前页前后各一屏的内容预构建问题就解决了。中文字体的问题是另一类体验细节。部分 OpenHarmony 设备上默认字体对中文的渲染不够统一个别系统字体在某些字重下显示发虚。解决方法是打包一个自定义中文字体资源在 MaterialApp 的 theme 中全局指定 fontFamily。这样虽然让安装包变大了一些但换来的是所有设备上一致的文字渲染效果对打卡类这种以文字为主要载体的 App 来说值得。还有一个我在真机与模拟器之间切换时反复踩的坑模拟器上默认没有配置通知渠道调用本地通知会静默失败看起来像代码没生效。后来我加了统一的渠道初始化方法在应用启动时确保渠道已创建问题才消失。真机上一切正常模拟器上报无声 Bug这种环境差异要靠系统日志才能定位到。写在最后的个人体会这个项目做下来我对 Flutter for OpenHarmony 的真实感受是可行可用但需要克制。可行的部分在于 Flutter 的上层开发体验完整保留了下来状态管理、UI 组件、异步模型和标准 Flutter 完全一致没有学习上的额外成本。可用的部分在于基础数据库、文件读写、本地通知、定时任务这些中轻度应用需要的能力经过适配后都能稳定工作。需要克制的地方在于第三方生态还不够丰富每一个依赖都要先想想在 OpenHarmony 上跑不跑得起来。如果接下来要继续扩展这个生活助手 App我会优先补两件事一是把打卡数据做成可导出的格式后续可以做云备份和跨设备同步二是加一个每周复盘视图把一周的完成情况汇总成一张卡片这种反馈对习惯养成的持续性很有帮助。数据结构和 Repository 层在规划时已经为这两个功能预留了扩展位到时候改动成本应该可控。最后分享一个小技巧如果你也在折腾 Flutter for OpenHarmony建议把所有平台相关代码收拢到一个目录里目录之外的代码全部保持纯 Dart。这样项目未来无论迁移到哪个平台只需要重写薄薄一层平台适配主体代码一分都不动。这套思路在我这个习惯打卡功能上已经验证过省下的时间足够你多喝好几杯咖啡。
阅读完成 · 觉得有帮助?