1. 项目概述为什么选择此刻实现收藏与持久化今天是今日资讯App系列的第十二篇主角是“收藏功能”和“数据持久化”。追过这个系列的朋友应该清楚前面几篇我们陆续搞定了资讯列表、详情页、分类导航、WebView承载、轻量性能优化等功能App已经有了一个资讯客户端该有的骨架。但骨架归骨架一个没有收藏功能的资讯App就跟没有书签的浏览器一样——用户刷到一篇感兴趣的内容只能靠记忆或者截屏保存体验上始终差了口气。收藏功能某种意义上属于“刚需中的刚需”。用户的真实阅读习惯是先刷看到有价值的文章先收藏之后再集中阅读或反复回看。对开发者的意义则更直接——它是第一个需要将用户行为数据落地到本地的功能涉及数据怎么存、增量怎么更新、状态怎么同步、组件怎么解耦等技术点。放到OpenHarmony Flutter的场景下还增加了国产系统的适配细节。任何一个做资讯类跨端应用的开发者这篇内容都值得重点参考。这一篇的技术重心是数据持久化方案的选型与实现。我会从设计思路谈起到具体代码落地再到实践中踩过的坑尽量把这个功能讲透。你会看到我用什么方案做本地存储、为什么这么选、收藏状态如何跨页面同步以及处理边界情况时那些“文档里不会写”的细节。需要提前声明的是本文中的实现方案、代码片段和取舍思路基于我在类似场景某跨平台资讯客户端中的实际开发经验。你可以把整套逻辑直接搬进自己的项目也可以根据自身需求调整存储方案。2. 存储方案选型本地数据库、键值对、还是ORM框架做持久化第一步不是写代码是选型。选型选错了后面写代码的过程就是给自己挖坑的过程。2.1 三套候选方案的横向对比目前OpenHarmony环境中数据持久化的主流路子大致有三条键值数据库KV Store、关系型数据库RDB、纯文件存储。如果放到Flutter层面还能往里面加一层SharedPreferences或本地JSON文件。键值数据库的典型代表是DataPreferecePreferences实现和分布式KVStore。特点是结构简单、API友好、读写速度快适合存储配置信息、用户偏好、小体量的标志位。比如记录“夜间模式是否打开”“最后阅读位置”这种情况属于KVStore的舒适区。关系型数据库则以RDB Store为主它支持SQL语法、多表联查、条件检索适合结构化数据和复杂查询。资讯收藏场景如果要存标题、摘要、链接、收藏时间、是否已读等字段RDB是逻辑上最“正规”的方案。纯文件方式就没什么好说的——把JSON序列化后写到应用文件目录读的时候整份读入反序列化。实现简单但性能和可靠性都一般数据量大后读写成本直线上升。三套方案各有适用场景初期评估时我倾向于RDB原因后面详述。但用“方案比较表”做一次直观对比更有价值方案数据结构复杂度读写性能查询能力实现成本适合场景键值数据库简单键值对高弱只能按key访问极低用户配置、标志位关系型数据库结构化多表中强支持SQL中高收藏列表、历史记录JSON文件序列化任意但需自定义序列化低弱需自行实现过滤低小型配置、导入导出2.2 收藏功能为什么最终选了关系型数据库我最终定的方案是OpenHarmony的RDB Store核心原因有三。第一收藏数据天然是结构化的。我期望的收藏表至少要包含资讯标题title、资讯链接url、来源source、摘要summary、收藏时间timestamp、封面图字段cover六个字段。如果用KVStore一种做法是把整张收藏表序列化为一个大JSON对象塞到一个key下但这意味着每次新增、取消收藏都要做全量读写数据量几十条时没感觉破百上千后就会有可感知的卡顿。如果按单条收藏做keyKVStore在复杂的条件查询比如按时间倒序取第20到第30条上又力不从心。这活就是RDB的剧本。第二个原因是RDB的增量更新优势。对单条数据执行insert和delete代价几乎不随数据总量增长。基于RDB提供的能力收藏列表的本地分页方案也能做得很自然——用limit和offset配合order by timestamp desc即可根本不需要把全部收藏载入内存。第三个原因比较现实——Flutter生态在OpenHarmony上还处于适配期第三方ORM库的兼容性并不稳定。与其依赖一套Er少的适配层比如sqflite在OpenHarmony上的移植尚不完善不如直接用系统原生的ohos.data.rdb能力稳字当头。先自己实现一套轻量封装把数据访问层DAO抽象出来后续如果Flutter侧生态成熟了替换成本也不高。此外使用RDB还有一个隐性收益让“数据库操作层面”和“UI逻辑层面”的边界变得清楚。数据访问通过异步接口暴露给UI层页面只关心“调用方法、拿到结果、刷新状态”不关心底层是SQL还是别的。对于后续可能增加的历史浏览记录、稍后阅读等功能这套数据访问层可以直接复用扩展性也更好。2.3 一个Out了的选择直接操作文件序列化这里需要单独说一说被很多人视为“偷懒首选”的文件序列化方案。如果收藏功能只是一个“临时给测试看看”的原型那没问题——把收藏列表序列化为JSON存在文件里启动时读入内存变更后重写文件思路很简单。但真实产品中这个方案会逐渐暴露出问题并发写带来的JSON损坏风险、全量读写的性能瓶颈、数据校验和字段变更时的迁移成本。而且文件方案在“收藏状态跨页面同步”上天然麻烦——需要自己在内存中维护一份全局状态再与文件内容做一致性同步。写两层代码出bug的概率翻倍。多实例写入还可能造成文件内容互相覆盖。如果你在做一个周期很短的前端原型或者想快速验证交互流程文件序列化完全够用。但如果你像我一样希望这个App能持续迭代到上架级别的完成度RDB省掉的麻烦远超它引入的复杂度。3. 数据库表设计与数据访问层搭建选型定了之后下一步就是建表和封装访问层。这里我尽量把代码和设计思路都拆开讲方便你直接照着落地。3.1 收藏表结构设计与建表SQL鉴于是资讯App的收藏我建了一张favorites表字段设计如下字段名类型约束含义idINTEGERPRIMARY KEY AUTOINCREMENT自增主键news_idTEXTNOT NULL UNIQUE资讯唯一标识titleTEXTNOT NULL标题urlTEXTNOT NULL原文链接sourceTEXTNULL来源或作者summaryTEXTNULL摘要coverTEXTNULL封面图URLcreated_atINTEGERNOT NULL收藏时间戳毫秒这里有两个设计细节值得你特别留意。第一个是news_id字段。我设置了一个独立于自增主键的信息ID并加了UNIQUE约束。为什么不自接用主键作为业务标识因为后期很可能出现“同一篇文章在不同页面被重复展示”的场景如果靠自增主键判断“是否已收藏”不仅要额外查一次还需要跨表映射。使用业务唯一键news_id做重复收藏判断时只需要一条SELECT 1 WHERE news_id ?即可语义上也更干净。第二个是created_at用了整型毫秒时间戳而非日期字符串。这个选择纯粹是从排序和筛选的角度考虑的——数字比较永远比字符串解析快而且毫秒精度足够应对“按收藏时间倒序”这类需求。如果需要展示格式化日期在查询结果里做一次转换即可没必要在库里存成人类可读的文本格式。建表SQL如下CREATE TABLE IF NOT EXISTS favorites ( id INTEGER PRIMARY KEY AUTOINCREMENT, news_id TEXT NOT NULL UNIQUE, title TEXT NOT NULL, url TEXT NOT NULL, source TEXT, summary TEXT, cover TEXT, created_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_favorites_created_at ON favorites(created_at DESC);索引建在created_at上是典型的“查询驱动设计”收藏列表页几乎总是按时间倒序铺开没有这个索引数据量上去后排序会逐渐吃力。3.2 初始化数据库与数据访问对象封装在OpenHarmony中操作RDB核心流程三步走获取RdbStore配置、创建数据库、执行SQL。我把这部分封装到一个FavoritesDatabase类里职责很单一持有数据库实例对外暴露DAO方法。先看初始化部分import { abilityAccessCtrl } from kit.AbilityKit; import { relationalStore } from kit.ArkData; import { UIAbility } from kit.AbilityKit; import { common } from kit.AbilityKit; const DATABASE_NAME news_app.db; const DATABASE_VERSION 1; export class FavoritesDatabase { private static instance: FavoritesDatabase; private store: relationalStore.RdbStore | null null; static getInstance(): FavoritesDatabase { if (!FavoritesDatabase.instance) { FavoritesDatabase.instance new FavoritesDatabase(); } return FavoritesDatabase.instance; } async init(context: common.UIAbilityContext): Promisevoid { const config: relationalStore.StoreConfig { name: DATABASE_NAME, securityLevel: relationalStore.SecurityLevel.S1 }; this.store await relationalStore.getRdbStore(context, config); await this.store.executeSql( CREATE TABLE IF NOT EXISTS favorites (...) ); // 建索引 await this.store.executeSql( CREATE INDEX IF NOT EXISTS idx_favorites_created_at ON favorites(created_at DESC) ); } getStore(): relationalStore.RdbStore | null { return this.store; } }注意securityLevel字段。OpenHarmony的RDB安全级别分S1到S4分别对应不同敏感度的数据。收藏资讯列表里包含的是资讯标题和链接属于对普通用户无重大影响的常规数据S1低级别即可。但如果你后续要存用户身份信息或支付凭据务必调高安全级别。这个字段直接影响数据库文件在设备上的加密保护强度不是随便填一个就行。有了初始化代码接着封装DAO。这块我分别实现了五个核心方法插入收藏、取消收藏、判断是否收藏、查询全部收藏、分页查询收藏。看代码export class FavoriteDao { private store: relationalStore.RdbStore; constructor(store: relationalStore.RdbStore) { this.store store; } // 新增收藏重复插入自动忽略 async addFavorite(item: FavoriteInfo): Promiseboolean { const values new relationalStore.ValuesBucket(); values.put(news_id, item.newsId); values.put(title, item.title); values.put(url, item.url); values.put(source, item.source ?? ); values.put(summary, item.summary ?? ); values.put(cover, item.cover ?? ); values.put(created_at, Date.now()); const rowId await this.store.insert(favorites, values, relationalStore.ConflictResolution.ON_CONFLICT_IGNORE); return rowId 0; } // 取消收藏 async removeFavorite(newsId: string): Promisenumber { const predicates new relationalStore.RdbPredicates(favorites); predicates.equalTo(news_id, newsId); const rows await this.store.delete(predicates); return rows; } // 判断是否已收藏 async isFavorite(newsId: string): Promiseboolean { const predicates new relationalStore.RdbPredicates(favorites); predicates.equalTo(news_id, newsId); const result await this.store.query(predicates, [news_id]); const hasRow result.rowCount 0; result.close(); return hasRow; } }这里有几个必须强调的规范性问题。第一insert时使用了ConflictResolution.ON_CONFLICT_IGNORE。配合表结构里的UNIQUE约束这样既能防止重复收藏同一篇文章也不会让插入操作抛异常打断用户操作。如果你不加冲突解决策略相同news_id的重复插入会触发SQLite默认的约束冲突直接抛错。第二查询结果集必须close。result是一个结果集对象如果你不显式关闭它每次查询都会持有底层游标资源长期运行下来内存和文件句柄的泄漏会让你查到哭。这个习惯一定要养成——只要是调用query()方法拿到结果用完一律close。第三主键和业务字段分离的设计红利在这个DAO里开始显现。news_id唯一约束让“判断是否收藏”的方法实现极其简单——存在即true不存在即false不用关心id字段。3.3 Flutter侧的轻量持久化辅助数据库主体在OpenHarmony侧但Flutter侧的UI状态也得有持久化托底。比如用户收藏了一篇文章App重启后详情页的“已收藏”按钮状态要能恢复。这里我用SharedPreferences做了一个轻量的收藏标识缓存。class FavoritePrefs { static const _keyPrefix fav_status_; static Futurebool isFavorite(String newsId) async { final prefs await SharedPreferences.getInstance(); return prefs.getBool($_keyPrefix$newsId) ?? false; } static Futurevoid setFavorite(String newsId, bool isFav) async { final prefs await SharedPreferences.getInstance(); await prefs.setBool($_keyPrefix$newsId, isFav); } }这个缓存的核心价值在于UI响应速度。RDB查询再快也是异步跨越原生边界的过程每次进入详情页都等数据库返回再渲染图标会有一闪而过的“加载中”体验。用键值缓存先行恢复UI状态后台再用DAO核对真实数据双写一致性的代价换来的是交互上的顺滑。当然这里要提醒一个细节缓存和数据库双写之间有天然的一致性窗口。只要不是崩溃瞬间恰好写在缓存和数据库中间影响基本可控。真要较真可以在初始化完成后再拉取一次真实收藏列表做整体校准覆盖掉偶发的不一致状态。4. 收藏功能的具体实现与UI联动设计层面该定的都定了下面进入正题把收藏功能真正落到UI层和业务逻辑层里去。这一章我会拆成两块来写——收藏列表页的实现和收藏按钮的跨页面状态同步。4.1 收藏页列表联动RDB与Flutter的完整链路收藏列表页在UI上的核心诉求很简单从数据库查出收藏数据渲染成列表支持下拉刷新和取消收藏。它的难点在于桥接。数据源在OpenHarmony的RDB中UI在Flutter侧。两者之间需要一个“Channel”来传递数据。我在项目中使用了Platform Channel的方案——Flutter侧通过pigeon生成的接口调用原生侧DAO方法原生侧返回数据后Flutter将Map转为Model对象再交给列表渲染。先看原生侧为列表页暴露的接口方法async queryFavoritesByPage(offset: number, limit: number): PromiseArrayFavoriteInfo { const predicates new relationalStore.RdbPredicates(favorites); predicates.orderByDesc(created_at); predicates.limit(limit, offset); const result await this.store.query(predicates); const list: FavoriteInfo[] []; while (result.goToNextRow()) { list.push({ newsId: result.getString(result.getColumnIndex(news_id)), title: result.getString(result.getColumnIndex(title)), url: result.getString(result.getColumnIndex(url)), source: result.getString(result.getColumnIndex(source)), summary: result.getString(result.getColumnIndex(summary)), cover: result.getString(result.getColumnIndex(cover)), createdAt: result.getLong(result.getColumnIndex(created_at)), }); } result.close(); return list; }Flutter侧获取数据并渲染列表的代码长这样class FavoritesPage extends StatefulWidget { override StateFavoritesPage createState() _FavoritesPageState(); } class _FavoritesPageState extends StateFavoritesPage { final ListFavoriteInfo _favorites []; bool _loading false; bool _hasMore true; override void initState() { super.initState(); _loadFavorites(); } Futurevoid _loadFavorites({bool refresh false}) async { if (_loading) return; _loading true; final offset refresh ? 0 : _favorites.length; final limit 20; final list await FavoritesApi.queryFavoritesByPage(offset, limit); setState(() { if (refresh) { _favorites.clear(); } _favorites.addAll(list); _hasMore list.length limit; }); _loading false; } Widget build(BuildContext context) { // 这里用 ListView.builder 渲染收藏列表 // 每个item展示标题、来源、收藏时间和“取消收藏”按钮 } }我在分页实现上用的是经典的“刷新时清空加载更多时追加”逻辑。每次进入收藏页重新拉第一页列表滚动到底部时触发下一页顶部下拉时清空重查。收藏列表通常不会太长这个方案的性能和交互都足够自然。需要注意的坑是下拉刷新时的并发保护。_loading标志就是干这个的——防止网络慢时用户反复触发加载产生重复数据。另外分页上限20条是我拍脑袋定的实测这个数量在手机屏幕上的加载延迟和滚动帧率表现都正常。如果你的列表项包含大图可以把limit调到10图片加载压力会小很多。4.2 收藏按钮的状态同步三个场景三种时机收藏列表页好做真正的复杂度在“收藏按钮”的跨页面状态同步上。资讯列表、详情页、收藏页三个场景都可能触达同一篇文章。用户可能在详情页收藏在列表页看到同一篇文章时按钮图标要反映“已收藏”状态用户取消收藏后详情页的按钮也要同步更新。这个问题不解决好就会发生如下尴尬场景用户明明在收藏页取消了收藏返回详情页一看收藏图标还是实心的。我在这个项目中的做法是两层联动。第一层是进入页面时的初始化。资讯列表页的initState里对当前屏幕上的若干条资讯批量查询收藏状态查询完成后逐条刷新图标数据。我封装了一个批量查询方法async queryFavoriteStatusBatch(newsIds: string[]): PromiseMapstring, boolean { if (newsIds.length 0) return {}; const predicates new relationalStore.RdbPredicates(favorites); predicates.in(news_id, newsIds); const result await this.store.query(predicates, [news_id]); const map new Mapstring, boolean(); while (result.goToNextRow()) { const nid result.getString(0); map.set(nid, true); } result.close(); return map; }一次性查询代替循环调用减少跨桥次数。桥接开销在Flutter和原生交互中是不可忽视的批量处理是有明显收益的优化思路。第二层是事件监听。我在原生侧定义了一个收藏变更事件总线Flutter侧向总线注册监听。任何页面的收藏、取消操作都会触发事件广播各页面收到事件后只更新与变动news_id对应的UI项。事件总线的实现可以用OpenHarmony的Emitter组件也可以用自定义回调集合。我这里用的是Emitterimport { emitter } from kit.BasicServicesKit; const FAVORITE_CHANGED 1000001; // 收藏/取消收藏后发布事件 function notifyFavoriteChanged(newsId: string, isFavorite: boolean) { emitter.emit({ eventId: FAVORITE_CHANGED, priority: emitter.EventPriority.LOW }, { data: { newsId: newsId, isFavorite: isFavorite } }); }Flutter侧注册监听的逻辑类似。页面收到事件后根据newsId判断是否与当前展示的资讯有关有关则更新图标状态。这套机制的优点是简单可靠、跨页面解耦。缺点是事件广播是“全局”的——页面不在前台时也会收到回调所以Flutter侧代码里必须加一道“当前widget是否处于激活状态”的判断避免对不可见页面做多余的build。4.3 收藏按钮交互细节与体验优化收藏功能的交互细节比看起来要多。这些细节处理得好不好直接影响用户对“这个App到底用不用心”的判断。第一点击反馈要即时。用户点击收藏图标的瞬间视觉反馈必须立刻发生——图标立即变成实心或空心哪怕后续数据库写入失败需要回滚也要先让用户看到“操作已生效”。如果等数据库插入完再刷新UI网络满时用户会感到明显迟滞。实测“先更新本地状态再写数据库失败后回滚”的方案比“等待DB结果后刷新”的体验好很多。第二按钮状态需要防抖。快速连点收藏按钮会导致多次insert或delete调用。解决方法是给按钮添加一个短时间内不可重复触发的标志位——点击后300毫秒内忽略再次点击。实现简单但极其有效能挡住绝大多数误操作场景。第三空状态设计。收藏列表为空时绝不能让页面只剩一个白屏。我实现了一个带插图和文案的EmptyView“还没有收藏内容看到喜欢的资讯就收藏起来吧”。这句文案看着简单但能显著降低用户对于功能空置的困惑感。5. 数据持久化的几个关键细节与异常处理持久化不仅是“把数据存起来”更是“把数据安稳地存起来并在合适的时机读出来”。这一章聊聊我在实践中踩过的坑和后续的加固策略。5.1 数据校验与边界情况处理首先要红的第一个问题是对URL和空字段的处理。数据来自远端接口存在各种边界情况。比如某篇资讯没有封面图cover字段是空串或者来源字段缺失数据为null。如果插入前不处理轻则存储脏数据重则导致读取时类型转换异常闪退。我的做法是在DAO插入前强制做空值归一化凡是String类型字段为null时就使用空字符串占位不允许空串和null混存。这一点看似微小但后续做列表数据映射时能省掉大量“这个字段为什么是null”的排查时间。第二个需要关注的是重复上报问题。前文提到了ON_CONFLICT_IGNORE方案在外层业务上还需要再补一道保险插入前先查询一遍是否已收藏如果已收藏则直接返回当前收藏状态不重复执行insert。两道防线配合最大程度保护数据库不受重复数据污染。第三个是时间字段的时区问题。created_at存的是UTC毫秒时间戳不依赖设备时区。但UI展示时需要转成本地时间字符串——我写了一个工具方法function formatFavoriteTime(timestamp: number): string { const date new Date(timestamp); const now new Date(); const diffMs now.getTime() - date.getTime(); const diffMinutes Math.floor(diffMs / (1000 * 60)); const diffHours Math.floor(diffMinutes / 60); const diffDays Math.floor(diffHours / 24); if (diffMinutes 1) return 刚刚; if (diffMinutes 60) return $diffMinutes分钟前; if (diffHours 24) return $diffHours小时前; if (diffDays 7) return $diffDays天前; return ${date.getFullYear()}-${date.getMonth()1}-${date.getDate()}; }这个格式化方案的目标是“相对时间优先、绝对时间兜底”——刚收藏的资讯用“几分钟前”很有温度超过一周就老老实实展示日期避免用户认为时间错乱。实测这个方案在体验上比我见过的一些直接裸显示时间戳的方案高出一个水准。5.2 并发写冲突与交易管理收藏场景并发写的情况比较少见但并非不可能。用户连续点击收藏了两篇文章或者从列表页和详情页同时触发操作就可能出现并发写同一个数据库的情况。开源SQLite本身支持并发读但写操作是串行的OpenHarmony的RDB封装也继承了这一性质——多个写操作并发执行时底层会自动排队但上层代码仍然要处理好await的时序。遇到多字段更新或“先删后插”的复合操作时不要拆成两条独立SQL裸奔。正确姿势是用RDB提供的事务能力包起来async toggleFavoriteWithTransaction(item: FavoriteInfo, shouldAdd: boolean): Promisevoid { await this.store.beginTransaction(); try { if (shouldAdd) { await this.store.insert(favorites, values); } else { await this.store.delete(predicates); } await this.store.commit(); } catch (e) { await this.store.rollBack(); throw e; } }事务的意义在于原子性。如果插入失败但事务已经提交了一半理论上不会但防御性编程总是好的数据状态就会变得难以追踪。用事务包住之后要么全部成功要么全部回滚状态永远是干净的。5.3 存储数据的安全与迁移预留前文提到过securityLevel这个字段这里再强调一次S1适合对无特殊安全要求的普通数据S4适合高敏感数据。资讯收藏数据虽然看起来“都是公开信息”但用户的收藏行为本身也是一种隐私——反映出阅读偏好、关注领域甚至政治态度。所以我在配置里使用了不低于S1但兼顾性能的默认级别并在项目文档里标注了升级路径。另一个值得预留的坑是数据库版本升级。当前数据库版本是1表结构只在首次初始化时创建。后续如果要在favorites表上增加字段比如“是否已读”“分组ID”就需要在onUpgrade回调里做表结构变更。OpenHarmony的RdbStore在创建时可以传入版本号版本变化时会触发相应回调。我在初始化时预留了这个钩子{ name: DATABASE_NAME, securityLevel: relationalStore.SecurityLevel.S1, version: DATABASE_VERSION }升级逻辑不急于实现但版本号字段必须从一开始就规划好。等线上版本的数据表结构已经固化再回头补升级逻辑面临的就是大面积数据迁移的麻烦——能避免则避免。5.4 常见错误与坑速查表把我在实际开发中遇到的典型报错和处理办法整理成一张速查表方便你排查问题报错/现象可能原因处理办法建表时提示“table favorites already exists”数据库已创建过但初始化逻辑重复执行建表SQL使用IF NOT EXISTS避免重复建表插入收藏时抛ConstraintException相同news_id重复插入触发了UNIQUE约束insert时指定ON_CONFLICT_IGNORE或先查后插获取RdbStore时提示权限不足未在module.json5中声明存储相关权限检查module.json5权限声明Flutter侧调用原生方法超时原生方法阻塞在主线程将数据库读写放到异步线程中执行查询结果集未关闭导致内存上涨忘记调用result.close()养成用完即close的习惯用try-finally兜底收藏列表数据顺序错乱未按created_at排序或索引缺失查询时显式调用orderByDesc(created_at)数据库文件占用过大积累了冗余数据或索引碎片定期执行VACUUM或清理长时间前的收藏数据6. 端侧与设备侧适配问题在OpenHarmony上做持久化面对的不仅是代码逻辑问题还有设备和系统适配的细节。这里挑几个我实际处理过的问题说。6.1 设备存储位置与用户数据隔离OpenHarmony的应用数据目录是系统按包名隔离好的不需要开发者手动创建或管理磁盘路径。这比传统桌面应用的“把数据文件写到用户目录”要省心得多。RDB的数据库文件会自动落在应用的分布式数据目录下随应用卸载一并清除。但要注意如果你在开发过程中手动在代码里拼接了一个路径去创建数据库文件早期版本有人这么干应用升级或卸载重装时会出现“数据库文件残留”“新版本无法读取旧数据”的问题。正确做法永远是只传数据库名称给RDB框架让系统决定物理存储位置。这个原则在所有平台上都是一条铁律——不直接拼路径。6.2 不同屏幕形态下的收藏页适配OpenHarmony设备不只有手机还有平板、智慧屏甚至车机。收藏列表页在不同设备上的布局策略需要调整。我的做法是让列表项在宽屏设备上增加左右边距、在窄屏设备上保持全宽。Flutter侧用MediaQuery.of(context).size.width判断屏幕宽度超过阈值时套用更宽的约束即可。这不是一个很高级的技术点但属于“不说就没人告诉你的适配细节”。6.3 分布式场景的扩展思考OpenHarmony的一个特色能力是分布式数据管理——同账号的设备间数据可以协同。短期内资讯App的收藏数据只需要存在本机但随着HarmonyOS NEXT生态成熟未来可能出现“手机收藏、平板上看”的跨端续读需求。为此在DAO设计上我刻意隔离了本地数据源和远端数据源的接口——底层有朝一日要接入分布式数据库或云同步时只需替换DAO实现类UI层完全不受影响。这种面向未来的设计成本极低收益却可能在某个意想不到的版本迭代中直接体现。7. 实操验证与性能观察功能写完之后我按正常用户路径做了一轮完整的实操验证这部分的观察结果或许比代码更能说明问题。7.1 实测操作路径记录我在模拟器某运行OpenHarmony系统的测试设备上依次完成了以下操作流进入资讯列表页随机打开三篇文章。在A文章详情页点击收藏返回列表页确认A文章图标变为实心。再次进入A文章详情页确认收藏图标保持实心。在B文章详情页点击收藏然后直接进入收藏页确认A、B两篇文章均按收藏时间倒序展示。在收藏页对A文章取消收藏返回资讯列表页确认A文章图标变为空心。重启App进入详情页并确认B文章仍显示为已收藏进入收藏页并确认B文章仍在收藏列表中。整个流程没有出现状态不同步、闪退或数据丢失的问题。第6步是本次优化的重点——能扛过重启校验说明持久化的链路是通的。7.2 性能数据观察我对收藏列表的冷启动和滚动性能做了简单测量指标表现收藏列表首屏加载20条平均180ms内完成查询并渲染滚动帧率在测试机上稳定保持60帧收藏/取消收藏响应即时反馈无UI卡顿App重启后恢复收藏状态页面加载完成后150ms内完成恢复从数据看当前方案的性能余量是充足的。但需要说明的是这些数据是在一个测试设备上观察到的不代表真机所有型号的表现。如果后续收藏量突破千条或者列表项加上更复杂的图文卡片还需要重新评估分页大小和缓存策略。7.3 收藏页的滚动性能优化收藏列表的滚动性能优化核心思路和资讯列表类似——延迟加载、复用布局、避免过度重建。我做了两件事第一列表项图片使用缓存策略。封面图加载走统一的图片缓存层不做“每次滚动都拉网络图”的傻事。第二列表项widget拆成const构造的纯展示组件状态量降到最少。这两点能让列表滚动时的额外开销降到很小的程度实测体验符合预期。8. 后续扩展方向与个人心得收藏功能做完后其实可以发现它天然地带出了一些“下一步”的功能点。这里聊聊我个人对后续迭代方向的思考以及一些经验层面的碎碎念。8.1 功能扩展路径第一个自然会想到的是分组管理。用户收藏量一旦过百线性列表已经不够用了“全部收藏”之外应该支持“按话题分组”。这个需求对底层表结构的影响是增加一个group_id字段外加一张分组表。好消息是因为当前DAO层已将数据访问和UI解耦这种结构变更的改造范围是可控的不会牵连到页面层。第二个方向是收藏与稍后读的融合。收藏本质上是阅读行为的一种——收藏了不一定读所以“未读/已读”状态、阅读进度位置这些字段值得加进去。这套逻辑放到现有的RDB表上不过是增加字段和更新接口的事。第三个方向是云同步。前文说了分布式场景预留。如果产品要落地多端同步能力需要在远端建立一套与本地favorites表对应的同步数据结构并处理合并冲突。这是另一个量级的工作量但收藏功能的数据结构足够简单每条记录都有唯一ID和修改时间实现双向同步的复杂度远低于聊天记录同步。8.2 几个值得记住的取舍教训回看整个开发过程有一些决策当时觉得“不就是一个选择题”事后发现影响远超预期。第一个是明确区分“设计时的面向对象”和“运行时的性能优化”——不要为了虚拟的扩展性把简单功能复杂化也不要为了微秒级性能写出无法维护的代码。当前收藏功能的实现中RDB方案、批量查询接口、事件总线这三块就是权衡之后的产物。第二个是每个异步方法都要想清楚“失败时怎么办”。收藏按钮点击后如果数据库写入失败UI已经变了怎么让用户知道我在代码里加入了一个轻量Toast提示“收藏失败请重试”并把数据状态回滚为“未收藏”。这种边界处理看似只会出现在极端情况但恰恰是这样的细节决定了用户对App稳定性的评价。第三点是测试驱动思路的价值。在写数据库层代码之前我在一个最小化Demo里验证了RDB的完整读写链路确认OpenHarmony的API在Flutter桥接下能稳定工作后才把代码搬到主工程。先验证风险点再上量的策略帮我省下了大量联调时间。8.3 对FlutterOpenHarmony组合的一次坦白最后想坦白一点Flutter在OpenHarmony上的生态确实还是“前面跑着的人少、路也还没完全铺平”的状态。官方适配一直在推进但像RDB这种原生能力Flutter侧还没有像sqflite那样成熟的插件可以直接用。我的方案是通过自写Platform Channel桥接实现的——这意味着需要你去写原生侧代码不是纯Dart一把梭。但从另一个角度看这也意味着机会。关注这套技术栈的人正在变多能拿出完整可运行方案的人还不多。早点把坑踩平后面就能吃到技术红利。我这个系列坚持写到现在也是基于同样的判断——跨端平台后浪推前浪提前布局的人不会吃亏。就我目前的体验来说收藏功能这一套本地持久化的架构预计足够支撑后续若干个版本的迭代需要。如果你正在用Flutter做OpenHarmony方向的应用开发希望这篇把路趟平的文章能帮你少折腾几个晚上。
阅读完成 · 觉得有帮助?