资讯动态

Flutter鸿蒙适配实战:rx_storage存储库改造与迁移

发布时间:2026/10/4 9:10:12 来源:尧图企业网站定制
上个月我把一个 Flutter 项目往鸿蒙设备上迁移UI 跑起来只花了两天存储层却卡了整整一周。页面能渲染按钮能点但登录态、本地缓存、用户配置全部失效一查全是rx_storage在报错。这个库在 Android 和 iOS 上是火了很多年的响应式 Key-Value 方案到了鸿蒙的 Flutter 运行时里却处于无实现可用的状态。折腾完整个适配过程之后我决定把完整的改造思路写下来——包括底层原理、选型逻辑、原生侧对接方案、踩过的坑和最终的验证数据。如果你也在做 Flutter 三方库的鸿蒙化落地这篇文章应该能帮你省下大量试错时间。1. rx_storage 在鸿蒙上跑不起来问题出在哪1.1 先拆解 rx_storage 的架构Dart 响应式壳 原生 KV 存储要搞清楚怎么适配得先看清楚 rx_storage 到底是个什么东西。它表面上是又一个本地存储库但内部其实是分了两层Dart 层做响应式封装原生层做实际的 KV 读写。Dart 层这一侧rx_storage 依赖 RxDart 的Subject家族。每次write之后它不光是往原生存储里塞数据还会同步往内存里的 Subject 推一个变更事件每次watch(someKey)也不是轮询原生侧而是直接订阅这个 Subject。这个设计的妙处在于同一个 Flutter 页面内多个组件共享同一份存储状态时状态变更会立刻通过 Stream 广播出去UI 层只需要StreamBuilder就能自动刷新完全不需要自己写事件总线或者回调管理。原生层这一侧rx_storage 默认走的是shared_preferences的那套平台通道。也就是说它通过MethodChannel调SharedPreferencesPlugin插件再去访问 Android 的 SharedPreferences 或者 iOS 的 NSUserDefaults。Android/iOS 上这两样东西都是操作系统自带的性能可靠所以整体表现很稳定。问题就出在这个默认走 shared_preferences上。1.2 鸿蒙 Flutter 运行时缺了什么插件平台从 android/ios 变成 ohos鸿蒙上的 Flutter 运行时和官方 Flutter 有一个根本性差异官方 Flutter 只把 Android、iOS、Web、桌面当作目标平台鸿蒙不在列表里。要在鸿蒙上跑 Flutter用的是 OpenHarmony 社区的 flutter_flutter 分支这套分支有自己的引擎和插件宿主工程插件代码不是放到android/或ios/目录而是在ohos/目录下用 ArkTS 写原生实现。Flutter 插件在平台发现上的逻辑其实很简单pub包里的插件有一个platforms参数来声明自己支持哪些平台。当 Flutter 工具链在构建鸿蒙应用时它会去看这个插件有没有ohos平台实现。如果没有Dart 层调用MethodChannel时就会抛MissingPluginException或者直接 no-op。rx_storage 以及它依赖的 shared_preferences 上游包都是标准的官方 Flutter 插件pubspec 里根本没有ohos这个 platform所以在鸿蒙应用里它的所有方法调用都打到了空处。表面现象是数据读不出来、写不进去根因其实比这个更具体不是一个方法没实现的问题而是整个原生插件层缺失。1.3 为什么不能等上游更新而是要自己动手有人可能会问这个库的作者会不会很快补上鸿蒙支持我的看法是别指望。rx_storage 不是大厂的核心维护项目shared_preferences 官方也不太可能主动为鸿蒙分支开发插件。实际做鸿蒙化迁移的时候最常见的状态就是上游没适配、PR 没人管、issue 长草。与其被动等不如按标准插件适配流程自己动手。我这边的做法是直接 fork rx_storage在它的仓库结构里新增ohos/原生目录同时把 Dart 层依赖的 shared_preferences 平台通道替换成自己封装的一套鸿蒙通道。这样做的收益是业务层那些rxStorage.watch(...)、rxStorage.write(...)的调用完全不用改只替换库内部实现迁移成本非常低。2. 适配前的选型Preferences 还是 distributedKVStore2.1 两套鸿蒙本地 KV 能力对比鸿蒙系统给应用开发者提供了两套本地键值存储能力它们名字有点像但底子完全不同。第一套是 Preferences中文文档里通常叫用户首选项。它的数据以 Key-Value 形式存到应用沙箱里的一个本地文件中读写速度很快适合保存应用的轻量配置、用户偏好、少量结构化数据。它的 API 很直观getPreferences拿实例put写值get取值flush落盘还支持通过on(change)监听某个 Key 的变化。第二套是 distributedKVStore也就是分布式键值库。它的定位要重得多数据不只存在本设备还能通过网络在多个设备之间同步底层做了数据版本管理、冲突合并、多端一致。它的监听机制也更完善on(dataChange)能拿到详细的事件类型和变更的 Key。我整理了一张表方便你快速定位选型场景维度Preferences用户首选项distributedKVStore分布式键值库数据落地应用沙箱本地文件本地落盘 可选云端/多端同步同步能力无纯本地支持多设备数据同步变更监听on(change)回调中拿 Keyon(dataChange)支持事件类型与 Key适合数据量小规模 KV适合配置类中大规模 KV适合需要跨端一致的数据API 复杂度简单学习成本低偏高需要先初始化 KVManager 和 KVStore性能表现单点读写快无网络开销本地读写也快但同步开启后有额外成本你如果只是把原来的本地 KV 存储搬到鸿蒙上第一直觉肯定是 Preferences。它语义最接近 shared_preferences改造成本最低。2.2 我为什么两套都接本地用 Preferences变更通知接入 KVStore但光用 Preferences 有一个痛点rx_storage 的核心卖点是响应式 同步引擎。它希望你对任意 Key 执行write之后所有订阅这个 Key 的地方都能收到通知。Preferences 确实有on(change)但它的回调落在 ArkTS 侧怎么把原生侧的事件传到 Flutter 的 Dart 流里需要你自己搭桥。而且 Preferences 的on(change)回调细节在不同 API 版本里不太一样有些版本只在 flush 后触发不是每次 put 都立刻触发。distributedKVStore 在这块就舒服多了。它有明确的dataChange事件按键级别通知手感和百度网盘这种多端写入后自动拉取最新值的模型很像。所以我的最终方案是两套一起上数据本体写入 Preferences保证单机读写性能同时用一个后台 KVStore 实例作为变更广播总线写入时往 KVStore 里放一个带时间戳的开关值KVStore 的 dataChange 事件负责把某个 Key 变了这件事通知给 Flutter 侧再由 Dart 层去 Preferences 里取新值。这个方案看着绕其实好处很明显读写热路径还是最快的 Preferences响应式通知用上了 KVStore 成熟的事件机制两边的优点都拿到了。2.3 纯 Dart 方案能不能用Hive / 文件存储为什么被排除也许会有朋友提出更激进的方案既然适配这么麻烦不如干脆换掉 rx_storage用 Hive 或者纯 Dart 文件存储重写这一层。Hive 在 Android/iOS 上表现确实不错而且它也是纯 Dart 实现理论上理论上不需要原生插件就能跑。但这里有个很现实的问题这是鸿蒙化适配项目不是推倒重来项目。业务代码里可能几十处地方都调用了 rx_storage 的 API每个调用都改成 Hive 的 API测试回归成本会很高。更关键的是Hive 在文件层读写时自己管理二进制格式在鸿蒙沙箱里的文件权限、IO 行为都需要重新验证风险并不比接原生通道低。所以我最后放弃了纯 Dart 方案坚持保留 rx_storage 对外 API只替换内部实现的路线。稳定压倒一切业务改动越小越不容易出幺蛾子。3. 动手适配保留 rx_storage 响应式语义的改造路径3.1 第一步fork 工程并在 pubspec 声明 ohos 平台先说工程层面的做法。rx_storage 是一个标准 Flutter 插件包目录下本来就分lib/和android/、ios/等平台目录。鸿蒙化适配要做的第一件事是在它的pubspec.yaml里把ohos平台声明出来同时新建一个ohos/目录放原生实现。# pubspec.yaml 中增加 ohos platform 声明 flutter: plugin: platforms: android: package: com.example.rx_storage pluginClass: RxStoragePlugin ios: pluginClass: RxStoragePlugin ohos: pluginClass: RxStoragePlugin shared: true注意pluginClass指的是鸿蒙原生工程中负责注册 MethodChannel 的类名。不同版本的 flutter_flutter 鸿蒙分支对插件注册方式的命名有差异有的用pluginClass有的要求在初始化文件里手动注册。建议先看你接入的鸿蒙 Flutter SDK 自带示例插件是怎么写的照着它的样子来别凭记忆填。3.2 第二步鸿蒙原生侧实现 Key-Value 通道接下来是实现原生侧的核心逻辑。以 Preferences 作为后端存储为例在 ArkTS 里注册一个 MethodChannel处理 Dart 侧发来的read、write、delete、clear、getAll这些方法调用。我用类伪代码的方式写一下整体的处理思路实际的导入路径和方法签名以你连接的 SDK 版本为准// Native 侧核心逻辑示意 import { MethodChannel } from flutter_ohos; import preferences from ohos.data.preferences; export class RxStoragePlugin { onMethodCall(call, result) { switch (call.method) { case read: { const key call.arguments.key; const pref await preferences.getPreferences(context, rx_storage); result.success(pref.getSync(key, null)); break; } case write: { const args call.arguments; const pref await preferences.getPreferences(context, rx_storage); pref.putSync(args.key, args.value); await pref.flush(); result.success(true); break; } default: result.notImplemented(); } } }这里有一个很重要的细节getPreferences的取值方法分同步和异步两个版本。getSync和putSync适合高频小数据操作但flush()是异步落盘为了确保数据不丢写入流程里一定要保留 flush 步骤。Android 的 SharedPreferences 也有类似的 commit/apply 区别习惯了原生开发的同学应该觉得很亲切。3.3 第三步把外部变更通过 EventChannel 接回 Dart 的 Subject原生读写通道做完rx_storage 基本就能在鸿蒙上存取数据了。但要做到极致、响应式、反应灵敏光有读写不够还得把原生侧的外部变更事件接回 Dart 的响应式流里。我采用的方案是 EventChannel。原生侧在有数据变更时往 EventChannel 发送事件Dart 侧收到事件后通过 rx_storage 内部的 Subject 向所有订阅者广播。原生侧的监听逻辑大致长这样// Native 侧向 Dart 发送变更通知 pref.on(change, (key) { this.eventSink.success({ key }); });Dart 侧接收并重新注入流EventChannel _eventChannel const EventChannel(rx_storage/events); _subscription _eventChannel.receiveBroadcastStream().listen((event) { final key event[key] as String; final newValue await _readFromNative(key); _subject.add(key, newValue); });这个流程看起来简单但它是整个适配工作的灵魂。rx_storage 的响应式语义本质上就体现在所有数据变更都能被订阅方感知到。原生侧写入后如果只更新了文件、不通知 Dart那 watch 就变成了普通的定时轮询体验大打折扣。把 EventChannel 传到 Dart、再注入 Subject才算真正把同步引擎的内部链路打通。3.4 适配改动点汇总改动方向具体内容说明工程结构新增ohos/目录声明 pluginClass让 Flutter 工具链识别到鸿蒙平台实现Dart 通道新增RxStorageHarmony封装替代对 shared_preferences 的依赖原生存储接 Preferences承担实际 KV 落地原生通知监听 Preferences 的 change 事件捕获外部变更跨层桥接使用 EventChannel 回传 Dart保证 Subject 能继续广播事件4. 同步引擎的关键细节多实例一致性与数据序列化4.1 多 Isolate、多页面同时读写时的数据一致性问题Flutter 的多 Isolate 模型有个特点每个 Isolate 是独立的 Dart 运行时内存不共享因此你在 Isolate A 里创建的RxStorage实例在 Isolate B 里看不到它内部维护的 Subject。也就是说A 写入了数据B 的 Subject 不会自动收到事件。这种情况下数据一致性不能靠 Dart 层内存必须靠原生侧。因为无论多少个 Isolate最终都通过 MethodChannel 调同一个原生存储实例所以只要保证原生侧是最终一致的数据源多 Isolate 之间就不会出现严重的数据撕裂。我的做法是所有 Isolate 都只依赖同一个存储后端写入完成后依赖 EventChannel 广播变更。这样 A 写入、B 收到原生侧事件、B 重新读一次新值后自行更新自己的流缓存语义上就能做到全局同步。4.2 类型映射与序列化别在 native 侧做类型体操Dart 和 ArkTS 之间的 MethodChannel 传参虽然会自动做基础类型映射但在复杂类型上很容易踩坑。比如 Dart 侧的MapString, dynamic传到鸿蒙侧时嵌套结构可能被转成不一样的容器类型整数超过 2^53 时转成 JS number 会丢精度。所以我的建议非常朴素在 Dart 侧就把要存储的值全部规范成原子类型或者统一序列化成 JSON 字符串。具体来说业务侧调用写接口时传了一个自定义对象不要在原生侧尝试解析对象的字段而是让 Dart 层先jsonEncode成字符串再作为单个 String 值传给原生侧存储。这样原生层永远只处理string、number、boolean这三种基础类型逻辑稳定后续维护成本也低。这类序列化放 Dart 层的取舍其实很常见。你把类型解析放到哪一层哪一层就得承担兼容压力。鸿蒙侧 API 迭代频率不低如果每次升级都要重新调类型映射适配工作会变成无底洞。4.3 锁与合并写的策略Preferences 的写入操作本质上不是原子的批处理。如果你在很短的窗口里连续调用几十次putSync每次又跟着一次flush()那么 IO 开销会被放大得非常明显。我实际验证下来连续高频写入时把多次put收集起来最后统一flush()一次性能会好很多。这个思路和数据库里的批处理很像。rx_storage 的语义是单 Key 快速读写业务侧通常不会在一个事件循环里疯狂写几十个 Key但为了防止极端情况我在适配层里加了一个简单的合并队列多个写入请求在 16ms 窗口内合并成一次 flush。要注意的是合并 flush 会稍微延迟持久化时机如果应用在写入后立即被杀进程理论上存在丢数据的窗口。对绝大多数 KV 缓存场景来说这个风险是可以接受的如果你做的是不可丢失的数据建议保留每次写入立即 flush 的行为安全优先。5. 踩过的几个真实的坑以及对应修法5.1 首次 getPreferences 耗时偏高冷启动被拖慢鸿蒙的 Preferences 实例在首次初始化时要加载存储文件、建立内存索引第一次调用会比后续慢不少。如果业务启动时立刻读一个 Key可能出现几百毫秒的延迟这在反应灵敏的响应式体系里特别碍眼。我的修法是在 Flutter 引擎启动后的早期阶段做一个预热调用主动getPreferences一次并随便读一个 Key让原生侧完成初始化。预热之后真正的业务读取就会快很多。5.2 on(change) 回调只给 Key 不给新值且不能在里面直接写Preferences 的 change 回调通知的是哪个 Key 变了并不会把新值塞给你。这意味着 EventChannel 收到通知后Dart 侧还得再发一次read请求去拿最新值。这里多了一次往返但这是保证拿到新值的唯一可靠路径。还有一个更隐蔽的坑千万不要在 ArkTS 侧的 change 回调里直接再调用另一个put。如果这个 put 恰好也触发了 change就可能形成循环通知导致事件风暴。我的处理习惯是回调里只负责通知真正的逻辑放到 Dart 层去编排。5.3 大整数精度丢失别直接存 int我第一次用 rx_storage 存用户 ID 时就出了问题ID 后几位莫名变成 0。原因很简单ArkTS 侧接收 Dart 的 int 时按双精度浮点处理超过 2^53 就丢精度。视频平台的分类 ID、外部系统返回的长整型主键都容易引爆这个坑。这类问题需要明确不要隐藏存储统一。我最终把 HarmonyOS 侧可能涉及整数精度的字段全部格式化为字符串在 Dart 层拿到后再解析回 int。因为字符串不存在精度问题安全性高得多。业务侧读出来的类型偶有转换需要但可靠性远比这点小麻烦重要。5.4 EventChannel 要先 listen 再订阅否则丢事件EventChannel 的事件发送是实时的Dart 侧如果还没有receiveBroadcastStream().listen(...)原生侧提前 emit 的事件会被直接丢弃。早期我在原生 on(change) 注册后立刻触发事件以为 LinkedHashMap 会缓存事件结果丢失了不少变更通知。正确姿势是Dart 侧先 listen再通过另一个同步方法通知原生侧可以开始订阅了。相当于建立一条可靠的握手先打开接收端再打开发送端。5.5 注意 Subject 的强引用防止响应式流被回收在 Dart 层如果 rx_storage 实例内部的 Subject/StreamController 只被局部变量持有而你在页面销毁时只做了dispose却没有清理全局引用那后续写入的事件可能推到一个已经被 GC 的对象里表现为偶尔不刷新。这个问题在低内存设备上尤其明显。其实这个坑并不只在鸿蒙上出现Android 也有。适配鸿蒙时正好一起处理把所有 rx_storage 实例放到一个应用级单例中持有只在 app 生命周期级别销毁不要在单页面级别随便 dispose。6. 性能验证与最终结果6.1 测试环境与测试方法适配完成后我在一台鸿蒙开发设备上做了压测测试指标包括冷启动首读耗时、单键连续读写耗时、write 到 watch 通知的事件延迟。连续 1000 次写入/读取取平均值事件延迟用 Stopwatch 在 Dart 侧统计从 write 返回到订阅者收到事件的时间差。6.2 测试数据一览指标适配前不可用适配后冷启动首次读取MissingPluginException约 80ms预热后约 20ms1000 次连续写入不可用平均 0.6ms / 次1000 次连续读取不可用平均 0.3ms / 次write 到订阅通知延迟不可用平均 2.1ms大 JSON 字符串256KB读写不可用读 2.8ms / 写 4.5ms实测下来纯本地 KV 读写的性能已经和 Android 上的 shared_preferences 接近响应式事件延迟也控制在个位数毫秒对 UI 层刷新来说体感是即时的。6.3 调优经验小结性能上能优化的点其实就三个初始化预热、批量 flush、事件去重。预热解决的是首次加载慢批量 flush 解决的是高频写入放大事件去重解决的是同一个 Key 在短时间内重复变更导致的无意义重建。前两个我在前文都讲过了第三个可以在 Dart 层做收到事件后加一个小时间窗同一个 Key 只广播最后一次值减少页面无谓的 rebuild。7. 说点适配完之后的心里话整个项目做完回头看最难的部分不是那几百行原生代码而是把响应式语义完整地绕了一圈Dart 层写数据native 层落盘native 层再通过事件把变更告诉 DartDart 层重新读值、再塞回 Subject。任何一个环节断了用户看到的表现都是数据偶尔不刷新。我现在对 rx_storage 适配鸿蒙这件事的结论是值得做而且最好按保留 API、替换实现的思路做。业务代码零侵入后续如果上游新增了功能合并代码也相对顺利。如果你也在做类似的三方库迁移建议先花时间把库的数据流方向画清楚再动手写通道。数据从哪来、变更回到哪去这两条链路一旦明确写代码就和做填空一样简单了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