ARTICLE DETAIL

资讯详情

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

Flutter for OpenHarmony实现收藏功能:状态同步与跨端通信实战

Flutter for OpenHarmony实现收藏功能:状态同步与跨端通信实战 做微动漫App的时候我给自己挖过一个坑首页、详情页、我的页面都要用到收藏一开始图省事在每个页面里各自setState结果收藏状态到处不同步。详情页点完收藏回到首页角标没变再点进去发现按钮状态还是旧的。后来换成Flutter for OpenHarmony问题又多了一层——鸿蒙生态的插件支持不像Android那么齐全很多常用的Flutter包拿过来直接跑轻则编译不过重则运行期挂掉。这篇文章记录的是我在一个微动漫类App里落地收藏功能的完整过程包括业务边界怎么拆、存储和状态管理怎么选、Dart和ArkTS之间怎么通信以及打包上OpenHarmony设备时踩过的坑。如果你是正在做OpenHarmony适配的Flutter开发者或者刚接触跨端开发想找一个能把生态链路走通的小项目收藏功能刚好合适。它不是那种“几行代码搞定”的玩具但也没有复杂到让人望而却步。1. 先理清收藏功能的业务边界1.1 微动漫App里的收藏到底承担了什么微动漫App的内容形态很典型短动画、动态漫画、条漫更新节奏快单集时长短。用户的收藏行为跟长视频平台不一样更多是在碎片时间里“先把东西收起来晚点再看”。所以收藏功能在微动漫App里至少承担三个角色追更单、备看清单、情感标记。需求评审阶段产品通常会提一堆“想要”的功能比如收藏夹分组、标签管理、批量导出、分享收藏单。我的建议是MVP阶段先砍到最小闭环能收藏、能取消收藏、能看到已收藏列表、有明确的空状态。先把这几个操作跑通再去考虑分组和云同步。原因很简单收藏功能看起来简单实际牵扯本地存储、全局状态、页面通信、原生桥接每多一个需求复杂度都会翻倍。还有一个容易被忽略的非功能约束——离线可用。微动漫用户在地铁、电梯里看内容网络很差是常态。用户在弱网环境下点收藏如果还要等服务器响应才算成功体验基本就废了。所以无论后续要不要做云同步收藏动作本身必须先落本地然后再考虑上行同步。这个判断会影响后面的技术选型。1.2 技术选型本地存储和状态管理怎么配本地存储我第一版用了shared_preferences家族的OHOS适配版本。很多人一听就说“这玩意不是只能存字符串吗”但对收藏这个场景来说数据量小、结构简单、读写频率低Preferences完全够用。我踩过一个过度设计的坑项目刚起步就把sqflite、drift、objectbox全列进方案结果功能没写多少数据库迁移和样板代码先堆了几百行。收藏这种轻量功能先把KV存储跑通等收藏量真上万了再换数据库也不迟。状态管理我选了flutter_bloc里的Cubit。选择理由很务实收藏状态需要被App里多个页面共享而且操作动作只有toggle收藏、加载列表、清空列表这几类Cubit写起来比完整Bloc少很多模板。我也见过用Provider做同样事的没问题但Cubit对异步操作和流式通知的支持更顺滑后面接EventChannel推送收藏变更时还能复用同一套Stream思路。组件通信层面既然选了全局状态管理页面之间“点完收藏另一个页面不知道”的问题就已经解决了一半。另一半需要靠平台通道如果后续要做云端收藏同步Dart侧要告诉ArkTS侧去发起网络请求ArkTS侧收到服务端推送后又要通知Dart侧刷新收藏列表这就分别对应MethodChannel和EventChannel。通信方案不是越多越好而是每个场景有最合适的那一个。提示技术选型阶段最重要的不是“哪个框架最强”而是“这个功能需要的最小可靠方案是什么”。收藏功能用不到的重量级状态管理、数据库组件一律砍掉。2. 搭建Flutter for OpenHarmony工程别在第一步就卡住2.1 环境搭建和ohos平台目录想在OpenHarmony上跑Flutter标准官方版Flutter SDK并不能直接支持你需要使用带ohos平台支持的Flutter SDK分支一般由OpenHarmony SIG或社区维护。拉下来之后把Flutter的bin目录配到PATH里和正常Flutter开发一样。创建工程时记得把ohos平台加进去flutter create --platforms ohos,android .如果你的项目已经存在也可以在工程根目录手动补一个ohos目录然后用DevEco Studio打开这个目录IDE会自动识别hvigor工程配置。一个很容易踩的坑是很多人习惯了在Android Studio里新建Flutter项目到了OpenHarmony侧也用AS强行管理ohos目录结果配置经常对不上。我的建议是分工明确Dart侧代码继续用Android Studio或VSCode写ohos原生工程统一交给DevEco Studio管理。两边IDE共用同一个工程目录不会冲突但别试图用一套IDE把所有事情都管了。环境变量这里我吃过亏。除了Flutter SDK路径还要确保ohos-sdk所在路径被正确指引到hvigor配置里。以下是我个人项目里用到的环境参考组件项目里的版本/路径备注Flutter SDK社区OHOS适配分支锁定3.22.x不要用滚动版本否则插件适配容易崩OpenHarmony SDKAPI 12及以上一些新组件API只有高版本才有DevEco Studio5.x用于管理ohos目录和签名hvigor随DevEco Studio自动配置打包hap的构建工具2.2 OpenHarmony插件适配的核心思路Flutter on OpenHarmony的系统架构简单说就是Dart Framework通过Flutter引擎调用OpenHarmony的图形、输入、平台服务。插件的作用是让Dart代码能调到ArkTS侧的原生能力。理解这个之后判断一个Flutter插件能不能在ohos上用就很简单看它有没有ohos原生目录和对应的ArkTS实现没有的话就得自己补。收藏功能通常要依赖shared_preferences、path_provider这类基础插件。如果你发现某个插件没有ohos支持不要立刻放弃整个方案先看看它的实现逻辑。比如shared_preferencesDart侧API就是一套Android用SharedPreferencesiOS用NSUserDefaultsOpenHarmony侧可以用系统Preferences封装一个同名实现。社区里也有一些大佬维护了ohos适配版本遇到问题还可以自己写插件。之前我们给一个登录认证插件做OHOS适配流程可以抽象成三步把原生能力封装成ArkTS模块在Flutter侧声明MethodChannel处理Dart和ArkTS之间的类型映射。收藏插件要做的事也是一模一样的套路。很多人一听到HDI就紧张其实HDI是硬件接口层大部分App业务根本碰不到只有做系统能力适配或者接底层音视频编解码时才会涉及。微动漫App如果后面要接自家播放器SDK通常是用PlatformView来做而不是直接操作HDI。再说一个跟渲染相关的坑。新版Flutter引擎默认启用Impeller渲染器但OpenHarmony适配分支对Impeller的支持可能滞后。如果你在真机上发现画面出现奇怪的色块、花屏或者某些动画不刷新可以先试着关闭Impeller再跑一次flutter run --no-enable-impeller3. 收藏功能核心代码从持久化到页面交互3.1 用FavoritesRepository封装存储逻辑我不建议让UI直接操作SharedPreferences。哪怕功能再简单也要加一层Repository。原因很简单今天你用Preferences明天想换sqlite后天要接云同步如果UI层直接散落着几十处prefs读写改起来会想哭。先定义一个收藏条目模型。注意一定要存完整展示信息只存ID的坑我踩过——收藏列表页需要标题和封面没有这些信息就只能去服务端逐个请求离线状态下整个列表直接废掉class FavoriteItem { final String id; final String title; final String coverUrl; final int createdAt; FavoriteItem({ required this.id, required this.title, this.coverUrl , required this.createdAt, }); MapString, dynamic toJson() { id: id, title: title, coverUrl: coverUrl, createdAt: createdAt, }; factory FavoriteItem.fromJson(MapString, dynamic json) FavoriteItem( id: json[id] as String, title: json[title] as String, coverUrl: json[coverUrl] as String? ?? , createdAt: json[createdAt] as int, ); }然后写Repositoryclass FavoritesRepository { static const _prefsKey favorite_items; FutureListFavoriteItem loadAll() async { final prefs await SharedPreferences.getInstance(); final raw prefs.getString(_prefsKey); if (raw null || raw.isEmpty) return []; final list jsonDecode(raw) as Listdynamic; return list .map((e) FavoriteItem.fromJson(e as MapString, dynamic)) .toList(); } Futurevoid saveAll(ListFavoriteItem items) async { final prefs await SharedPreferences.getInstance(); final raw jsonEncode(items.map((e) e.toJson()).toList()); await prefs.setString(_prefsKey, raw); } }这个实现有几个刻意的选择。第一整个收藏列表序列化成一条JSON存储读取时一次性拿到全部数据简单粗暴但够用。第二每次保存是全量覆盖收藏数量少的时候没有性能问题。第三createdAt字段是排序依据新收藏的条目要排在最前面没有这个时间戳后面做云同步合并时也会非常被动。3.2 用Cubit管理收藏状态页面永不丢状态收藏状态是一个典型的全局共享状态详情页需要判断“当前作品是否已收藏”首页需要展示收藏角标收藏列表页需要展示全部已收藏内容。我用Cubit来管理这一份状态class FavoriteCubit extends CubitListFavoriteItem { FavoriteCubit(this._repository) : super(const []); final FavoritesRepository _repository; Futurevoid load() async { final items await _repository.loadAll(); emit(items); } bool isFavorite(String id) state.any((e) e.id id); Futurevoid toggle(FavoriteItem item) async { final exists isFavorite(item.id); final next ListFavoriteItem.from(state); if (exists) { next.removeWhere((e) e.id item.id); } else { next.insert(0, item); } emit(next); await _repository.saveAll(next); } }两个细节必须说明。第一Cubit的state要当作不可变数据来用我每次都用List.from(state)先复制再修改绝不在原列表上add/remove后直接emit。如果直接改原state再emitFlutter的相等性判断会认为状态没有变化页面就不会刷新。第二toggle里是“先emit、后保存”这样用户点收藏按钮的瞬间UI就有反馈写Preferences的异步过程在后台完成。Preferences本地写入基本不会失败所以这个顺序是安全的。经常有人问Flutter的Navigator切换页面之后会不会丢失状态我的回答是状态放Cubit里就不会丢。Navigator每次push都会创建新的页面Widget页面里的局部setState状态当然会被重置但如果状态在Cubit里页面重建只是重新订阅同一个数据源取到的还是同一份内存数据。收藏功能里最忌讳的就是把收藏状态复制到页面State里那样详情页改完、收藏页不知道收藏页删完、详情页还显示已收藏。3.3 详情页按钮和收藏列表的UI落地详情页的收藏按钮建议用BlocBuilder或者context.watch来监听Cubit而不是自己维护一个bool。我见过有人用IconButton加onPressed里setState然后在initState里从Cubit读一次初始值后续状态变化完全感知不到这是典型错误。final favoriteCubit context.readFavoriteCubit(); FavoriteButton( isFavorite: context.watchFavoriteCubit().isFavorite(comicId), onPressed: () { final item FavoriteItem( id: comicId, title: comicTitle, coverUrl: comicCoverUrl, createdAt: DateTime.now().millisecondsSinceEpoch, ); favoriteCubit.toggle(item); }, );按钮的点击区域要控制好收藏按钮在卡片上会被多个事件竞争我建议至少保持48x48的逻辑像素热区同时给icon的过渡动画一点反馈不然用户不知道到底点没点上。收藏列表页则直接监听Cubit的完整状态BlocBuilderFavoriteCubit, ListFavoriteItem( builder: (context, items) { if (items.isEmpty) { return const EmptyFavorites(); } return ListView.builder( itemCount: items.length, itemBuilder: (context, index) { final item items[index]; return FavoriteCard( title: item.title, coverUrl: item.coverUrl, onRemove: () context.readFavoriteCubit().toggle(item), ); }, ); }, )列表页的取消收藏复用了同一个toggle方法这样详情页、列表页、首页角标拿到的一定是同一份数据。不过要注意性能如果收藏列表有几千条每次toggle都会触发整个列表重建多少会有点卡。MVP阶段无所谓数据量上来之后让Cubit发出带diff的事件比如FavoriteAdded和FavoriteRemovedUI局部更新就好。4. 跨页面与原生能力MethodChannel和EventChannel配合实战4.1 Dart和ArkTS之间如何可靠通信微动漫App的收藏功能一旦接入账号体系就会遇到两种通信场景。第一种是Dart主动通知ArkTS发起云同步比如用户点击“同步收藏”按钮登录token存在原生侧网络请求需要由原生侧发出。这个用MethodChannel最合适Dart调用、原生返回结果语义上是“请求-应答”。第二种是服务端推送收藏变更比如某部作品下架了后台通知到设备原生侧需要主动告诉Dart侧“刷新一下收藏列表”。这个用EventChannel最合适原生侧单向推数据Dart侧用Stream接收。Dart侧先定义好通道class FavoritesSyncChannel { static const _channel MethodChannel(com.example.microanime/favorites); static const _eventChannel EventChannel(com.example.microanime/favorites_events); Futurebool syncToCloud(ListFavoriteItem items) async { final result await _channel.invokeMethodbool(syncFavorites, { items: items.map((e) e.toJson()).toList(), }); return result ?? false; } StreamMapdynamic, dynamic get favoritesEvents _eventChannel.receiveBroadcastStream(); }MethodChannel和EventChannel要特别注意生命周期。EventChannel的Stream必须在App启动阶段注册一次最好放在全局单例或者main()里初始化不要放在页面build里。理由是用户一旦离开当前页面Stream被销毁原生侧再推送收藏变更就再也收不到了。这个bug特别隐蔽因为开发调试时你通常就停在一个页面上问题不会暴露上了真机一锁屏再解锁就丢事件。Flutter组件通信的方式很多InheritedWidget、Provider、Stream、EventChannel各有用武之地。我的切分标准是UI局部共享状态用InheritedWidget或Provider全局业务状态用Cubit这类状态管理库跨原生边界用MethodChannel和EventChannel。把这三类边界分清楚代码结构就不会糊成一团。4.2 自己写一个OpenHarmony收藏插件要过哪些坎如果社区已有的shared_preferences适配满足不了“收藏同步到云端”的需求我们就得自己补一个OHOS插件。以ArkTS侧为例核心结构大致是这样import { MethodCall, MethodChannel, EventChannel } from ohos/flutter_ohos_plugin; export class FavoritesPlugin { private channel: MethodChannel new MethodChannel(com.example.microanime/favorites); private eventChannel: EventChannel new EventChannel(com.example.microanime/favorites_events); constructor() { this.channel.setMethodCallHandler((call: MethodCall) { if (call.method syncFavorites) { const args call.arguments as Recordstring, Object; return this.syncCloud(args[items] as string); } return Promise.reject(new Error(unknown method: call.method)); }); } private syncCloud(itemsJson: string): Promiseboolean { // 调用鸿蒙侧的网络框架把收藏列表上传到服务器 return Promise.resolve(true); } pushRemoteRemove(ids: string[]): void { this.eventChannel.sink?.success({ type: remoteRemove, ids: ids, }); } }这份代码是结构示意具体API名称会随Flutter SDK for OpenHarmony的版本变化但思路是通用的。写插件最容易踩的坑是类型映射Dart侧的Map、List、String、数字等类型在ArkTS侧都能对应但自定义对象不行建议一律序列化成JSON字符串再传。我们曾经把一个FavoriteItem对象直接塞进通道结果原生侧拿到的是一个不可读的结构排查了大半天改成JSON字符串后一次通过。还有一个非常容易被忽略的点权限声明。微动漫App要同步收藏列表必须在OHOS工程的module.json5里申请网络权限{ module: { requestPermissions: [ { name: ohos.permission.INTERNET } ] } }没有这个权限时真机上网络请求会静默失败Dart侧还以为是同步成功返回了false。在开发调试阶段权限够用但等你要发布签名的hap包时权限配置对不对会直接影响上架审核。4.3 PlatformView嵌入原生页面的边界处理微动漫App里面如果要用原生播放器、原生广告组件就会涉及PlatformView。很多初次接触混编的同学会把“嵌入原生播放器”和“必须用PlatformView”绑定其实要分情况。如果只是在详情页里嵌一个播放窗口PlatformView能用但如果你发现PlatformView在部分OpenHarmony版本上跟Flutter列表同屏滚动时出现图层抖动、白屏就得考虑换方案。我自己遇到过的解法有三种把播放器切换成Texture方式渲染让系统把视频帧作为纹理交给Flutter绘制或者干脆把整屏交给原生页面通过Navigator做原生和Flutter的页面级混编还有一种就是降级为“启动原生播放器Activity/Ability”彻底脱离Flutter渲染链路。收藏列表页本身用不到PlatformView但很多微动漫App会在收藏列表页里带原生Banner广告这时候你就得做取舍。提示PlatformView的图层问题不是写代码能完全规避的它跟OpenHarmony的窗口合成机制有关。遇到白屏先别急着改业务代码先确认是不是在低端真机上复现再考虑纹理方案。5. 打包、真机运行和常见报错排查5.1 OpenHarmony打包hap时被反复摩擦的3个问题打包阶段我踩过的坑比写业务代码还多列三个最有代表性的都值得记进项目Wiki。第一个release构建时抛出java.lang.AssertionError: java.lang.Exception: could not close input stream之类的错误。这个报错看起来像代码问题实际多数是构建缓存或资源I/O冲突。我的处理顺序是先执行flutter clean删掉build目录重新构建不行就把gradle缓存目录里的Flutter相关缓存清掉还不行就检查工程路径里有没有中文目录或者特殊符号。我项目里出现过一次原因是Windows的杀毒软件锁了临时文件把构建产物目录加白名单就好了。第二个构建脚本里出现“you are applying flutters main gradle plugin imperatively using the apply script”的提示。这个问题的本质是Flutter 3.x以后推荐用pluginManagement方式声明插件而旧工程模板还在用apply plugin: flutter这种命令式写法。处理方法是把settings.gradle里的插件声明方式改过来同时检查ohos模块的依赖配置不要混用两套插件加载逻辑。第三个插件版本跟Flutter SDK不匹配。OpenHarmony生态的Flutter插件往往跟着特定SDK版本走如果你把Flutter SDK升级了一个小版本很多适配插件会直接编译失败。我的办法是给项目锁死Flutter SDK版本不追新插件一律用固定版本号不用latest。记录一个实际打包遇问题的排查表报错/异常常见原因处理建议AssertionError: could not close input stream构建缓存损坏或资源文件被占用flutter clean清理gradle缓存检查路径main gradle plugin applied imperatively插件声明方式混用改用pluginManagement统一声明插件编译失败Flutter SDK升级锁定SDK和插件版本不要盲目升级真机无网络module.json5缺少权限添加ohos.permission.INTERNET5.2 真机运行时的状态刷新与性能细节微动漫App的收藏列表页一般会加下拉刷新用Flutter自带的RefreshIndicator就行。但有一个细节要提醒RefreshIndicator的回调必须是一个Future刷新过程中这个Future没返回loading动画就一直转。而且刷新逻辑要基于当前Cubit状态去和服务端做合并不能简单地“重新加载本地数据”或者“清空后拉远端”否则用户刚收藏的内容会被远端旧数据覆盖掉。代码结构大致是Futurevoid _refreshFavorites(BuildContext context) async { final cubit context.readFavoriteCubit(); final currentItems cubit.state; final remoteItems await favoritesApi.fetchRemote(); final merged _mergeLocalAndRemote(currentItems, remoteItems); await cubit.replace(merged); }收藏功能里还有一个容易忽略的时序坑。有些同学习惯用SharedPreferences.getInstance().then((prefs) setState())然后纠结Future的then回调到底是不是放在微任务队列里。其实这不是关键关键是不要在then回调里依赖一个已经被销毁的页面上下文更不要用then来触发UI状态更新。收藏状态统一走Cubit的emitDart侧的异步顺序自然会被状态管理框架处理好UI只订阅状态流就行。真机调试的时候还有一个判断陷阱OHOS上的flutter run首次启动会比较慢Dart VM要attach到设备紧接着还要编译加载原生hap容易让人误以为“性能很差”。建议用release包再测性能别拿debug包的数据去跟产品汇报。6. 收藏功能还能怎么扩展6.1 从单机收藏到云同步单机收藏跑通之后最值得做的扩展是云同步。数据结构要先升级给FavoriteItem加一个updatedAt字段同步策略用“本地上传远端合并”不要简单覆盖。具体流程是用户登录后先把本地收藏列表全量上传服务端返回最新的收藏全集客户端再用updatedAt做合并冲突时以后修改时间为准。更稳的方案是引入离线变更队列。用户每次收藏和取消收藏除了更新主收藏列表还要在本地写一条变更记录“add/remove id 时间戳”。网络恢复后客户端把变更队列按顺序发给服务端服务端逐条应用。这个设计能让断网状态下的收藏操作完全不丢等微动漫App后续要加“多端收藏同步”时这个队列就是现成的底座。6.2 从列表到智能推荐和收藏夹分组另一个扩展方向是收藏夹管理。微动漫内容更新快用户收藏多了之后按“更新中”“已完结”“想看”分组非常实用。这个分组逻辑建议放在Cubit的派生数据里不要在UI层每次build时重复计算。比如可以暴露一个groupedFavoritesgetter根据每个条目的状态字段归到不同的List页面直接订阅分组结果就好。收藏数据本身还可以反哺推荐系统。当用户收藏了几部作品App首页的“猜你喜欢”就能基于收藏标签做内容召回。这一步在技术上只是把收藏列表从本地读出来上传到推荐服务但前提是之前每一步的数据模型都留有扩展余地。我当时做收藏功能时把FavoriteItem设计成可以加tags、status等字段的JSON结构后来接推荐和分组几乎没改存储结构。我自己做一遍这个功能最大的体会是收藏功能是典型的“业务简单、工程不简单”的需求。把本地存储抽象成Repository、把收藏状态收敛到全局Cubit、把原生通道做成独立插件这三件事认真做好后面无论换数据库、接入云同步还是适配新的OpenHarmony版本收藏功能都不会伤筋动骨。最后再分享一个小技巧在详情页和列表页同时操作同一个收藏项时一定要以ID为主键做幂等按钮连点也不怕这样用户怎么快速折腾都不会出现数据错乱。
返回列表