news 2026/10/20 15:12:48

Flutter鸿蒙本地数据持久化:Realm与realm_generator实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flutter鸿蒙本地数据持久化:Realm与realm_generator实战

在鸿蒙设备上跑 Flutter 工程,最让我头疼的从来不是 UI 适配,而是本地数据这一层。一开始图省事,全用 SharedPreferences 存 JSON,数据量一上来,启动分析和列表加载肉眼可见地卡。后来决定引入 Realm,但手写模型映射和维护字段同步又成了新的体力活。折腾了一轮之后,我用 realm_generator 把整个数据层从“手写维护”变成了“声明模型后自动生成”,并在鸿蒙端完成了插件集成和数据架构治理。这篇文章就把这套实战过程拆开讲,包括选型逻辑、工程配置、生成器原理、鸿蒙集成的坑,以及最后沉淀下来的数据层架构,给同样在 Flutter + 鸿蒙 这条路上做落地的人一个可以直接抄作业的参考。

1. 项目拆解与选型思路

1.1 为什么在鸿蒙场景下选择 Realm

在鸿蒙出现之前,Flutter 的本地数据库基本是老三样:SharedPreferences、Hive、sqflite。SharedPreferences 适合小量键值,Hive 性能不错但类型安全弱一些,sqflite 功能强但要从 SQL 语句写起,映射到 Dart 对象靠自己折腾。Realm 之所以在这个项目里胜出,不是因为它比所有人都快,而是因为它在 Flutter 跨端场景里提供了一条比较顺的“对象映射 + 自动生成”路径。

我在做选型对比时,重点看四个维度:编写效率、查询能力、鸿蒙适配成本、团队上手难度。Realm 的 Flutter 绑定在这几个维度上相对均衡。查询可以直接用类似realm.query<Task>('priority > $0', [8])的方式,生成的 Query 对象是惰性求值,排序和过滤都延迟到真正迭代时执行。这一点对处理大列表很友好,不会在查询语句构造时就把全部数据加载进内存。

更重要的是,Realm 的 Flutter 版本把模型对象和数据库字段直接绑定,配合代码生成器,模型类声明完,增删改查的底层字段名全部自动生成。鸿蒙侧虽然需要额外接入原生适配包,但整体接入路径比我想象中短,后面会详细讲。

1.2 realm_generator 在工程里的真实定位

很多同事第一次听到 realm_generator,以为是另一个数据库框架,其实不是。它只是 Realm Flutter 里的一个编译期代码生成器,底层还是 Realm 存储引擎,只是它帮我们把“模型类声明”到“底层字段操作”之间的样板代码全部自动生成出来。

打个比方,手动写 Realm 模型,就像你买了个智能家居中枢,但每个开关都要自己去接线。而 realm_generator 的作用是,你只需要告诉它房间里有什么灯、什么窗帘、什么插座,它自动把控制面板帮你接好。字段名、类型转换、主键映射、索引标记,这些容易写错又繁琐的工作,全部交给生成器处理。

它在工程里的位置大概是:Dart 模型类源码 -> build_runner 执行生成 -> 生成对应的.realm.dart文件 -> 打开数据库时注册 schema。也就是说,它是链接“业务模型”和“存储引擎”的桥梁,一方面保证强类型,另一方面减少手工映射带来的低级错误。

1.3 手写、半生成、全自动的取舍

我一开始见过不少项目是“半生成”状态:模型类自己写,查询和写入时却还是手写字符串拼条件。比如realm.objects('Task').where('ownerName == $0', name),一旦字段改名,这个字符串条件不会在编译时给你任何提醒,跑起来才报错。这种半生成状态其实最危险,因为你以为自己有类型保护,实际还是裸奔。

用生成器之后,查询条件里的字段、排序字段、索引字段都可以由生成的代码提供强类型引用。改字段名时,编译直接提示错误。这个体验差异在我看来是决定性的。当然,全自动也不是没有代价,生成代码的可读性一般,遇到要 debug 底层字段映射时你得打开生成的.realm.dart看一遍,但总比自己手工维护所有映射成本低得多。

2. 工程准备与鸿蒙侧集成

2.1 前置条件与工程结构确认

