ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flutter 适配 OpenHarmony 实战:收藏搭配功能跨端实现全解析

Flutter 适配 OpenHarmony 实战:收藏搭配功能跨端实现全解析 Flutter 跑在 OpenHarmony 上听起来很诱人真正做起来才发现不只是换一个 target 那么简单。最近我一直在做衣橱管家 App 的 OpenHarmony 适配其中最有代表性的就是收藏搭配实现用户在搭配详情页点一颗红心列表页、桌面卡片、原生侧都要一起动。别小看这个功能它一次牵扯到 Flutter 状态管理、本地持久化、MethodChannel 双向通信和 EventChannel 事件监听。这篇文章把从搭建 OpenHarmony 版 Flutter 工程到收藏搭配功能落地的完整过程梳理出来中间会附上可用的代码和踩坑记录。如果你想把手头的 Flutter 项目迁到 OpenHarmony或者正在纠结收藏类功能怎么做跨端同步这篇应该能帮上忙。1. 整体设计收藏搭配在跨端项目里的定位1.1 项目背景衣橱管家 App 到底做什么衣橱管家 App 的核心场景是帮用户把衣服数字资产化拍照录入每一件上衣、裤子、鞋子和配饰然后在衣橱里组合成一套套搭配。用户可以给搭配写备注、看天气建议也可以直接收藏那些“今天不知道穿什么”时一眼相中的组合。这个 App 最开始只做了 Android 版后来要覆盖 OpenHarmony 设备。OpenHarmony 上现在已经能跑不少原生应用但重建一个团队、重写一套页面成本太高所以团队决定用 Flutter 做跨端。衣橱管家 App 的页面复杂度不低有网格、有滑动删除、有瀑布流但 Flutter 在列表渲染和交互动画上一直比较稳选它是最不折腾的路径。整个项目里收藏搭配虽然只是一个小模块却是用户留存的核心它把“随机翻衣柜”变成了“快速选穿搭”所以这个功能的质量直接决定用户体验。1.2 核心需求拆解收藏搭配要解决哪些问题收藏搭配看起来很简单界面上就是一个爱心图标但实际上拆开之后至少有四层问题。第一层是收藏状态的即时响应。用户在详情页点击收藏红心必须立刻亮起来不能等数据库写完了再刷新图标否则会有明显的掉帧感用户会以为没点到。第二层是跨页面一致。衣橱首页、搭配列表、收藏夹、详情页会同时展示同一个搭配的收藏状态任何一个页面把状态改了其他页面不能还停留在旧数据上。第三层是持久化。收藏数据不能每次启动 App 都丢退出进程再进来收藏列表必须还在。第四层是 OpenHarmony 原生侧的联动这个最容易被忽略。OpenHarmony 的桌面卡片可以展示搭配用户在卡片上点收藏时事件要能回到 Flutter 页面里同时 Flutter 收藏后也要把信号同步给原生侧。四层问题环环相扣任何一个环节处理不好功能都能跑但体验会别扭。设计的时候我会按照“状态单一数据源 异步持久化 消息总线”的思路来组织代码。1.3 技术选型为什么是 Flutter OpenHarmonyOpenHarmony 不是 Flutter 官方支持的 target必须有专门的适配分支。OpenHarmony SIG 维护的 flutter_flutter 分支能构建出 .hap 安装包市面上常说的“Flutter for OpenHarmony”主要就是这套东西。用这个组合最直接的好处是业务代码可以复用Dart 层面的一套衣橱逻辑在 Android、iOS、OpenHarmony 上都能跑只有真正需要触碰系统能力的部分才写平台通道。相比 React NativeFlutter 在 OpenHarmony 上的渲染层更独立不依赖宿主 View 体系OpenHarmony 侧接入成本相对可控。但代价也很明显社区生态比 Android/iOS 差一截很多第三方 plugin 默认没有 OpenHarmony 实现使用前必须确认平台接口是否适配。后面我会专门讲这种第三方依赖缺失的应对方案核心思路是用 MethodChannel 自己补一条原生通道。2. 工程搭建把 Flutter 跑成 OpenHarmony 的 HAP2.1 安装 OpenHarmony 版 Flutter SDK普通 Flutter SDK 打不出 HAP需要先换成 OpenHarmony 分支的 Flutter SDK。开 GitHub 找到 OpenHarmony 的 flutter_flutter 仓库把它 clone 到本地然后配置 SDK 路径。这里有一个容易踩的坑不要直接删掉原版 Flutter SDK保留两个 SDK 目录运行时切换。OpenHarmony 分支的版本迭代节奏比主线慢比如主线已经 Flutter 3.2x 了OH 分支可能还停在较老的版本。项目开始前先在flutter --version里确认当前分支版本号后面所有依赖版本都按这个版本来锁定。配置完成后执行flutter doctor会多出 OHOS 相关的检查项。如果提示 SDK 尚未被完全支持就是那个常见警告The current configured Flutter SDK is not known to be fully supported不用太慌这是分支版本与工程模板版本不一致导致的。只要flutter doctor能看到 OpenHarmony 工具链并且能创建 ohos 工程就可以继续。我一般把警告当作“提醒锁定版本”的信号而不当成致命错误。2.2 创建 Flutter 工程并补充 OHOS 平台在 Android Studio 里正常创建 Flutter 项目工程名就叫wardrobe_app。不过要注意创建之后还不能直接跑需要手动添加 OpenHarmony 平台支持。传统方式是执行flutter create --platforms ohos wardrobe_app这条命令会在项目里生成一个ohos目录里面是 OpenHarmony 工程外壳。这个目录结构跟 Android 的android/、iOS 的ios/类似Flutter 引擎和 Dart 代码会被打进这个原生壳里。如果你的项目是从老工程迁移过来的没有ohos目录也可以用同样的命令补生成不影响已有代码。ohos目录里有三个东西要重点关注entry/src/main是 Ability 的入口ohos/build-profile.json5控制应用签名和目标设备AppScope/app.json5记录应用包名和权限声明。后续如果有原生日历、相册等权限需求都在这里配置不能只写 Flutter 侧代码。2.3 第一次构建 HAP 与签名配置项目能跑起来之后第一个目标是打出可安装的 HAP。执行命令flutter build hap --release这个过程会把 Flutter 引擎、Dart AOT 产物、原生 Ability 和资源全部打进一个 HAP 包。如果直接安装到开发机测试也可以先用 DevEco Studio 打开ohos目录在 “File Project Structure Signing Configs” 里勾选自动签名然后点运行。签名这个环节最容易卡人。OpenHarmony 的应用安装必须签名没有签名文件就会提示 install failed。用 DevEco Studio 的自动签名会自动申请调试证书但它在命令行构建时不一定生效。我建议第一次跑通还是用 DevEco Studio 点击运行确认能启动后再回头搞flutter build hap的命令行自动签名配置。把签名信息写进 build-profile 之后命令行构建就没有障碍了。3. 收藏数据层如何优雅地存住那些搭配3.1 领域模型ClothingItem 与 Outfit收藏搭配是围绕“搭配”这个概念展开的所以数据模型需要先立起来。衣橱里的最小单位是衣物一个大衣物品类可以是这样class ClothingItem { final String id; final String name; final String category; // top / bottom / shoes / accessory final String imagePath; final ListString tags; ClothingItem({ required this.id, required this.name, required this.category, required this.imagePath, this.tags const [], }); factory ClothingItem.fromJson(MapString, dynamic json) { return ClothingItem( id: json[id] as String, name: json[name] as String, category: json[category] as String, imagePath: json[imagePath] as String, tags: (json[tags] as List?)?.castString() ?? [], ); } }搭配Outfit则是衣物的组合再加上创建时间和收藏状态class Outfit { final String id; final String name; final ListString clothingIds; final String note; final DateTime createdAt; final bool isFavorite; Outfit({ required this.id, required this.name, required this.clothingIds, this.note , required this.createdAt, this.isFavorite false, }); }字段类型定得越严谨后面做收藏列表筛选和排序就越省心。isFavorite直接放在搭配对象上是最直观的做法但要注意它只是一个内存状态真正的收藏关系还要落到持久层。3.2 收藏关系设计用集合而不是到处塞布尔值一开始我把isFavorite分散放在每个 Outfit 对象里结果发现收藏/取消收藏时要同时改好几个页面持有的对象很容易漏改。后来改成“收藏集合模式”内存里维护一个SetString专门存已被收藏的搭配 ID。class FavoriteState { final SetString favoriteOutfitIds; final ListOutfit allOutfits; bool isFavorite(String outfitId) favoriteOutfitIds.contains(outfitId); }所有页面判断一个搭配是否被收藏都通过isFavorite(outfitId)查集合而不是读取 Outfit 的字段。这样收藏操作只改一个集合页面通过监听状态变化自动刷新不会出现不同页面状态不一致的问题。收藏列表也从集合派生遍历全部搭配把 ID 在集合里的捞出来再按收藏时间倒序显示。3.3 持久化实现从 Hive 到 JSON 文件的取舍收藏数据不能只留在内存里必须持久化。最初我打算用 Hive因为这个库在 Flutter 社区用得多而且性能不错。但实际在 OpenHarmony 上遇到一个问题Hive 需要借助path_provider获取应用文档目录而path_provider在 OpenHarmony 上的插件适配当时并不完整拿不到正确的沙箱路径。反复试了几次之后我决定不走第三方插件直接用 MethodChannel 问原生侧要一个可写目录然后用 Dart 的File把收藏数据写成 JSON。这个方案有几个好处不依赖社区插件的 OpenHarmony 适配进度数据文件放在原生沙箱里原生侧也能直接读JSON 结构化程度足够排错时打开文件就能看内容。获取目录的 Dart 代码是这样的static const MethodChannel _storageChannel MethodChannel(wardrobe/storage); FutureString getAppStoragePath() async { final path await _storageChannel.invokeMethodString(getAppCacheDir); if (path null) { throw Exception(无法获取应用沙箱目录); } return path; }原生侧用 ArkTS 实现同一个 Channelchannel.setMethodCallHandler(async (call) { if (call.method getAppCacheDir) { const context getContext(this); return context.applicationInfo.cacheDir; } return null; });拿到目录之后维护一个FavoriteRepository负责读文件、写文件、修改收藏状态。代码长这样class FavoriteRepository { final String _filePath; FutureSetString loadFavoriteIds() async { final file File(_filePath); if (!await file.exists()) return {}; final content await file.readAsString(); final list jsonDecode(content) as List; return list.castString().toSet(); } Futurevoid saveFavoriteId(String id, bool isFavorite) async { final ids await loadFavoriteIds(); if (isFavorite) { ids.add(id); } else { ids.remove(id); } final file File(_filePath); await file.writeAsString(jsonEncode(ids.toList())); } }这个 Repository 不关心页面怎么展示只负责“收藏集合从哪来、存到哪去”。UI 层不会直接调用它而是先通过状态管理更新内存集合再由中间层异步落盘。这样点击收藏的动画能立即响应写入失败也有机会回滚。3.4 数据访问接口为原生同步预留扩展点持久化层面向外部提供的是一个抽象接口而不是一个具体类。接口定义尽量小方便后续换数据库或者接入 OpenHarmony 分布式数据服务时不改动上层。abstract class FavoriteDataSource { FutureSetString loadFavoriteIds(); Futurevoid saveFavoriteId(String id, bool isFavorite); }JSON 实现只是其中一个实现。后面如果要做跨设备同步可以加一个 OpenHarmony 分布式实现把本地 Set 的变化同步到其他设备上层逻辑完全不用动。这个抽象设计在整个收藏功能里看着不起眼但后期接入桌面卡片和原生同步时帮我省了不少麻烦。4. Flutter 与 OpenHarmony 原生互操作收藏背后的跨端消息4.1 MethodChannel 实现收藏数据写入原生侧在 OpenHarmony 场景下Flutter 不能只在自己的一亩三分地里玩。桌面卡片需要知道当前用户收藏了哪些搭配所以 Flutter 侧的收藏状态要同步到原生侧 Preferences 里。MethodChannel 的用法大家很熟Android 上怎么写OpenHarmony 上基本就是什么路子。在 Dart 侧定义一个通道专门负责写收藏信号static const MethodChannel _favoriteChannel MethodChannel(wardrobe/favorite); Futurevoid syncFavoriteToNative(String outfitId, bool isFavorite) async { try { await _favoriteChannel.invokeMethod(saveFavorite, { outfitId: outfitId, isFavorite: isFavorite, }); } on PlatformException catch (e) { // 记录日志不影响收藏主流程 } }原生侧在 ArkTS 里通过ohos.data.preferences保存收藏标记import preferences from ohos.data.preferences; async function saveFavorite(args) { const pref await preferences.getPreferences(getContext(this), wardrobe_store); await pref.put(favorite_${args.outfitId}, args.isFavorite); await pref.flush(); }这里有一个重要的经验MethodChannel 调用要放在持久化完成之后但不能阻塞 UI。具体做法是先更新 Flutter 内存状态让界面变红心再异步写入本地 JSON最后调 MethodChannel 通知原生侧。如果先把网络请求或原生调用放在前面红心会有几百毫秒延迟体验很差。4.2 EventChannel 接收桌面卡片收藏事件MethodChannel 适合 Flutter 主动请求原生返回数据适合 A 调用 B 然后等结果这种“你问我答”模式。但如果原生侧主动发生了一个事件想要告诉 Flutter再用 MethodChannel 就吃力了因为 Flutter 没办法长时间挂一个回调等原生来找它。这种场景需要 EventChannel。我在衣橱管家 App 里遇到的具体场景是这样的OpenHarmony 的桌面卡片上有一个收藏按钮用户在桌面不打开 App 就能直接收藏当前搭配。桌面卡片属于原生侧 Form 能力它点击之后要通知到 Flutter 进程。如果 App 正在前台Flutter 需要收到事件并立刻更新页面如果 App 不在前台则靠原生侧持久化等下次启动时加载。EventChannel 在 Dart 侧的使用代码static const EventChannel _favoriteEventChannel EventChannel(wardrobe/favorite_sync); void listenFavoriteChanges() { _favoriteEventChannel .receiveBroadcastStream() .listen((event) { final data event.castString, dynamic(); final outfitId data[outfitId] as String; final isFavorite data[isFavorite] as bool; // 更新状态管理中的收藏集合 context.readFavoriteCubit().syncFromNative(outfitId, isFavorite); }); }从这里能看到 EventChannel 和 MethodChannel 的分工差异前者是单向事件流后者是双向请求响应。如果你发现自己用 MethodChannel 写了一个循环轮询来等原生变化那一定是用错了通道应该果断换成 EventChannel。4.3 PlatformView 接入系统相册选择搭配封面收藏列表的卡片和详情页都需要图片图片来自用户拍照或系统相册。在 OpenHarmony 上调用系统相册我优先想到的是找现成 image picker 插件但很多插件只适配 Android/iOSOpenHarmony 上没有实现。如果要等插件适配项目排期等不起。我最终的方案是用 PlatformView 包一个原生的图片选择能力。PlatformView 允许把原生 View 嵌入 Flutter 的 Widget 树里OpenHarmony 的容器侧也可以提供对应的原生组件。这样衣橱管家 App 在“选择封面”这一步跳出来的是系统相册原生界面用户选完图片回调给 Flutter然后继续走收藏列表的排版展示。PlatformView 的接入有几个注意点原生侧要正确管理视图生命周期App 退后台再回前台时视图不能黑屏Flutter 侧的android:hardwareAccelerated之类的策略要调整OpenHarmony 上如果出现视图尺寸不对多半是原生组件没有实现好layout回调需要重置宽高。4.4 第三方插件缺失时的兜底思路这套收藏功能里真正需要触碰原生的地方其实不少。除了保存偏好和桌面卡片事件还有一个坑就是第三方插件。比如我用过某个图片压缩插件在 Android 上表现很好但因为它底层用了 Android SDK 的 Bitmap 接口OpenHarmony 上完全跑不起来。遇到这种问题我的处理路径是先看插件是否提供了 Federated Plugin 结构有没有xxx_open_harmony这个独立包如果没有就自己写一个 MethodChannel 适配层把插件核心功能用 OpenHarmony 原生 API 实现对外仍然暴露同样的接口。这样 Flutter 业务侧不用改唯一要替换的是依赖注入那一行。5. 收藏交互按钮、列表、路由的配合5.1 收藏按钮的状态管理与动画收藏按钮看起来只是一个小图标但交互细节非常多。我用了 Cubit 做状态管理Cubit 相比 Bloc 样板代码更少适合这种局部状态比较复杂的场景。FavoriteCubit 的核心实现class FavoriteCubit extends CubitFavoriteState { FavoriteCubit(this._repository) : super(FavoriteState.initial()); final FavoriteDataSource _repository; Futurevoid toggleFavorite(String outfitId) async { final currentIds state.favoriteOutfitIds; final isFavorite !currentIds.contains(outfitId); // 1. 乐观更新UI 马上响应 final newIds SetString.from(currentIds); if (isFavorite) { newIds.add(outfitId); } else { newIds.remove(outfitId); } emit(state.copyWith(favoriteOutfitIds: newIds)); // 2. 异步落盘 通知原生 await _repository.saveFavoriteId(outfitId, isFavorite); await syncFavoriteToNative(outfitId, isFavorite); } }这里用的是乐观更新策略先改内存状态让 UI 立刻刷新再去写文件。如果写入失败回滚到之前的集合并给用户一个短暂提示。这个回滚逻辑看着简单但很关键不然用户会以为自己收藏成功了实际重启后数据没保存住。动画方面收藏按钮的反馈用了两层动画。第一层是点击时的缩放AnimatedScale( scale: isFavorite ? 1.0 : 0.8, duration: const Duration(milliseconds: 200), child: Icon( isFavorite ? Icons.favorite : Icons.favorite_border, color: isFavorite ? Colors.red : Colors.grey, ), )第二层是图标切换的过渡。直接用AnimatedSwitcher会让图标在切换时有个淡入淡出配合缩放非常跟手。实测下来动画时长 200 毫秒左右最舒服太短显得生硬太长又让人等得着急。5.2 收藏列表、详情页、首页之间的状态同步衣橱管家 App 里同一套搭配会出现在三个页面首页推荐、搭配详情、收藏列表。收藏状态如果各存一份一定会出现“收藏列表里是红心切回首页又变灰”的诡异问题。解决思路很简单所有页面共用一个 FavoriteCubit 实例谁都不能自己维护收藏状态副本。跨页面共享状态用 Provider 挂在 MaterialApp 上层BlocProviderFavoriteCubit( create: (_) FavoriteCubit( JsonFavoriteDataSource(storagePath: storagePath), ), child: const WardrobeApp(), )详情页、主页都通过context.readFavoriteCubit()拿到同一个实例。这样不管哪个页面执行了 toggle其他页面监听到新的 FavoriteState 之后会自动重建实现了真正的单一数据源。有人会问 Navigator 切换页面后旧页面的状态会不会丢失。Flutter 的默认行为是压栈新路由之后旧 Route 的 State 依然保留只是不可见所以旧页面自己持有的普通 State 字段不会丢。但如果你把收藏状态放进了某个页面的局部 State那依然会出现数据过期因为这跟路由没关系是状态位置放错了。跨页面共享状态一定要提升到全局而不是指望路由不销毁。5.3 收藏列表的下拉刷新与图片缓存收藏列表的体验有四件事要做加载动画、下拉刷新、图片缓存、空态处理。收藏功能刚做完第一版时列表图片每次滑动都在闪后来才发现是缓存目录配置问题。在 OpenHarmony 上有些图片缓存插件默认目录拿不到要么手动指定要么改用 flutter_cache_manager 并提供能用的路径。下拉刷新倒是很标准用RefreshIndicator包住 ListViewonRefresh 里重新拉取一次搭配数据再重新加载收藏集合RefreshIndicator( onRefresh: () async { final outfits await repository.fetchAll(); emit(state.copyWith(allOutfits: outfits)); }, child: ListView(...), )这里要提一句OpenHarmony 上的下拉刷新阻尼和 Android 有些差异建议把位移阈值设低一点避免刷新指示器不触发。实测displacement: 40比较合适。5.4 收藏失败回滚与重复点击防抖最后是交互上的一个细节重复点击收藏按钮。用户快速点两下如果不做防抖会出现两个并发请求最后一个结果覆盖前一个收藏状态变得不可预期。我在 toggle 入口加了一个简单的冷却标记bool _toastLock false; Futurevoid toggleFavorite(String outfitId) async { if (_toastLock) return; _toastLock true; try { await context.readFavoriteCubit().toggleFavorite(outfitId); } finally { await Future.delayed(const Duration(milliseconds: 300)); _toastLock false; } }300 毫秒的冷却能过滤绝大多数误触。如果需要更优雅的方案可以改成 Last-In-Wins 的请求编号模式但衣橱管家的场景没必要防抖足够。6. 从调试到发布的排坑实录6.1 Impeller 渲染引擎在 OpenHarmony 上的兼容问题Flutter 新版本开始引入 Impeller 渲染引擎它默认在 iOS 上启用Android 和 OpenHarmony 上可以手动开启。我在一次功能测试中发现收藏列表的模糊背景失效图片缩放时边缘发虚最后定位到是 Impeller 对某些着色器支持不完整。如果你也遇到类似现象先别急着怀疑布局代码写错了。可以执行flutter run --no-enable-impeller看问题是否消失。如果消失就是渲染引擎兼容问题。在 OpenHarmony 上我建议现阶段保持使用旧的 Skia 渲染路径等官方把 OHOS 的 Impeller 适配做完整了再切换。切换方式可以在运行命令里加参数也可以在原生侧的 Engine 初始化参数里关闭。6.2 Gradle 插件与 HAP 打包告警构建过程中会看到这样一条告警You are applying Flutters main Gradle plugin imperatively using the apply script这通常是因为项目里的 Gradle 插件应用方式太老。新版 Flutter 模板推荐使用plugins { id com.flutter.gradle.application }这种声明式方式而不是在 build.gradle 里写apply from: $flutterRoot/packages/flutter_tools/gradle/app_plugin.gradle。按提示把插件应用方式改过来告警就没有了打包时也不容易出奇奇怪怪的问题。还有一次打 HAP 时遇到java.lang.AssertionError: Could not close开头的崩溃排查了一圈发现是构建缓存损坏。清理build/目录和ohos/.cxx缓存重新执行flutter clean再构建就通过了。这类文件句柄没有关闭的问题在 CI 并发构建时出现频率更高尽量单任务跑 HAP 打包。6.3 EventChannel 消息风暴与生命周期泄漏EventChannel 用起来容易忽略生命周期。收藏事件虽然频率不高但桌面卡片如果做了批量操作短时间内会推送很多事件。我在调试时发现详情页被销毁之后receiveBroadcastStream的订阅没有取消导致页面已经不存在了还会去触发setState直接抛异常。正确做法是把订阅绑定到页面的生命周期StreamSubscription? _sub; void initState() { super.initState(); _sub _favoriteEventChannel.receiveBroadcastStream().listen(...); } void dispose() { _sub?.cancel(); super.dispose(); }如果订阅需要跨多个页面共享就把它放到全局服务里不要放在某个页面内部。另一个建议是在事件监听回调里先判断 App 是否在前台避免后台状态下大量刷新 UI。6.4 常见问题速查表现象可能原因处理方式HAP 安装失败提示签名错误调试证书未配置DevEco Studio 开启自动签名检查 build-profile.json5收藏列表图片滑动闪烁缓存目录不可写或路径错误用 MethodChannel 获取沙箱缓存目录手动传给图片缓存库桌面卡片收藏事件 Flutter 收不到EventChannel 订阅时机不对或未取消在 Application 启动时注册订阅跟随 App 生命周期管理收藏状态重启后丢失持久化异步写失败被忽略检查 JSON 写入异常日志落盘失败时回滚内存状态构建报 could not close / AssertionError编译缓存损坏清理 build 目录和 .cxx 缓存重新 flutter cleanMethodChannel 频繁调用导致页面卡顿每次收藏都有多次原生调用合并操作收藏状态变更后只调一次原生偏好接口编译时提示 Flutter SDK not fully supported项目模板与 SDK 版本不一致锁定 SDK 版本统一团队开发环境忽略提示但关注后续告警实测下来这个速查表里出现频率最高的还是持久化路径问题和 EventChannel 生命周期泄漏。收藏功能本身不难难的是跨端细节建议大家在设计阶段就把 8 预留好不要等联调时再救火。我把这个项目里最麻烦的一段代码改到第四版才算稳定。前期总想着把所有收藏状态同步逻辑塞进一个文件里后来拆成 Repository、Cubit、Channel 监听三个部分逻辑立刻清爽了。如果你也在做 Flutter OpenHarmony 的跨端功能我的建议很直接先把平台通道画清楚再写业务代码。哪些用 MethodChannel哪些用 EventChannel哪些使用 PlatformView提前定好后面能少走很多弯路。收藏搭配这个功能后续还能扩展比如把收藏数据同步到 OpenHarmony 的分布式数据库实现手机和平板的收藏状态互通那时候 Repository 的抽象层就是为它留的口子。
返回列表