最近在折腾 Flutter 三端应用的时候把「专注时光盒」这个项目从 Android/iOS 一路带到了 OpenHarmony 上。说白了这就是一个帮你把碎片时间拼成整块的数字容器你设定一个专注时段它把手机里那些容易让你分心的东西暂时挡在外面到时间了给你一个温柔的提醒。听起来简单可真要在一套 Flutter 代码里同时跑通三大平台尤其是 OpenHarmony 这种还在快速演进的系统中间踩的坑和解决的问题很值得写出来复盘。这篇东西不是教程式的按键说明更像是一个实战后的总结。适合正在做 Flutter 跨端开发、对 OpenHarmony 感兴趣或者想做一个真正能用起来的专注类工具的人。我会从项目怎么拆解、为什么选 Flutter、OpenHarmony 上有哪些别人没明说但很关键的坑、核心功能怎么落地、以及我实际调试时遇到的几类问题这几个角度来讲。代码和配置会给到可以直接抄的程度但更想让你理解每一步背后的取舍。1. 项目整体设计与思路拆解1.1 为什么选 Flutter 来扛三端做之前其实犹豫过要不要用原生三套各写一遍但看完团队人力和维护成本就放弃了。专注类应用的核心逻辑集中在计时、状态切换、闹钟调度这套业务上UI 层的工作量反而集中在几个关键页面——首页的专注计时盘、统计图表、设置页。这类界面用 Flutter 的自绘能力来做效率和一致性都不错而且 Flutter 的引擎对 OpenHarmony 的适配已经进入可用状态官方在做持续完善。我选 Flutter 3.x 配合 OpenHarmony 的 Flutter SDK 分支而不是直接拿 TOB 的第三方封装原因是可控性更强出问题能定位到引擎层。三端意味着同一套 Dart 代码要跑在 Android 的 ART 上、iOS 的 AOT 上还要跑在 OpenHarmony 的 ArkUI 兼容层上。Flutter 在这里的价值不是“一次编写到处运行”那么天真而是把业务逻辑和 UI 描述做到与平台无关把平台相关的部分收敛到 plugin 层。比如通知栏权限、前台服务启动、电量优化白名单这些每个系统的入口和限制都不一样但 Flutter 的通道机制允许我写一套 Dart 调用再在平台侧各自实现。1.2 产品功能的取舍逻辑「专注时光盒」的核心定位不是又一个大而全的番茄钟而是做“碎片洪流中的守门员”。所以功能列表做了严格收敛核心是专注计时支持 5 到 120 分钟自定义、期间拦截其他应用的通知打扰、白名单电话放行、结束后的心流标签记录。市面上很多同类产品堆砌了种树、打卡排行、社交分享我全部砍掉只保留了“开始聚焦、到点结束、复盘一下”这个闭环。为什么这么砍因为专注类工具的第一价值是降低启动成本如果每次开始前都要设置一堆参数用户早就放弃了。我把开始按钮做成大面积的单一入口所有额外设置折叠进二级界面而且默认值合理——默认 25 分钟默认打开通知拦截默认开启白名单。这样用户从安装到第一次完成专注不超过十秒。第二个理由是数据诚实不做连续签到和虚拟成就避免产品为了留存而诱导用户产生负罪感这种设计才是真正对用户负责。1.3 技术架构与项目目录规划项目采用分层结构不是为了好看是真能减少改动时的心智负担。顶层是app目录放入口和路由features目录按业务功能拆分——focus、stats、settings三个子模块。每个子模块内部再分data、domain、presentation三层。core目录放主题、通用工具类、插件封装。lib/ app/ app.dart router.dart core/ theme/ utils/ platform/ features/ focus/ data/ domain/ presentation/ stats/ data/ domain/ presentation/ settings/这个结构的实用意义在于OpenHarmony 的适配代码被完全隔离在core/platform里比如通知栏权限、前台服务这些不会散落到业务层。以后某个平台需要单独处理只要改一个文件。另外每个 feature 的 domain 层只放纯 Dart 的实体和逻辑不依赖 Flutter SDK这样写单测非常舒服也不需要跑到模拟器上跑业务逻辑。2. OpenHarmony 适配的硬核细节2.1 Flutter 与 OpenHarmony 的连接方式OpenHarmony 上跑 Flutter不是直接把 Android 的 APK 塞进去而是通过 OpenHarmony 的 Flutter SDK 把 Dart 代码编译成能在该平台运行的应用包。实际使用的是flutter_flutter这个 fork 分支需要按官方文档把 Flutter SDK 替换掉然后通过 DevEco Studio 配合命令行构建出 HAP 包再签名安装到开发版的开发板上或模拟器。这里有个关键认知OpenHarmony 目前提供的 Flutter 支持本质是把 Flutter 引擎作为一个原生组件嵌入到 ArkUI 应用里然后通过系统通道与 UI 层通信。所以并不是所有 Flutter 插件都直接可用像shared_preferences、path_provider这些基础插件有兼容实现但一些依赖原生 View 的复杂插件比如地图、高德定位就需要单独找 OpenHarmony 版本或自己封装。2.2 权限模型与 Android 的巨大差异做三端时被折磨最多的就是权限。Android 有运行时权限iOS 有描述文件说明OpenHarmony 的权限模型则是基于模块和 ACL 的声明式授权。应用想在后台运行前台服务必须在module.json5里声明对应的权限还要在配置文件里申请 ACL否则运行时会被系统静默拦截而且没有明显的弹窗告诉你为什么失败。以通知栏常驻服务为例我需要在「专注时光盒」的配置里添加{ module: { requestPermissions: [ { name: ohos.permission.KEEP_BACKGROUND_RUNNING, reason: 需要保持专注计时在后台运行, usedScene: { ability: [ MainAbility ], when: inuse } }, { name: ohos.permission.NOTIFY_CONTACTS, reason: 需要发送专注完成通知, usedScene: { ability: [ MainAbility ], when: inuse } } ] } }这里有个容易踩的坑NOTIFY_CONTACTS这个权限名看着像是联系人实际上在 OpenHarmony 里它控制的是发送通知到通知中心的能力名字有误导性。刚开始我照着文档猜导致通知一直发不出去查了很久才发现是权限名搞错了。所以一定要打开系统自带的应用权限列表挨个对照别想当然。2.3 前台服务和保活的策略差异专注重计时最怕的是应用进程被系统杀掉几分钟后时间没了用户心态直接崩。Android 可以用foregroundService加wakelockiOS 用后台任务延长申请OpenHarmony 则要求在后台运行时申请“长时任务”类型否则应用进入后台后很快被冻结。OpenHarmony 把长时任务分成了好几种数据下载、音视频播放、定位导航、通话等等。专注计时不属于这些严格分类中的任何一项它更像是一个系统级的“受保护的短暂任务”。我的解决方案是采用前台服务 前台通知双保险。前台通知就是常驻状态栏提醒用户能看到剩余时间同时系统知道这个应用正在做什么不会一刀切冻结。实现上需要调用 OpenHarmony 提供的featureAbility接口启动服务然后在 Dart 侧通过 flutter 插件发出通知。实际测试中即使把屏幕关了挂机 40 分钟进程依然存活计时没有漂移。2.4 多设备屏幕适配的注意点OpenHarmony 不只跑在手机还可能在平板、开发板、甚至带屏设备上。这意味着屏幕适配不能只盯手机的 dp 值。我用的方案是三档断点小屏手机使用单列布局中屏平板使用双列布局大屏折叠屏开合使用自适应边距。Flutter 的LayoutBuilder可以稳妥地实现但因为 OpenHarmony 的窗口尺寸变化是动态的比如折叠屏开合瞬间需要监听窗口尺寸变化事件并重建布局否则会出现折页处的白色缝隙。另一个问题字体缩放。OpenHarmony 系统默认允许用户的字体缩放设置直接作用到应用上甚至有些开发板的系统字体设置会放大到 1.3 倍。如果你的界面用固定字号就会出现文字溢出。所以我在设置里强制关闭“跟随系统字体大小”的选项并在所有文本组件上设置textScaler: TextScaler.noScaling。这个改动在 Android 上没影响但在 OpenHarmony 上救了很多布局。3. 专注核心功能的实现细节3.1 专注计时器怎么保证稳定走时写计时器第一直觉是用Timer.periodic每秒走一下但真这么做你会发现应用退到后台Dart 的 Timer 可能会被系统暂停等回来的时候时间已经不准了。我的做法是用三个时间基准开始时间DateTime用户点击开始按钮的瞬间记录本地当前时间每次 UI 刷新时读取目标结束时间 开始时间 设定分钟界面上显示的不是一个递减的计数变量而是直接用“结束时间 - 当前时间”动态计算剩余秒数。这样即使 UI 偶尔卡顿或者系统调度延迟只要拿到一个新时间戳显示立刻校准不会累积误差。每分钟触发一次Timer来更新 UI而不是每秒触发减少无谓的界面刷新和电量消耗。void _startCountdown() { _endTime DateTime.now().add(Duration(minutes: _duration)); _timer Timer.periodic(Duration(seconds: 1), (t) { final remaining _endTime!.difference(DateTime.now()); if (remaining.isNegative) { _handleFinish(); } else { setState(() { _remaining remaining; }); } }); }注意上面虽然用了每秒的回调但核心逻辑是计算差值所以即使这一秒回调被延迟下一帧依然准确。另外在 OpenHarmony 上如果发现Timer.periodic在后台被延迟可以在平台侧启动一个原生长时任务来提供更稳定的节拍器这个后面会细讲。3.2 通知拦截与白名单的实现思路专注重计时里的“拦截通知”并不是真的挂断电话或删除消息而是让这些通知暂时不在状态栏弹出或者弹出时不亮屏不响铃。在 Android 上可以用NotificationListenerService直接拦截iOS 权限受限只能做引导用户开启“专注模式”OpenHarmony 上则需要调用notificationManager的订阅接口。最终我选了一个更轻量且兼容三端的策略不直接抢系统通知权而是引导用户开启系统的“勿扰模式”或“专注模式”。应用只需要在开始时请求系统打开勿扰结束时请求恢复。虽然用户多一步确认但胜在省心、稳定而且不会被系统降权。白名单功能是通过检测系统电话状态实现的——如果白名单联系人打来电话应用会在专注页面显示一个明显的“重要来电”按钮允许临时中断。这里有个细节如果你自己实现通知拦截在 OpenHarmony 上必须申请ohos.permission.READ_NOTIFICATION权限否则拿不到通知数据。但申请这个权限需要向用户解释用途而且在应用市场上架时可能被平台审核员质疑所以很多开发者干脆绕道系统勿扰。我也建议绕道。3.3 心流标签记录的效率优化专注结束后用户可以选择这一个时段做了什么、状态怎么样。这个记录不需要实时刷新用本地数据库就够了。数据表结构包含 focus_id、start_time、end_time、duration、tag、mood、note。因为没有用户系统所以不需要网络同步直接存在本地后续导出统计时用一个 CSV 生成功能。为了性能我用了sqflite插件但要注意OpenHarmony 上sqflite有兼容版本如果遇到数据库打开失败可能是因为系统存储路径不同。用path_provider获取的目录在三端上返回的值不一样Android 是/data/data/包名/filesOpenHarmony 上则是/data/app/包名/database。所以保存路径要单独写一个平台判断不能硬编码。我实测存一万条记录后按时间范围查询的耗时还在几十毫秒级性能足够。不需要上太重型的数据库框架。索引只建在了start_time上其他字段很少做范围查询。3.4 状态管理与页面刷新策略状态管理我用了Provider因为它足够简单而且ChangeNotifier很适合计时器这种需要频繁刷新局部页面的场景。专注页面有三个主要状态空闲、专注中、已完成。我定义了一个FocusTimerModel继承自ChangeNotifier里面保存当前状态、剩余秒数、历史会话列表。当 UI 刷新时用Select风险地只依赖剩余秒数这个字段避免每秒 notify 到整棵树。这样即使计时器在后台跑前台只有首页那个环形进度条会重绘设置页和统计页不受影响。对比过河图放大重排的分析从渲染层压力看效果很明显手机耗电也降了。4. 三端数据持久化与本地文件系统4.1 路径策略差异本地存储第一件事是搞清楚各个平台路径到底长什么样。Android 上有几个逻辑分区内部存储、外部存储、缓存目录。iOS 有沙盒目录。OpenHarmony 的文件系统结构更接近 Android但有个坑是getApplicationContext().getFilesDir()返回的路径在重启后可能变化原因是应用更新时数据目录会被迁移。所以不要在数据库里保存绝对路径用相对路径或每次启动时重新查询。我用的方案是数据库和 CSV 导出文件都放在由path_provider返回的supportedDatabasesPath或documentsDirectory下但启动时用getDatabasesPath()重新获取一次避免路径失效。4.2 数据库迁移的坑专注时光盒目前已经迭代过两次数据库结构第一次加了 mood 字段第二次加了一个is_remote标记虽然当前没有远程同步。在sqflite里做迁移逻辑并不难用onUpgrade回调处理新旧版本但有一个难以预见的问题是 OpenHarmony 上的远数据库文件一旦被“迁移”过旧版数据目录的文件经常读不出来因为数据库加密或者版本号校验失败。我的应对很简单不依赖版本号自动迁移而是每次启动时检查是否存在schema_version表如果没有就全量重建同时把旧数据导出到 CSV 备份再导入新库。这个方法笨但稳用户的专注记录基本不会丢。后来查了一下 OpenHarmony 的数据库底层实现发现它默认开启了 WAL 模式而某些版本的sqflite去读老库的时候会默认使用默认 journal 模式导致数据不一致。所以我在打开数据库时强制设置journal_mode WAL。await db.execute(PRAGMA journal_mode WAL;);4.3 轻量统计图表的绘制方案统计页面我用了flutter_echarts来画周专注趋势图。这个库本质是个 WebView 加载 ECharts JS在 Android 和 iOS 上挺稳定但 OpenHarmony 上 WebView 组件还在兼容阶段加载 JS 会偶发白屏或者丢帧。实验了十几次我用了一个更稳的组合简单的柱状图和环形图用自绘 Canvas 来做复杂的雷达图才用 ECharts。自绘的好处不只是稳定还有体积优势。flutter_echarts加上 ECharts 的压缩 JS 大概 2MB 多而我只写了几个 Canvas 图表组件大小只有几十 KB。对于专注统计这种需求柱状图看每周专注时长环形图看不同标签的占比Canvas 完全够用没必要为华丽图表牺牲包体和稳定性。4.4 数据备份与导出用户专注数据是自己的资产我做了自动本地备份和手动导出两种方式。自动备份就是每天凌晨如果状态是空闲把数据库复制一份带日期戳的文件放在备份目录保留最近 7 份。手动导出就是把统计期间的数据生成 CSV用户点击分享按钮后调用系统分享面板发送出去。OpenHarmony 的分享面板接口在当前 SDK 下还比较原始调用的时候会出现分享后回来页面状态丢失的问题。我的处理是在分享前把当前页面状态保存在内存里从分享面板回来后检查是否需要恢复。实测下来这个恢复逻辑基本避免了闪白、崩溃体验终于说得过去。5. 开发调试中的常见问题与排查实录5.1 插件在三端的行为不一致最典型的是shake插件用于检测摇一摇在我开发中用来做“立即结束专注”的快捷手势。Android 上摇一摇很灵敏iOS 上因为我模拟器里没法摇以为没问题到 OpenHarmony 上才发现加速度传感器的数据频率差异巨大导致几乎无法触发。我后来干脆不用这个手势改成双击屏幕结束并确认弹窗因为专注场景中主动结束已经做了三道防误触不需要再引入物理手势这个不稳定的变量。这说明什么问题呢跨端开发不能把所有互动方式都寄托在传感器、陀螺仪这类硬件依赖上应该在插件选型时就考虑三端支持情况优先选择基础能力接口稳定的功能比如点击、滑动、长按而不是震动、摇动、倾斜。5.2 通知点击穿透问题用户在专注结束后会收到“查看本次专注总结”的通知点击后会进入详情页。Android 上通过PendingIntent传递 intent extra 就可以找到目标页面OpenHarmony 上则需要用want参数传递而且want里能放的参数类型有限自定义对象需要序列化。我把 focus_id 作为字符串放进want里然后在 Dart 侧解析路由参数简单可靠。但是有个奇怪的现象有时点击通知没有任何反应查了半天发现是module.json5里没有配置 ability 的exported为 true外部应用无法拉起内部页面。加了这个字段之后一切正常。abilities: [ { name: MainAbility, exported: true, ... } ]5.3 后台运行被系统回收即便配好了长时任务OpenHarmony 仍会在资源紧张时回收进程。我的测试画像是手机内存不足时系统先回收后台应用。这时候前台服务会降级为普通服务但不会立刻被杀掉。如果彻底被杀掉唯一能做的就是下次启动时把未完成的专注标记为“中断”并且提示用户上一次专注未能完成。对于这类不可抗力我建议在设置页明确提供“开机自启动”选项因为专注应用很多时候是用户计划好的如果设备重启后忘了重新开始之前的专注记录就黄了。开机自启动在 Android 需要申请自启动权限OpenHarmony 上则要在module.json5里声明ohos.permission.RECEIVER_STARTUP_COMPLETED并且实现BOOT_COMPLETED广播接收。这个逻辑比较重我默认关闭只在引导页提示用户是否开启。5.4 排查日志怎么利用OpenHarmony 开发时最大的困难是崩溃堆栈信息往往不如 Android 那么好懂。我强烈建议你在代码里挂好全局异常捕获把错误上下文存入本地文件另外专门写一个“开发者调试”的隐藏入口打开后可以查看最近 20 条日志和崩溃堆栈。这个功能发布时用编译开关隐藏但只要出了线上问题用户反馈后能快速定位。我也学到了一个习惯每改完一个功能三端各跑一遍冒烟测试别以为 A 端没问题 B 端就一定没问题。Flutter 层的抽象帮我们挡掉了大多数问题但平台渠道的差异永远是野生的不是在真机上跑一遍根本不知道哪里会炸。6. 性能调优与资源占用控制6.1 减少不必要的 rebuild专注页面的环形进度条是我自己用CustomPainter画的每秒重绘整个圆环的视觉反馈不错但代价是 UI 线程容易抖动。用RepaintBoundary把圆环包起来之后重绘范围就没那么夸张了。配合const构造函数把静态部分标记好稳定的帧率立刻上去。实测在 OpenHarmony 开发板上未优化前专注页面帧率大概 40fps优化后稳定在 58-60fps。这个提升不是玄学就是老老实实减少重建范围别偷懒用一个大setState包所有控件的更新。6.2 内存泄漏案例我在统计页面加载好多图片直接用了Image.asset没有处理 controller dispose导致页面切换时内存一直涨。后来统一改成了ImageCache管理并手动清理不再使用的缓存。同样的问题也可能出现在AnimationController和Timer没有销毁的情况。所以每个页面尤其是带计时器和动画的在用完的时候务必dispose掉这个细节在长时间运行的应用里很重要。6.3 包体积的控制Flutter 应用本身就大加了一个 Canvas 图表加了一个本地数据库再加一些基础插件HAP 包到了 50MB 左右。为了控制包体我做三件事用flutter build hap --target-platform ohos-arm64 --split-per-abi按 CPU 架构拆包发布关闭不用的插件用--obfuscate混淆代码来减少符号表体积降低包体约 10%。虽然现在流量不值钱但对一个“轻量容器”定位的应用安装包动不动上百 MB 是有点讽刺。7. 上线相关的体验与经验7.1 三方平台审核注意点Android 和 iOS 上架时审核会问通知拦截、勿扰模式相关的能力使用场景。我需要提供用途说明和界面截图。OpenHarmony 目前没有公众应用市场那样严格的审核流程主要是企业分发或者自己的应用商店但特权保护类权限也建议提前准备证明材料。怎么说呢专注类应用天生和系统权限打交道审核材料别含糊。把“应用只在用户主动点开始专注时请求开启勿扰结束后自动关闭”这句话写进用途说明里大部分审核人员能理解。7.2 用户引导上的小技巧新用户最容易迷路的操作是第一次点“开始专注”系统弹出勿扰权限请求很多人会拒绝然后功能体验打折。所以我做了一个前置拦截在点击开始按钮前先自己弹一个半透明的说明页用图解释为什么需要这个权限得到用户同意后再调系统请求。这样权限授权成功率从百分之七十提到百分之九十以上。7.3 如何围绕数据做长期迭代专注记录攒得越多用户越离不开但前提是别让用户感觉数据被锁死了。我在设置页明确写了“数据本地存储不上传任何服务器”并且支持一键导出 CSV。在隐私敏感的时代有这个表示比什么都管用。迭代方向我会优先做每周回顾报告基于一周的标签和时长总结心流趋势自定义专注时长预设更细粒度的白名单分组这些功能都在原本的数据模型上扩展不重新发明轮子。8. 一段值得分享的个人体会把「专注时光盒」带到三端这事给我最大的感受是跨端开发真正难的地方不在 UI 怎么写而在于对每个平台能力边界建立清晰认知。OpenHarmony 还在快速发展接口变动谁也说不准所以更要把核心逻辑抽象好、隔离好这样不至于平台一变整个应用推倒重来。而且在这个碎片信息狂轰滥炸的时代能做一个帮人主动屏蔽干扰的工具本身就很有价值。我始终觉得“数字容器”这个概念很对它不强迫你使用只是在你需要时把杂乱的信息流挡在门外让你安心做手头的事。下一步如果有空我会把协议层做出来让专注数据可以本地加密备份到用户的私有网络存储上把“数据不出本机”这个承诺坚持到底。最后分享一个实用的小技巧如果你在 OpenHarmony 上遇到诡异的 UI 闪烁或点击失效先别怀疑 Flutter 代码试着把应用窗口的hardwareAccelerated配置关掉再跑很多时候是 OpenHarmony 渲染引擎的兼容问题不是你的业务逻辑问题。我在这上面浪费过整整半天最后就是改了一个配置选项解决的。
阅读完成 · 觉得有帮助?