开始之前,先把基础环境确认好。Flutter 版本建议用稳定渠道的较新版本,鸿蒙工程目录已经存在,并且 FlutterEngine 能在真机上跑起来,这样我们才把重点放到数据库层。如果工程连 Flutter 页面都还跑不通,先解决工具链的问题,否则后面排查数据库层问题时会混入环境噪声。

鸿蒙工程的本地数据库通常落在应用沙箱的 data 目录下,最好在开发阶段把 Realm 文件的 path 固定到一个可预期的位置,比如{getApplicationContext files 下的子目录}/db/xxx.realm,方便后续用工具看数据。我在调试时吃过“找不到数据库文件”的亏,后来发现是路径用了临时目录,系统清理后文件就没了。

2.2 pubspec 依赖与 build.yaml 配置

在 pubspec.yaml 里,realm 作为主依赖,realm_generator 放在 dev_dependencies,build_runner 也放在 dev_dependencies。注意 generator 千万不要放 dependencies,否则业务包体积会变大。示例配置如下:

dependencies: realm: ^3.0.0 dev_dependencies: realm_generator: ^3.0.0 build_runner: ^2.4.0

如果你的工程只有一个默认入口,不写 build.yaml 也能直接跑。但我建议还是显式加一个 build.yaml,把生成范围收敛到lib/models目录,避免以后模块多了 generator 全工程扫描,拉长构建时间,也避免误生成一些不该生成的内部类。

targets: $default: builders: realm_generator|realm: generate_for: - lib/models/

这里有个小坑:不同版本的 generator 对 generate_for 的写法有细微差异,有的版本用目录结尾的/会识别异常。遇到这种情况,可以去掉末尾斜杠或者指定具体的.dart文件。

2.3 鸿蒙插件的落地方式

鸿蒙侧最麻烦的一步是原生适配包的集成。因为 Flutter 的 realm 包在 Android 上默认依赖 Android 原生模块,鸿蒙并不直接兼容,所以需要找到对应的鸿蒙适配包,把它以源码或 .har 的方式集成进entry工程。

我采用的方式是先在entry/oh-package.json5里声明本地依赖,指向放在entry/libs下的 .har 文件。大致结构如下:

{ "dependencies": { "harmony_realm": "file:./libs/harmony_realm.har" } }

这里有个需要特别注意的地方:不要把它加进 Android 的依赖体系里,也不要天真的以为 Flutter 插件会自动传递鸿蒙实现。鸿蒙的 Flutter 插件机制需要在原生工程侧显式声明,且必须使用鸿蒙的包管理方式。集成完成后,先跑一个最简单的Realm(config)打开动作,确认没有MissingPluginException,再继续往下做模型生成。

注意:如果你发现MissingPluginException,优先检查 .har 是否正确打进包体,以及oh-package.json5的依赖声明是否生效,而不是去改模型代码。

2.4 hvigor 构建与缓存问题

鸿蒙工程构建用的是 hvigor,它有自己的构建缓存。每次改了原生依赖或者新增 .har,最好先清理一次缓存再构建。我遇到过几次奇怪的问题:代码看起来没问题,但运行起来还是旧版行为,清理缓存后立即恢复正常。

具体操作就是在工程根目录执行 hvigor 的清理任务:

hvigor --clear-cache hvigor assembleHap

如果你的 IDE 是 DevEco Studio,直接在构建菜单里选择 Clean 即可。这个动作虽然简单,但能省下大量定位假问题的的时间。

3. 模型定义与代码生成实操

3.1 模型类的正确写法

Realm 的模型类必须继承RealmObject,字段类型也有限制,不能直接把自定义普通类塞进去,必须是 Realm 支持的类型:bool、int、double、String、DateTime、ObjectId、Uint8List 以及 RealmList、RealmSet、RealmDictionary 等集合类型。

写模型类时要注意几个点:

  • 字段不要用final,生成器需要对字段做映射。
  • 主键字段用@PrimaryKey()标记,如果主键是自增的 ObjectId,在构造函数里赋值最方便。
  • 高频查询字段用@Indexed(),但不要给所有字段都加索引,因为写事务会有额外开销。
  • 不需要持久化的字段用@Ignore()标记。

一个比较标准的模型类大概是这样的:

