先跑个题:现在做移动端开发,如果你还只盯着双平台,那确实有点浪费了这几年卷出来的跨端能力。我这边最近在做的一个项目就是基于Flutter跑在OpenHarmony上,做一个口腔护理类的App,其中“我的收藏”这个模块做起来比想象中更有意思。今天把这块的完整实现思路、踩坑记录和代码细节都整理出来,从环境搭建到收藏功能的最终落地,希望对正在折腾Flutter + OpenHarmony组合的人有参考价值。
1. 项目背景与整体架构拆解
1.1 为什么选择Flutter for OpenHarmony
先说背景。我们团队要做一个面向个人用户的的口腔护理应用,核心场景包括:用户记录每日刷牙情况、查看护理知识库、收藏感兴趣的文章和刷牙方案,以及追踪口腔改善趋势。产品定位是轻量级、重内容、高粘性,所以移动端是主战场。
选择技术栈的时候,我们认真对比过几条路线:一是用ArkTS + ArkUI写原生鸿蒙应用,二是用uni-app这类偏Web的跨端方案,三是用Flutter。最终敲定Flutter,原因有几个:
第一,团队技术栈本来就是Dart + Flutter,已经有沉淀下来的组件库和工具链,完全切换去写ArkTS意味着团队要从头适应一套新的UI范式和状态管理模式,学习成本不低。
第二,OpenHarmony的Flutter适配已经相对成熟,社区里有专门的flutter_for_openharmony分支,日常用的widget和API覆盖度已经能做到85%以上,而且性能损耗在可接受范围内,特别是渲染层走的是自绘引擎,跟ArkUI的原生组件栈并不冲突。
第三,从业务角度考虑,我们后续有计划把这款App扩展到其他OS平台,包括各类国产OS,如果直接用ArkTS写死,以后每次适配都是重写一遍业务逻辑。Flutter这种一处编写、处处调试的模式,能最大程度保护业务代码集的复用率。
当然这条路不是没有代价。最直接的痛点是生态依赖的资源文件管理和部分原生能力的桥接需要自己实现,特别是访问剪贴板、本地数据库加密这类基础能力,不能用pub.dev上那些默认适配Android/iOS的插件,得找到OpenHarmony兼容版本,或者自己封装。
1.2 口腔护理App的功能地图
这个项目标题里已经写得很明确——“口腔护理”和“收藏实现”。整个App的功能规划我列了一张表,方便理解后续的实现重点:
| 模块名称 | 核心功能 | 技术实现要点 |
|---|---|---|
| 每日护理打卡 | 记录刷牙次数、时长、使用牙线情况 | 本地数据库读写 + 图表统计 |
| 护理知识库 | 展示口腔护理文章、刷牙教程视频 | 远端API拉取 + 本地缓存 |
| 个性化方案 | 根据用户口腔评测结果生成护理建议 | 规则引擎 + 状态管理 |
| 我的收藏 | 收藏护理文章、收藏刷牙方案、收藏课程 | 多类型收藏 + 分组管理 + 收藏夹排序 |
| 个人中心 | 用户信息、设置、数据导出 | 本地存储 + 系统能力扩展 |
列表里我标出了“我的收藏”这行,因为它不是简单的“塞图片进数据库”这么单薄。实际业务里,用户收藏的目标有好几类——图文文章、视频课程、护理方案。这些类型的“收藏”虽然动作一样,但后续的展示形态、快捷入口、取消收藏后的页面反馈,都需要针对性地做差异化设计。
1.3 我的收藏模块的需求拆解
从产品文档里筛下来,“我的收藏”要满足这几个核心需求:
- 以列表形式展示用户所有已收藏内容,按收藏类型分组或者混合排列
- 每条收藏记录展示标题、封面、来源类型、收藏时间
- 支持取消收藏,且取消后要实时刷新状态
- 如果收藏内容已经被下架(服务端标记删除),要展示占位状态并提示用户
- 收藏数据本地即时同步,支持离线查看已收藏的内容摘要
- 提供“全部”“文章”“视频”“方案”等分类筛选Tab
因为是多类型收藏,数据建模就不能用单一字段搞定。我采用的是统一收藏模型 + 记录内容类型的思路,每一条收藏记录都保存了最小化的内容快照(标题、封面、作者、摘要),这样即使用户处于离线状态,收藏列表也能正常渲染,不必每次都靠网络回源。
2. 开发环境搭建与Flutter for OpenHarmony适配要点
2.1 完整的开发环境配置清单
先说结论:Flutter for OpenHarmony并不是一个独立的fork,它本质上是一套基于标准Flutter框架之上的平台适配层。官方维护的路径是通过OpenAtom OpenHarmony的Gitee仓库来拉取,不是直接通过flutter SDK命令行安装的那种常规方式。
我这边用的环境版本是:
- OpenHarmony SDK:从OpenHarmony官网下载的5.0 Release版本
- Flutter SDK:基于官方stable分支拉取后切换的flutter_flutter,再配合ohos分支适配
- IDE:DevEco Studio(用于构建hap和调试build-profile)
- Dart SDK版本:跟随Flutter SDK绑定
搭建时最容易卡住的是两个地方:一个是SDK的版本匹配,Flutter适配分支和OpenHarmony SDK Release如果版本错位,编译时会出现各种奇奇怪怪的符号找不到的报错;另一个是环境变量设置,需要增加OHOS_SDK_HOME指向DevEco的sdk目录,否则构建阶段会提示找不到native工具链。
我整理了一套快速检查环境配置的shell命令(macOS环境,Windows版同理调整路径):
export PATH="$HOME/flutter_for_ohos/bin:$PATH" export OHOS_SDK_HOME="$HOME/ohos-sdk" export DEVECO_SDK_HOME="$HOME/DevEcoStudio/sdk" flutter doctor flutter devices如果doctor输出里能看到ohos设备(真机或模拟器),说明SDK关联正常。我第一次配的时候,flutter devices始终只能扫到macOS和Chrome,排查了半天,最后发现是OHOS_SDK_HOME指向错了目录,指到了DevEco安装目录而不是内部的sdk子目录。
2.2 真机运行与编译产物详解
DevEco和Flutter工具链配合之后,运行方式很清晰:先通过DevEco创建或者导入一个空的OpenHarmony工程壳,这个壳工程负责生成hap包基础结构,然后把Flutter编译成的so库和Dart产物打包进去。
实际操作里,在项目根目录执行:
flutter build hap --debug成功之后会在build目录下生成hap文件。注意,直接双击hap在桌面上可安装不了,需要通过hdc工具安装到真机:
hdc install build/outputs/hap/debug/xxx.hap这个流程里我反复踩到一个问题:flutter build hap执行成功,但装进OpenHarmony设备之后白屏。后来定位到原因是Debug模式下Dart虚拟机需要连接观察者服务,如果设备跟电脑不在同一局域网或者USB调试端口没映射成功,就会卡在等待连接。解决办法是确保设备开启开发者模式,并且hdc list targets能看到设备。
2.3 依赖库与OpenHarmony的兼容性检查
这是最折磨人的环境适配环节。因为Flutter生态的主流库默认只做了Android和iOS的原生适配,用到OpenHarmony上会直接编译不过去。
我采用的策略是:
- 第一步,逐个检查pubspec.yaml里的依赖项,去OpenHarmony社区或flutter_for_openharmony仓库里翻一下是否已经有人贡献了ohos的实现。
- 第二步,对于没有适配的纯Dart库,比如状态管理、网络框架这类,它们本身不依赖原生平台通道,可以直接用,完全不用改。
- 第三步,对于涉及文件读写、数据库、分享面板等原生能力的库,优先找“pure dart”替代方案,实在找不到就自己封装平台通道。
以我们这个项目为例,用到的关键依赖清单及适配状态如下:
- dio:纯Dart实现,可直接用,处理网络请求
- provider:纯Dart实现,状态管理,直接用
- sqflite:原生插件,需替换为drift或直接使用OpenHarmony的轻量级偏好存储
- shared_preferences:已有ohos适配版,直接用
- cached_network_image:依靠原生图片解码,建议替换为普通Image.network + 自定义缓存策略
替换shared_preferences那块,需要在pubspec里显式指定git源指向ohos适配仓库,不能只写版本号,否则拉下来的还是Android版本。
3. 我的收藏模块的数据层设计
3.1 多类型收藏的统一建模方案
收藏这件事,产品上看似简单,但如果一个App里收藏的类型五花八门,后端还分开建表,前端就会很难做统一展示。我们的选择是:前端统一存储一条“收藏记录”,里面包含内容快照和类型标记。
数据模型我这样写的:
class FavoriteItem { final String id; // 收藏记录的唯一ID final String targetId; // 被收藏内容的原始ID final String type; // article / video / plan final String title; final String coverUrl; final String summary; final DateTime collectedAt; final bool isOfflineValid; // 本地快照是否还可用 }之所以要保存title、coverUrl和summary,是为了收藏列表页不需要实时调用远端接口。用户进入收藏页的加载速度直接从“等网络”变成了“读本地”,体感差别很大。
至于targetId和type的组合,在收藏时需要确保唯一性。我用的是“type + targetId”拼接成一个内部索引键,这样用户在文章页点了收藏,再在列表页取消收藏,能够精确匹配到同一条记录,而不会出现重复收藏或取消错位置的问题。
3.2 本地存储方案对比与选型
收藏功能最忌讳的体验问题是:网络差的时候打不开收藏列表。如果收藏数据全靠远端存储,那么离线状态基本就废了。所以我的设计原则是:远端负责同步,本地负责展示。
本地存储我对比过几种方案:
| 方案 | 优点 | 缺点 | 适配OpenHarmony难度 |
|---|---|---|---|
| shared_preferences | 轻量、接口简单 | 不适合存储大量列表数据 | 低 |
| sqflite | 功能完善、类SQL | 原生依赖,适配麻烦 | 中 |
| hive | 纯Dart、高性能 | 类型安全稍弱 | 低 |
| drift | 类型安全、响应式 | 依赖Dart代码生成 | 中低 |
最终选了hive,理由是它在纯Dart环境下就跑得非常稳,没有原生代码侵入,天然适配OpenHarmony。我在项目里建了两个box:一个存“收藏列表”,一个存“收藏索引”,分开存的好处是筛选收藏类型时不需要遍历整个列表再过滤,直接按索引key前缀去获取对应集合就行。
3.3 收藏状态同步与冲突处理机制
既然是App,就免不了多端数据一致性的问题。我们的App还没有强制登录,所以收藏数据一开始就是本地优先的,只在用户主动“同步到云端”时才走远端接口。简单场景下,这种设计避免了很多麻烦。
冲突处理主要发生在“本地取消收藏后,云端还停留着旧的收藏状态”这钟情况。我处理的方法比较简单粗暴:本地收藏操作产生的变更事件会记录一个同步标记,每次App启动时,如果检测到有未同步的变更,就向服务端提交批量变更请求。
为了确保操作反馈是即时的,我设计成了“先改UI、再写本地、最后同步远端”的三段式。任何一次收藏或取消收藏操作:
- 更新内存中的状态(Provider里的状态类)
- 异步写入hive本地存储
- 队列里追加一个待同步任务,网络可用时统一上报
这个方法在弱网环境下实测很稳,用户不会因为网络慢而觉得界面卡住了。
4. 收藏列表页的UI实现与交互细节
4.1 双Tab布局与收藏类型分组展示
从视觉交互的角度,我的收藏页如果没有分类梳理,很容易变成一锅粥。因此我在页面上做了一个二级结构:头部是Tab切换栏,下方是内容列表区。
第一个Tab是“全部收藏”,内部按时间倒序排列,不做类型区分,但每一条卡片右上角会显示类型标签(图文/视频/方案)。第二个Tab到第四个Tab分别是“文章”“课程”“方案”三个分类Tab,进入后只展示对应类型的数据。
这个交互在代码层面的核心是一个TabController + 三个独立列表。一开始我想偷懒,做一个ListView根据筛选条件实时过滤,后来发现数据多了以后,切换Tab时还会触发重新渲染和计算,简单场景没区别,但到上千条收藏时明显有卡顿。所以最佳实践还是用一个IndexedStack包住三个独立列表,每个列表各自监听自己对应的数据流。
这样还有一个额外好处:用户在某个Tab里滚动到很长的位置,切换到其他Tab再回来时,滚动位置是保留的,不会被重置。
4.2 卡片组件的设计策略
收藏列表的单条样式,我试过几轮之后确定了一套组合:左侧是封面缩略图,右上角是类型角标,中间部分是标题和摘要,左下角是收藏时间。右侧放置一个取消收藏的按钮,但按钮平时隐藏,只有点击卡片进入详情页后再做“取消”行为。
我把常见的三种类型的卡片放一起做了对比:
- 文章卡片:封面是横版图,摘要文字显示两行,时间放在底部
- 视频课程卡片:封面居中缩略图,右下角叠一个播放ICON,点击卡片直接跳转到播放页
- 护理方案卡片:封面可以是自定义渐变色块,没有图片时用Scheme主题色兜底,更清爽一些
卡片组件的难点在于“不同数据源 → 统一样式结构”的映射。我封装了一层FavCardAdapter,根据FavoriteItem.type返回对应的子卡片widget,但整体卡片的外部边框、点击水波纹、阴影样式保持一致。
4.3 下拉刷新、分页加载与空状态设计
因为收藏数据可能很多,列表不做分页就会导致Hive加载耗时过长,所以我在列表底部实现了“上拉加载更多”的逻辑:每次先取30条,滚动到底部时再加载下一批。
下拉刷新则是重新从远端拉取收藏状态,跟本地做合并。这里有一个小坑,如果远端返回的数据和本地差异太大(比如另一台手机已经取消了某条收藏),本地记录的updatedAt时间可能比较老,合并策略就变得复杂。我采用了比较保守的方案:远端记录和本地记录如果id一致,以远端为准;如果本地有而远端没有,视为已删除,从本地剔除。
空状态我做了两套视觉:一种是彻底没有任何收藏,页面中央放一个插图,配文案“去发现值得收藏的内容”;另一种是当前分类筛选后没有内容,展示“该分类下还没有收藏内容”的轻提示。区分这两种状态对于用户理解很重要,总不能让人以为收藏丢失了。
5. 收藏交互流程:收藏、取消收藏与详情页联动
5.1 收藏按钮的即时响应与防重复点击
用户在内容详情页点击收藏按钮时,最怕遇到的情况是:点了收藏,按钮变成红色实心,然而实际因为网络慢,操作其实没成功,过一会按钮又自动弹回去了。这种交互不确定性很影响信任感。
我的处理方式是:
- 收藏按钮点击后,立刻进入“处理中”状态,按钮禁用,防止重复提交
- 同时写入Hive本地存储,更新Provider状态,页面UI立即切换为已收藏
- 如果后续同步云端失败,才把状态回滚成未收藏
因为这个App的收藏功能核心是本地优先,所以即使云端失败,本地收藏也并不会丢失,用户不会感知到任何异常,最多就是下次启动时看到收藏内容仍然还在。
代码里我用了Provider + ChangeNotifier统一管理状态:
class FavoriteController extends ChangeNotifier { Set<String> _favoriteKeys = {}; bool isFavorite(String type, String targetId) { return _favoriteKeys.contains('$type_$targetId'); } Future<void> toggleFavorite(FavoriteItem item) async { final key = '${item.type}_${item.targetId}'; if (_favoriteKeys.contains(key)) { _favoriteKeys.remove(key); await _localStore.delete(key); } else { _favoriteKeys.add(key); await _localStore.put(item); } notifyListeners(); } }这个类写得非常精简,但实际用下来很可靠。核心只维护两个动作,收藏和取消收藏。
5.2 详情页与收藏列表页的状态互通
这里要重点说一下跨页面状态同步。用户从收藏列表点进详情页,如果直接在详情页取消了收藏,然后返回列表页时,列表里那条记录不应该还躺在那里。
我用的是Provider在根节点注入FavoriteController,让所有页面共享同一个状态容器。这样当详情页执行取消收藏后,不需要发任何事件通知列表页,列表页在rebuild时自动从provider里取最新的收藏key集合,发现这条收藏已经不存在了,就把对应记录从显示数据里过滤掉。
为了防止用户“从收藏列表点进详情页→取消收藏→返回列表→列表还在随后才消失”的现象,我特意在收藏列表每一项的回调里做了本地状态读取,而不是缓存列表构建时的旧状态。也就是:
onTap: () { Navigator.push(...).then((_) { // 重新读取FavoriteController里的收藏状态 _refreshCurrentList(); }); }5.3 批量取消收藏的实现与效率优化
除了单条取消,我们产品还有一个批量管理的入口:长按卡片进入多选模式,然后可以一次性取消多条收藏。
批量取消的效率差异在数据量上来以后非常明显。如果是遍历每一条调用toggle,每调一次都触发notifyListeners并重写整个Hive box,会卡顿得非常明显。优化后我把操作分成了两步:
- 先批量删除Hive中的数据,一次性写入
- 最后统一notifyListeners,触发一次UI刷新
实测在500条收藏里一次性取消50条时,这个方案要比逐个操作快接近一个数量级,界面没有任何掉帧感觉。
5.4 特殊场景处理:内容被下架后的占位展示
内容运营过程中,部分文章或视频会被下架或设置为不可见,这时候收藏列表如果还让用户点进去然后看到404页面,体验就很糟糕。
我实现的方案是:收藏记录里保存内容快照(标题、封面、摘要),同时保存一个isOfflineValid布尔值。如果某次同步发现原内容失效,就把这个值置为false。对应在UI上,卡片的封面上会叠一层半透明遮罩,摘要处替换成“内容已下架”,点击时弹出一个轻提示“该内容已失效,可从收藏中删除”,而不是跳详情页。
从产品角度看,这样既保留了用户曾经收藏的痕迹,又避免用户觉得收藏是个“不可靠”的功能。
6. 网络层与收藏数据的远程同步
6.1 接口设计与请求模型
收藏相关的远端接口,我们设计得比较克制。总共就三个:
POST /api/v1/favorites/sync GET /api/v1/favorites DELETE /api/v1/favorites/:type/:targetIdsync接口是批量提交本地变更,请求体是一组操作序列。这里重点说一下为什么不用单条的add接口。用户在弱网环境下的操作往往是多条积累,如果每条都单独请求,服务器压力大,客户端也容易被限流。批次处理更适合移动端场景,尤其适合我们这种本地优先的存储结构。
实际对接时,我们用dio做网络封装,注意需要配置好baseUrl、超时时间,并处理Token过期的场景。Service代码大概长这样:
class FavoriteSyncService { final Dio _dio = Dio(BaseOptions( baseUrl: 'https://api.example.com', connectTimeout: 10000, receiveTimeout: 10000, )); Future<void> syncLocalChanges(List<FavoriteChange> changes) async { final response = await _dio.post('/api/v1/favorites/sync', data: { 'changes': changes.map((e) => e.toJson()).toList(), }); if (response.statusCode != 200) { throw Exception('同步失败'); } } }6.2 增量同步策略的设计细节
增量同步不是把本地所有收藏每次都全部上传,那是不可持续的。我的设计中每个收藏记录都带有本地的updatedAt时间戳,每次同步时,服务端根据这个时间截断,只处理updatedAt大于上次同步时间的记录。
这个策略还有一个好处,就是支持多设备间最终一致性。比如手机A取消了收藏,时间戳更新了,手机B在同步时会发现这条记录在远端已经标记为删除,本地对应条目也会被清掉,不会残留。
当然有一个边界问题需要考虑:如果设备长期离线,时间戳完全靠客户端本地生成,可能会出现时间错乱。我这里的规避方式是每次从服务端拉取时间作为校准基准,本地记录时用的是设备时间,但上传时换算成服务器时间偏移量,尽量保证时间戳顺序的准确性。
7. 性能优化与细节打磨
7.1 列表滑动帧率优化实践
我在收藏列表页性能优化上做了三个层面的工作。
第一层是图像缓存。封面图不能每次都走网络加载,那样滑动时会产生大量图片请求负载。后来我封装了一个本地图片缓存的工具类,基于dio下载封面并保存到文件系统,下次加载时直接读文件,加载速度提升了数倍。
第二层是商品化容器复用。在Flutter里,ListView.builder本身就是懒加载,但需要注意不要在itemBuilder里做过重的计算。我把FavoriteItem的模型toJson和解析逻辑全部放在模型类内部,尽量避免在build过程里做Json序列化。
第三层是消抖。用户在短时间内快速滑动时,会触发大量item widget创建。我在itemBuilder里对封面图加载做了延迟优化,只有在item真正滑入可视区域时才触发图片解码,离开可视区域就取消解码任务。这一块用VisibilityDetector库处理,实测帧率从偶发18帧提升到了稳定55帧以上。
7.2 Hive存储的性能调优
Hive本身性能很好,但用得不对也容易踩坑。我遇到的最大问题是在box里存储大量FavoriteItem对象,每次新增或删除时,Hive会触发整个box的写入锁,导致UI线程卡顿。
解决方案是:
- 将收藏列表拆成“索引box”和“数据box”,索引box只存收藏key的小列表,数据box按key存独立条目
- 操作单条收藏时,只重写单独条目,不同时操作整个列表
- 批量操作时用box的writeTxn事务,减少反复的文件flush
7.3 冷启动收藏状态恢复优化
App启动时,收藏功能不能拖慢整体启动速度。我的做法是先加载索引box里的key集合,这个数据小,加载速度快,可以先让收藏按钮变成正确的“已收藏/未收藏”状态。然后后台异步加载完整收藏列表,约200毫秒后插入到列表页。
这样用户在详情页打开时几乎感觉不到收藏状态的加载过程,而收藏列表页虽然数据是延迟加载的,但会有一个轻量的骨架屏过渡,不至于白屏。
8. 踩坑记录:Flutter for OpenHarmony实战中遇到的6个典型问题
8.1 编译报错:undefined symbol: OH_AbilityContext
这是一个非常典型的OpenHarmony适配弗lod问题。原生平台通道在编译时找不到OpenHarmony的Ability接口符号。
排查思路是这样的:
- 检查build-profile.json5里是否配置了正确的signingConfigs
- 检查CMakeLists里是否添加了对应的libace_lib依赖
- 确认native代码里import的OpenHarmony的头文件路径是否正确
我这个问题的根因是native层使用了旧版本的OpenHarmony NDK头文件,升级到5.0 Release配套的NDK之后,重新编译就通过了。
8.2 真机调试时无法连接Dart VM
设备已经用USB连上了,hdc也能看到,但是flutter attach就是找不到进程。这个问题折腾了我两天。
后来破案了:OpenHarmony的开发者模式需要手动打开“可调试”开关,而且需要在开发者选项里额外勾选“允许调试模式”。默认只在部分build变体下开放,release构建不支持attach。全盘检查之后我才发现是包类型弄错了,用release包去连接调试,当然不行。必须用debug包。
8.3 图片加载失败与解码异常
在OpenHarmony上,Image.network解码网络图片的底层链路和Android不同,部分图片格式在低版本OpenHarmony上不支持。我在收藏封面图中遇到WebP格式解码失败的问题。
最终方案是在网络层强制转换成JPEG格式缓存,或者要求服务端上传封面图时统一走CDN转码成标准格式。如果项目里必须用WebP,可以用纯Dart的解码库,例如image包手动解完再交给Flutter渲染。
8.4 Hive打开box时文件损坏
有用户反馈,App被系统强杀后,再次启动时会偶发HiveBoxCorrupted异常。排查下来是OpenHarmony的文件系统缓存策略跟Android不同,在系统杀进程的瞬间,Hive可能还没flush完。
解决方案是自定义Hive的存储目录,并在每次写入后手动调compact()。同时在初始化时增加try-catch,如果出现损坏就直接删除旧box重建,保住可用性。
8.5 列表卡片点击后,页面状态错乱
问题出在收藏列表跟详情页之间传参。因为路由跳转时直接把整个FavoriteItem对象传过去了,而详情页里其他操作可能会修改这个对象的引用,导致返回列表时原条目的状态也变了。
最后的解法很简单:跳转时传targetId和type两个参数,详情页自己根据id重新拉取数据,或者从FavoriteController里重新查找,避免共享同一个可变对象实例。
8.6 从本地预览图加载慢
Hive存储收藏快照后,封面图路径存储在记录里,但图片文件本身可能同时被系统清理掉(比如缓存清理),导致列表加载时图片404。
我采取的是双保险:图片如果在本地磁盘不存在,就自动降级为网络占位图,同时后台重新下载一张存回缓存目录。
9. 一个容易被忽略的细节:收藏入口的多场景拓展
“收藏”功能如果只在详情页放一个按钮,用起来总觉得不够顺手。我做了一些额外的入口优化,这里也一并分享。
第一个场景是文章列表页。用户在信息流里刷到感兴趣的文章,不需要点进详情页,可以直接在列表卡片右下角操作收藏。这个入口的设计需要避免误触,所以平时是一个低调的收藏图标,点击之后才高亮填充。
第二个场景是分享回流。用户把内容分享给朋友,朋友打开后如果觉得不错,可以通过分享页底部的快捷“收藏”入口,直接收藏到自己的账号下。
第三个场景是全局搜索的结果页。搜索结果里已经命中了某些收藏过的内容,结果卡片上会直接显示“已收藏”的标签,省得用户反复点进去确认。
多入口意味着同一个toggleFavorite方法会从多个地方同时触发。由于FavoriteController里维护的是统一状态,天然免疫了多入口并发操作的问题。但需要注意,不同的入口在调用时,最好都能先判断当前收藏状态,避免连续点击导致状态反复横跳。
10. 关于未来规划:收藏功能的可扩展方向
最后聊两句这个模块以后可能的演进方向。
现在收藏的粒度是“整篇内容”,但实际口腔护理场景里,用户有时只想收藏文章里的某一个刷牙小技巧,或者某一个具体操作步骤。如果后续做收藏的精细化,可以考虑支持“段落级收藏”,把文章的指定区域生成一个引用片段保存,类似于剪藏功能。
另一个方向是收藏夹分组。目前的收藏是扁平结构,当收藏数量破百之后,找一条老收藏就变得麻烦。再加上“自定义分组”“智能分组(按类型自动归类)”等能力,就能更贴近知识管理工具的使用习惯。
还有一点是收藏内容的周期性提醒。比如用户收藏了一篇“儿童正确刷牙指南”,系统可以在每周日提醒用户回顾一下。这个功能结合本地通知和收藏数据的元信息,可以实现得比较简单,但对产品价值的提升却很明显。
这些就是我这次做Flutter for OpenHarmony口腔护理App时,关于“我的收藏”模块的全部实战笔记。个人体会是,跨端适配的最大成本其实不在于Flutter框架本身能不能跑起来,而在于生态里大量第三方库和原生能力的兼容迁移。在这条路上,收藏这类偏数据存储和UI展示的功能,反而是最不容易掉链子、最能快速跑通的模块。如果只是拿它当试水Flutter适配的第一个功能,再合适不过了。