Flutter for OpenHarmony实战:SharedPreferences本地存储与视力保护App设计
用眼过度这个事说起来都是泪。白天盯电脑晚上刷手机眼睛干涩、酸胀、视疲劳年轻人的视力问题早就不是少数人的事了。所以我一直想做一个能强制自己休息的提醒工具正好在调研Flutter跨端方案时看到Flutter for OpenHarmony已经能跑起来了就顺手把视力保护提醒App作为实验项目。这篇文章就完整记录一下这个App的实战过程重点讲清楚SharedPreferences本地存储这一块——什么时候读、什么时候写、怎么设计键、怎么多页面同步以及在整个开发过程中踩过哪些坑。这个实战项目适合两类人参考一是想在OpenHarmony上快速试水Flutter的开发者二是做过通用Flutter App、但不清楚跨到鸿蒙之后存储和生命周期有什么不一样的人。整个MVP不算复杂但麻雀虽小五脏俱全提醒、统计、设置三个功能都涉及足够把Flutter for OpenHarmony的开发链路完整串一遍。1. 为什么选Flutter for OpenHarmony先想清楚要解决什么问题1.1 视力保护App的核心需求拆解做任何App之前先列清楚需求。视力保护提醒App最核心的无非这么几件事定时提醒到了设定间隔提醒用户站起来远眺或者闭眼休息休息倒计时提醒之后进入休息状态倒计时结束再恢复工作状态用眼时长统计记录当天累计使用时长、休息次数用数据让人看见自己的用眼习惯设置项持久化提醒间隔、是否开启声音、是否工作日模式等等这些设置关闭App再打开也不能丢。这里我强烈建议第一期只做MVP不要上来就加什么AI识别坐姿、摄像头检测距离。那些都要靠原生插件配合在OpenHarmony生态里还不一定有人写过。先把“提醒—休息—统计—设置”这个闭环跑通后面的扩展再说。1.2 Flutter在这条路上的可行性判断做技术选型的时候我对比过三条路OpenHarmony原生开发、泛跨端框架、Flutter for OpenHarmony。原生开发体验最好但只服务于单平台以后如果想覆盖其他端就要重写成本很高泛跨端框架生态成熟但渲染层和原生交互在某些场景下会有点隔靴搔痒Flutter for OpenHarmony是社区把Flutter引擎移植到OpenHarmony上的方案Dart代码基本不用动同时还能用OpenHarmony的原生能力做补充。我最终选了Flutter原因是写起来顺手、UI一致性高而且我手头还有维护其他平台App的需求一套逻辑能复用多端。唯一要接受的事实是OpenHarmony和其他移动系统并不是一回事通用安装包不能直接装上去第三方Flutter插件的兼容性也有待验证尤其是那些深度依赖系统特定API的库在OpenHarmony上很可能要自己改方案或用原生扩展替代。提示如果你的App重度依赖某个Flutter插件比如特定的地图SDK、支付SDK先确认它在OpenHarmony上有没有可用替代再决定要不要动手。我这次用的插件很少所以基本没遇到无解的问题。2. 环境准备和工程初始化动手前最容易被绊倒的几个细节2.1 开发环境与SDK配置Flutter for OpenHarmony的开发环境和传统Flutter差不多但有几个关键差异。首先是IDE和SDKOpenHarmony应用开发需要下载对应的IDE工具和SDK配置环境变量时要把SDK路径对齐同时让Flutter工具链识别到OpenHarmony平台。我遇到的一个普遍存在的小问题是环境变量路径里有空格或中文这在桌面系统上特别容易翻车编译到一半报一堆找不到文件的错。后来我把整套工具链放到一个纯英文无空格的目录下问题就消失了。其次是版本对齐OpenHarmony SDK、IDE版本、Flutter适配版本三者的版本号是有匹配关系的不能随意混搭否则会出现“设备连上了但代码装不上”或者“装上了但插件报错”这种玄学问题。最稳妥的做法就是用官方教程里标明的推荐组合版本不要追新追新一时爽踩坑火葬场。2.2 工程结构差异与依赖检查创建工程后你会发现目录结构比纯Flutter项目多了一些OpenHarmony相关的配置签名配置、权限声明、模块配置都需要手动检查。这里有一个非常容易被忽略的坑很多Flutter插件在包管理器上虽然能正常下载但内部的实现并不适用于OpenHarmony需要看插件是否提供了对应的原生实现。视力提醒App要用到本地通知能力我调研了一圈发现不能直接依赖通用版本需要找带OpenHarmony适配的实现或者在工程里自己写一个原生平台的扩展通道。依赖检查的实操建议是新建项目后第一时间把你要用的核心依赖全部加入并执行一次构建不要等业务代码写一半才去测试插件是否可用。构建失败的插件尽早换方案越早发现处理成本越低。我就是因为偷懒拖到写统计页时才引入图表类库结果构建报错临时换方案折腾了一整天。3. 提醒业务逻辑设计把“护眼规则”变成可靠代码3.1 提醒模型与定时策略视力保护领域有两个经典规则20-20-20法则每20分钟看远处至少20秒还有一种简化的番茄钟式每工作25分钟强制休息5分钟。我把它做成可配置项默认使用20分钟和20秒用户可以在设置里改成25分钟和5分钟也可以自定义间隔、自定义休息时长。代码层面我用核心的定时器来实现策略。前台运行的时候一个周期性的计时器每秒或每30秒检查一次当前累计工作时长达到阈值就进入提醒状态。这里要特别小心一个逻辑计时器不能只数“运行了几秒”而要把“App是否真的在前台”考虑进去。用户切到别的App或者手机熄屏这段时间就不应该全算成“用眼时长”否则提醒会不准。所以我在每次切换前台和后台时记录时间戳用时间差来校正。3.2 前台页面与后台通知的分工最理想的情况是App一直挂在后台还能准点提醒。这个需求就涉及后台通知能力。前台页面负责显示倒计时动画和当前状态真正的到点提醒必须依赖系统的通知调度能力否则App一退到后台Dart的计时器很可能被冻结提醒也就不准时了。我在实现里的分工是这样的页面在可见状态时用UI计时器驱动界面的进度展示页面进入后台时把“下次提醒时间”持久化到本地交给原生层的定时通知能力用户重新打开App或通知被点击时读取持久化的时间戳校正累计时长。原生层怎么实现我就不铺开贴代码了但你要记住一句话提醒这件事不要完全依赖Flutter层的计时器在OpenHarmony上尤其如此。系统省电策略和设备休眠策略都会影响计时器精度唯一可靠的办法是把关键时间点写进本地存储到点交给原生侧触发通知。3.3 用眼时长统计的数据口径统计功能看起来简单但数据口径要统一。我的设计是每次App回到前台先用当前时间减去上次离开时记录的时间戳把差值累加到“今日用眼时长”里同时把“上次离开时间”更新为当前时间。如果差值大于某个阈值比如超过几分钟说明用户中间可能长时间离开这段时间就不统计避免误计。这个字段同样要持久化。如果用户App崩溃或者系统杀进程内存里的累加值会丢所以每隔一段时间就要把统计值写入本地我用的存储方案就是本文主角SharedPreferences。一开始我图省事只在退出App时保存结果好几次进程被杀导致当天统计清零第二天看到数据归零心里拔凉拔凉的后来改成定时落盘才算踏实。4. SharedPreferences本地存储实战键值设计的细节与路径4.1 为什么轻量配置选择SharedPreferencesFlutter本身有shared_preferences插件OpenHarmony上也提供了对应的适配实现。它本质上是一个轻量级的键值对存储把数据以特定格式写到应用私有目录适合保存配置项、用户偏好、小体量的状态数据。我的判断标准很简单单个键的value不超过几KB、总量不大、不需要复杂查询的数据优先用SharedPreferences需要结构化查询、要按时间范围统计、数据量会上千条就换数据库。视力提醒App的设置项和统计值刚好属于前者所以用它没有任何问题。做开发最忌讳的就是武器库乱用杀鸡用牛刀和牛刀杀鸡都不对匹配场景才是关键。4.2 键的命名规划与数据结构设计这是整个存储实战里我觉得最值得认真做的一步。很多人在项目里随手写sp.getString(interval)项目一大全是一堆魔法字符串改起来想哭。我提前列了一张键表规划每一类数据的键名、类型和默认值remindInterval提醒间隔分钟数整数breakDuration休息时长秒数整数soundEnabled是否允许声音提醒布尔totalFocusToday今日累计用眼时长秒数整数lastLeaveTimestamp上次离开前台的时间戳整数lastRemindTimestamp上次提醒时间戳整数needAutoStart是否开机自动恢复提醒布尔configVersion配置版本号整数。实际上我并没有把所有设置拆成散键而是用一个结构化Map序列化成JSON字符串后放进一个聚合键里统一保存。原因是设置项是整体读写的拆成太多键不仅增删字段时要改一堆代码还要维护一堆默认值。整体存一个Map读出来转换成对象反而更清晰。但统计值单独存一个键因为它写入频率高不该和低频设置项混在一起否则每次统计更新都要重写整个序列化字符串白白浪费IO。这里给一个非常实际的建议把键名定义成常量注释写上每个键的默认值和单位精度。比如“总时长存秒、间隔存分钟”这种口径不写清楚后面维护的人其实就是未来的你一定会懵。我吃过这个亏两周后回来看自己的代码还要翻半天才记起单位是什么。4.3 读写封装内存缓存与延迟落盘SharedPreferences的API本身不复杂拿实例、按类型读、按类型写。但如果每个页面都直接操作Preferences会有几个问题一是重复解析JSON的开销二是大量异步操作会让代码很难看三是页面之间状态不一致。我写了一个单独的存储管理类思路是启动时一次性把JSON读入内存转换成AppConfig对象所有页面读写都走这个对象需要落盘时调用保存方法把内存对象序列化后写入存储。这样整体读盘次数很少性能不错代码里也几乎没有散落的键名。但内存缓存带来一个体验问题如果用户改了设置马上杀进程可能来不及保存。我的策略是重要设置在修改后立即保存非重要状态类数据比如累计时长做一个防抖保存每隔10秒或累计变更超过一定量再写一次避免频繁IO。这个设计既保证了核心设置不丢又不会因为高频写盘拖慢UI。注意设置“立即保存”的时候尽量在用户点击开关或选完选项的瞬间触发不要等退出页面才保存。不少App的“设置丢失”就是这么来的——以为在退出时统一保存结果用户中途被杀进程或直接滑动关闭一脸懵。4.4 启动流程与默认值兜底一个容易被新手忽略的点是从本地存储读配置是异步操作。如果直接在启动入口里进入主界面首帧渲染时配置还没读到页面就会用默认值闪一下更糟的是读回来之后界面数据对不上用户一脸懵。我的做法是把它调成异步启动读取配置完成后再进入主界面。启动时会有短暂的白屏但换来了配置绝对可靠值得。这个取舍在低端机上感受更明显至少不会出现“明明昨天设置的25分钟今天打开变回20分钟”的诡异问题。默认值兜底也很关键。读出的字段返回空、JSON解析异常、字段缺失这些情况都要能自动回退到合理的默认配置。尤其是软件升级后旧版本没有的新增字段要用默认值补齐不能因为缺字段直接崩。我把兜底逻辑统一写在存储管理类的初始化函数里数据层的异常不会漏到UI层。这样即使存储数据被破坏App也不会白屏最多是设置恢复默认。4.5 多页面状态同步放弃“到处读盘”设置页面改了提醒间隔首页的倒计时立刻要变统计页面写的累计时长首页要实时显示。这里很多人会继续走“改完数据重新读一次SharedPreferences”的老路其实没必要状态同步交给内存模型更自然。我的方案是存储管理类维护一个可监听的对象Dart里用ChangeNotifier或ValueNotifier实现页面通过监听绑定到配置对象上。设置改动时内存对象先变、UI立刻刷新同时触发延迟落盘。SharedPreferences只是最后的持久化出口不再承担页面间通信的责任。这套组合在Flutter里很成熟写起来也清晰本质就是把“存储”和“状态”两个概念拆开各干各的活。5. 实测中的意外与处理真机调试与常见问题5.1 模拟器和真机的提醒差异我在项目早期一直用模拟器开发功能看起来一切正常。结果换到真机上发现模拟器上准点触发的计时器在真机上经常延迟几十秒甚至几分钟。原因就是系统省电策略和调度差异。这个问题最终不是靠Dart代码解决的而是尽量把提醒任务下沉到原生通知能力同时把时间点写入存储避免完全依赖进程被冻结后的Dart计时器。另外真机上还遇到一个问题应用权限默认是关闭的通知权限没开启时即便代码写得再对用户也收不到提醒。第一次在真机测试时我以为代码没生效折腾了半天才发现是权限弹窗被我忽略了。所以真机调试的第一步就是检查权限状态这是最容易忽略也最影响体验的细节。5.2 存储相关问题的排查链路整个项目里和SharedPreferences相关的坑集中起来大概有这么几类我把现场表现和排查思路整理成一张表问题现象可能原因排查方向重启App后设置恢复默认写入失败或被异常中断检查保存方法是否被调用、是否有异常被吞掉部分设备设置丢失系统清理应用数据检查是否过度依赖系统不清理的私有目录确认权限策略首屏闪现默认设置异步读取没等完成改为启动时先读取再进主界面的流程同一份数据多页面不一致各页面各自缓存了旧对象统一走内存模型不各自维护副本JSON解析异常导致白屏数据损坏或字段缺失初始化函数里增加异常兜底和默认值回退排查存储问题时我建议先在日志里把读写路径和值都打出来确认写入成功、读回一致再做业务层判断。不要一上来就怀疑是系统bug大部分时候是自己代码哪里有疏漏。我见过一个案例同事反复说存储“丢数据”结果排查下来是他在两个页面各写各的键一个写totalFocus一个写total_focus命名不一致导致看起来像数据丢失其实是两个不同键这个教训值得记一下。5.3 启动速度与性能体感用了统一内存缓存之后App启动时只是读一次JSON感知上几乎无感。这套方案如果想继续优化可以把整个配置对象序列化后存成文件本质上能做到一样的效果但对大多数App而言没有这个必要SharedPreferences在这个量级已经绰绰有余。另外特别想提一句不要在热路径上频繁调用SharedPreferences的读取方法比如首页动画每一帧去读一次。这种写法虽然不会立刻崩溃但会造成不必要的IO压力甚至导致肉眼可见的卡顿。把“存储读写”和“界面渲染”从机制上隔离开才是轻量存储的正确食用方式。很多性能问题不是算法复杂而是架构上就没有把该分离的东西分开。6. 下一步扩展空间什么时候该换存储方案6.1 数据量变大后的存储方案演进做完整套MVP我的真实感受是轻量存储够用就好别过度设计。但如果你打算给视力保护App加两个功能存储方案可能就要升级了一是把每天的用眼数据存入历史表按周、按月画统计图表二是增加多设备同步需要合并冲突处理。当数据从“几十个配置”变成“成百上千条记录”时关系型数据库的查询和索引能力是SharedPreferences给不了的。OpenHarmony端也有对应的数据库组件能把Flutter层的数据操作单独抽象一层。我在项目里已经预留了一个存储接口将来换数据库只是替换实现类的事业务代码不用大改。这个设计建议尽早做别等数据多了再重构那会疼很多。接口隔离这种事前期多花半个小时后面省的是好几个下午。6.2 提醒策略与产品层面的扩展提醒策略上可以做得更细致根据时间段配置不同的提醒间隔、工作日和周末区别对待、连续提醒次数达到上限后停止等。这些逻辑在MVP里没塞进去我怕的是第一个版本就臃肿。先把核心闭环稳定住再谈个性化是我在实战里一直坚持的节奏。另外如果以后想要更智能的视力保护还可以接入环境光传感器或者距离传感器检测到用户在暗光环境或者离屏幕太近时主动提醒。但这些能力都需要原生插件支撑在OpenHarmony上的适配情况要单独验证不是Flutter层能直接搞定的。产品功能的实现路径要基于底层生态的实际能力来设计。最后说点个人体会。Flutter for OpenHarmony这个方向实际做下来比我想象中稳但也没完全成熟。它的价值在于节省跨端成本Dart代码几乎零改动就能在多个平台复用它的代价在于第三方生态需要自己踩点尤其是通知、权限、传感器这类偏原生的能力。视力保护App这个项目本身不大但正好覆盖了定时逻辑、生命周期、本地存储这几个跨端开发的核心问题等于一次把OpenHarmony上的Flutter开发链路完整走了一遍。如果你也想拿它练手我的建议是先跑通一个最小的“设置—存储—提醒—统计”闭环再考虑加功能。闭环越早跑通后面每一步你都更有底。