import 'package:realm/realm.dart'; part 'task.realm.dart'; class Task extends RealmObject { Task({ ObjectId? id, String title = '', int priority = 0, DateTime? dueAt, }) : _id = id ?? ObjectId(), _title = title, _priority = priority, _dueAt = dueAt; @PrimaryKey() late ObjectId _id; late String _title; @Indexed() late int _priority; late DateTime? _dueAt; @Ignore() bool isExpired = false; }

注意头部必须写part 'task.realm.dart';,这是生成器的目标文件。如果忘记这一行,生成器不会报错,但也不会生成对应的映射代码,后续打开数据库时 schema 会缺失。

3.2 运行生成命令与产物解读

模型定义好之后,在工程根目录跑:

dart run build_runner build --delete-conflicting-outputs

第一次跑可以把整个生成过程看一遍,尤其注意有没有绿色输出显示生成了哪个文件。如果没生成.realm.dart,先检查 part 指令和文件目录是否匹配。

生成的.realm.dart文件内容一般很长,核心做的事情可以归纳为:为每个字段生成底层 setter/getter,注册字段元信息,生成 schema 描述。这些逻辑普通开发者不需要逐行改,但建议至少扫一眼,知道字段名是怎么映射的,这样以后出现“字段读出来是 null”这类问题时,你能快速定位是生成文件旧了,还是打开数据库时 schema 配置漏了。

打开数据库时,要把模型类的 schema 传进去:

final config = Configuration.local([Task.schema], path: 'db/xxx.realm'); final realm = Realm(config);

如果没有传 schema,Realm 会尝试读取现有数据库文件,如果文件不存在通常能自动创建,但如果文件已经存在且 schema 不一致,就会报 misconfigured schema 的错误。

3.3 生成前后的功能对照

生成器带来的收益,列个表更直观:

功能点手写代码生成器代码
字段映射每次新增字段要手写映射模型声明后自动完成
主键冲突检测自己处理异常生成器统一管理元信息
查询字段名字符串拼接,容易拼错强类型引用,编译期检查
schema 版本管理手动迁移容易漏生成代码携带 schema 信息,减少遗漏
索引维护需要手动关联注解驱动,自动注册

这并不意味着生成器能解决所有问题。它只解决“模型定义和底层存储之间的映射”,不解决业务逻辑,也不替代迁移策略。真正要保证数据层稳定,还是得靠架构。

4. 强类型模型与持久化架构治理

4.1 强类型模型解决了什么

我用一段实际经历说明。之前手写 Map 做缓存时,有一次把字段priority误写成了priorityLevel,JSON 序列化不报错,前端取出来的数据全是默认值。这种问题在编译期完全暴露不出来,只能靠运行时打印日志,配合多个端联调才能发现。

用强类型模型之后,这种低级错误基本绝迹。字段定义在模型类里,生成器自动生成对应的读写方法,任何引用都走编译期校验。哪怕只是迁移数据库时少了新字段,代码都编译不过。

这里要提醒一点:不要在一个模型类里同时混用“业务显示字段”和“存储字段”而不做任何隔离。比如 UI 上需要一个“是否过期”的临时判断,我在模型里用@Ignore()忽略掉,这个字段不会持久化,但可以在模型类里保留计算逻辑。这种做法能让项目快速迭代,但注意不要往模型里塞太多 UI 状态,否则数据层会越来越像一堆散乱的静态变量。

4.2 查询与写入性能调优

Realm 推荐一次写事务处理多个对象,而不是循环单个 add。实际测试时,循环写入的表现特别差,每次事务提交都要等待磁盘同步,几十条数据可能就慢到明显卡顿。正确的做法是:

realm.write(() { for (final item in items) { realm.add(RealmModel.fromMap(item)); } });

读操作方面,查询本身是惰性的,使用.sort()、.filter()不会立即执行。但如果你在.watch()流里频繁重建查询,或者把每个列表项都做一次新的realm.find,同样会引发性能问题。更好的方式是数据进入内存或 ViewModel 后,尽量复用同一个查询结果。

索引配置也要克制。索引能加速读,但写入时要多维护一层索引结构。低频筛选字段不要加索引,真正的高频条件字段才值得加。

4.3 事务、监听与 UI 响应式架构

Realm 提供.watch(),可以对单个对象或集合变化做订阅。这个能力很适合 Flutter 的响应式 UI,但使用不当也会成为性能杀手。

我的建议是:UI 层尽量不要直接持有 RealmObject。正确的做法是,数据层把 RealmObject 转化为轻量 DTO 或领域对象,提供给 ViewModel,再由页面渲染。这样做有几个好处:RealmObject 在事务提交后自动刷新,如果 UI 层持有的是旧引用,可能读到的数据与预期不符;另外,领域对象更容易做单元测试和 mock。

监听订阅记得在页面销毁时取消,否则StreamSubscription泄漏会导致页面回收后仍在更新数据。真机上比较常见的现象就是退出列表页再进来,内存不降反升,多半是订阅没清理。

5. 鸿蒙适配中踩过的坑与排查实录

5.1 编译期的坑

我在鸿蒙工程里遇到的最大编译坑,是 Flutter 侧添加 realm 依赖后,原生插件没有被正确打包进 HAP。报错信息是MissingPluginException,但问题实际发生在构建链路上。原因是我把 .har 文件放错了目录,导致 hvigor 没有把它当作运行时依赖打包。

排查顺序很重要:

  1. 先确认模型文件有没有生成成功,排除 Dart 侧问题。
  2. 再看鸿蒙原生侧依赖是否加载,最简单的验证是把数据库路径改成明显错误的位置,看能否正常抛出 Realm 相关的路径异常。
  3. 如果异常是MissingPluginException,多半是插件没走到鸿蒙的实现,回头检查 oh-package.json5。

还有一次,构建成功但打开数据库时报“unknown table”,一看生成的.realm.dart文件日期还是几天以前的,说明 build_runner 没有真正重新执行,清理构建缓存后重新生成就恢复了。

5.2 运行期的坑

列表页因为查询构造不当变卡的案例也很典型。起初是在每个 item 的 build 方法里写realm.all<Task>().where(...),导致每次滚动都在创建新查询,内存和 CPU 双双报警。改成在 ViewModel 里先获取RealmResults,再对结果做分页切片,性能立刻好了很多。

另一个常见问题是数据库本地文件路径下写到错误的位置,尤其是在鸿蒙文件系统沙箱限制下。如果路径指向了缓存目录,系统可能随时清理掉文件,表现为应用重启后数据丢失。日志里可能什么错误都没有,数据就这么消失了。我把 path 改成应用数据目录后,这个问题再没出现过。

5.3 快速定位问题的方法论

复盘下来,数据库类问题最怕黑盒排查。我建立一个固定流程,先确认生成文件版本,再确认数据库 schema 版本,最后才是看业务代码逻辑。日志要分两层:Flutter 侧打 dart 日志,鸿蒙侧用 DevEco 过滤原生插件日志,两边的错误码对上之后,才定位到具体是查询错误还是 SQLite 层的问题。

现象常见原因处理办法
打开数据库报 schema 错误生成文件过期或 schema 列表漏传重新 build_runner,确认传入全部模型
数据写入后列表不刷新watch 订阅未正确注册检查订阅流,页面销毁时取消
列表卡顿每行重建查询查询结果缓存或提升到 ViewModel
缺少插件异常.har 未打包或路径不对检查 oh-package.json5 与入口目录
应用重启数据消失数据库 path 指向缓存目录改为应用数据目录,保证沙箱持久性

6. 工程实践建议与后续扩展

6.1 多模块项目的模型目录规划

项目变大后,不要把所有模型塞在一个文件里。我现在的做法是,lib/models下按业务域划分子目录,比如task/、user/、setting/。每个域里的模型文件独立生成自己的.realm.dart,打开数据库时把所有 schema 汇总。

这样拆分的好处是,团队并行开发时不太会发生文件冲突。每个域模型的字段变化都集中在自己的目录里,review 起来也直观。如果项目更大,可以进一步把models、repositories、providers分别放进各自模块,用 publish 的方式在模块间共享。

6.2 数据迁移策略

Realm 的数据迁移是在Configuration.local()里通过schemaVersion和migrationCallback显式控制的。最开始项目里没有迁移概念,一旦模型增加必填字段,老用户数据库打不开,只能清空重建。后来我补上了迁移策略:

final config = Configuration.local( [Task.schema], schemaVersion: 2, migrationCallback: (migration, oldSchemaVersion) { if (oldSchemaVersion < 2) { // 给已有对象补充默认值,或者重命名旧字段 } }, );

迁移回调里最怕直接访问新版本才添加的对象,应该在 migration 提供的入口上操作历史 schema。这种问题跑了之后才明白,迁移不是“改完模型自动就行的”,必须提前设计好版本号规则。

6.3 自动化流水线建议

我最后把生成和迁移检查放进了 CI。每次代码合并前,CI 会执行一次dart run build_runner build并检查 git diff 中是否包含预期的.realm.dart文件变化。这样即使有同事忘了重新生成,也会在流程上被卡住。

另外,release 构建时开的混淆需要小心。RealmObject 子类的字段名要保持安全,否则混淆后的随机字段名会让生成的映射关系错乱。鸿蒙 release 模式尤其要检查 keep 规则。

6.4 后续扩展方向

这套架构稳定之后,后续可以继续扩展的方向包括:把数据库操作抽象成统一的 Repository 接口,方便以后替换或测试;把 Realm 的 schema 版本号纳入应用版本发布检查;把本地数据上传逻辑改造成通过事务队列发布,避免网络异常时数据堆积。每一步都不难,但前提是先把模型生成和鸿蒙集成的基础打好。

我个人的体会是,realm_generator 最核心的价值不是省那些代码量,而是让数据层的变更在编译期就暴露问题。只要模型声明写对,生成代码就是可信的。最后再分享一个小技巧:每次改完模型,先把旧的生成文件删掉再执行 build_runner,能避免很多奇怪的缓存冲突,尤其在鸿蒙这种构建链路比较复杂的工程里,这个习惯很管用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 15:12:23

新模型能力跃迁实测:推理代码提升与从业者应对指南

AI圈这两天算是彻底炸了。好几个大模型交流群从早上刷屏到半夜&#xff0c;一张张跑分图到处转&#xff0c;评论区一半在喊“真的假的”&#xff0c;另一半在喊“这下完蛋了&#xff0c;工作要没了”。说实话&#xff0c;这种场面我这些年经历过好几次&#xff0c;但这次确实不…

作者头像 李华
网站建设 2026/10/20 15:12:01

RAG工程落地指南:从索引构建到生产监控的完整实践

做了几年大模型应用&#xff0c;我越来越觉得 RAG&#xff08;检索增强生成&#xff09;是落地价值最高、但也最容易“看着简单做着翻车”的技术方向。很多团队搭个 Demo 只需要一两天&#xff0c;可真要推到生产环境&#xff0c;问题接踵而至&#xff1a;检索结果看着合理却没…

作者头像 李华
网站建设 2026/10/20 15:11:02

OpenCV 4.8.0 Windows 可调试编译包:支持dnn_face与face模块

简介&#xff1a;本资源是面向Windows平台OpenCV初学者与项目开发者的预编译完整库包&#xff0c;专为解决官方二进制包缺失人脸识别等关键功能模块的痛点而设计。基于OpenCV 4.8.0与opencv_contrib 4.8.0源码&#xff0c;在Windows 10 VS2022&#xff08;x64&#xff09;环境…

作者头像 李华
网站建设 2026/10/20 15:10:59

AI编码助手上下文管理实战:context-mode机制解析

我是在一个很普通的周末开始较真 context-mode 这事的。那时候我在维护一个编辑器侧的 AI 辅助插件&#xff0c;用户反馈最多的不是“模型不够聪明”&#xff0c;而是“同一个问题换个文件问&#xff0c;答案完全不一样”。这种情况只要你去做代码补全、智能问答、文档摘要&…

作者头像 李华
网站建设 2026/10/20 15:10:26

JDBC数据库访问与DAO设计模式:从代码失控到分层治理的实战指南

接手一个老项目&#xff0c;打开代码库第一眼最容易让人崩溃的&#xff0c;永远是数据访问这一层。几百个类、上千行SQL&#xff0c;有的直接用字符串拼&#xff0c;有的用预编译&#xff0c;有的返回List&#xff0c;有的返回Map&#xff0c;还有个别方法把ResultSet直接甩给上…

作者头像 李华