☰
Flutter与鸿蒙双端视频播放器实战:推荐流与桥接层避坑指南
2026/10/3 18:04:38 网站建设 项目流程

做影悦播放器这段时间,我最大的一个体会是:每次你以为自己已经在写业务逻辑了,其实大部分时间都在填 Flutter 与系统原生的桥接坑。这个项目要从零搭一个推荐视频流首页,支持上滑下滑切换视频、预加载下一集、进度事件实时刷新,还要把整套东西跑在 HarmonyOS 6.0 上。最开始我图省事,想直接用现成插件拼一版,结果在事件通道、渲染纹理、页面状态这三块都吃了不少苦头。

做完以后再回头看,这套东西其实可以拆成几个非常清晰的问题:推荐流怎么做、播放器内核怎么选、Flutter 怎么和鸿蒙的原生能力通信、页面切换怎么保住播放状态。如果你也在做 Flutter 视频类应用,或者正在把一套 Flutter 代码适配到鸿蒙生态,这篇就当是我把走通的路和踩过的坑一起复盘一遍。

1. 先看整体设计:为什么要做 Flutter + HarmonyOS 双端播放器

1.1 需求拆解:推荐视频流才是重头戏

影悦这个项目的核心功能并不复杂:首页是一个推荐视频流,用户上滑进入下一个视频,自动开始播放,再配合点赞、收藏、进度条拖拽这类基础交互。听起来像短视频产品,实际做起来会发现“推荐视频”这四个字不只是UI层面的列表,它背后挂着三条硬指标。

第一条是切换效率。用户手指滑一下,下一集必须在几百毫秒内开始出画面,否则体验就是“卡的”。第二条是播放状态连续。我在做的时候最怕的一件事就是用户从推荐流点进详情页,再返回来,结果推荐流里那个正在播放的视频回到“从零开始”的加载状态。第三条是画质与性能平衡。推荐流里视频规格参差不齐,有的1080P,有的720P,播放器内核一旦处理不好,低端机上就是发热掉帧。

把这些需求落下来会发现,纯 Flutter 做不了播放器内核。视频解码、硬解加速、缓冲控制这些能力必须交给系统原生的媒体框架,Flutter 层负责的应该是 UI、手势、状态、生命周期。这就是整个项目的技术基调:Flutter 做界面和业务编排,HarmonyOS 原生提供媒体播放能力,中间通过桥接层把它们粘在一起。

1.2 架构分层:把播放器能力抽象成接口

项目一开始,我先没有急着写页面,而是把播放器能力收敛成一个接口。这个决定在后面适配鸿蒙时省了非常多事。简单说,我把播放器抽象成了PlayerAgent,暴露给 Flutter 上层的方法只有几个:prepare、play、pause、seekTo、release、getTextureId。

UI 层永远不直接 new 一个平台播放器,也不关心底层是 Android 的 MediaPlayer、鸿蒙的 AVPlayer 还是别的内核。UI 层只认 PlayerAgent 这个接口,拿到 textureId 就渲染纹理,收到事件回调就刷新进度条。这样一来,推荐视频流页面的代码就不会被平台细节污染。

配套的还有一个状态机概念。播放器在生命周期里会经历 idle、loading、ready、playing、paused、error 这几个状态,每个状态都对应一个明确的事。我在做影悦时最容易犯的错是“直接在回调里写业务逻辑”,比如在 playing 状态回调里去刷新列表某个 item,这种写法一旦页面重建,回调就全乱了。后来改成统一的状态流,所有页面组件只订阅状态流的快照,问题就少了很多。

这一层的设计原则说白了就一句话:平台能力可以不同,但对外表现必须一致。后面鸿蒙适配能把大部分代码直接复用,靠的就是这里收口收得干净。

2. 推荐视频流实现:PageView 与自动播放的细节

2.1 列表骨架与预加载策略

推荐流的 UI 骨架我选了 PageView.builder,每个 item 占据一整屏。这里有一个关键配置:cacheExtent。默认情况下列表只会保留当前页和相邻页,如果用户手速很快,滑动到第三页时第二页已经销毁,播放器跟着被回收,画面就会闪黑。我的做法是把 cacheExtent 设成两屏,同时搭配预加载机制。

预加载策略是我在推荐流里最满意的一块。当页面停在第 N 个视频后,我会提前调用第 N+1 个视频的 prepare,但不会调用 play。相当于先把播放器预热到 ready 状态,用户一滑过去,只需要从 ready 切到 playing,延迟就非常低。这里要注意一个坑:不能把所有邻居都提前加载,否则内存会爆。影悦的做法是只预热后一集,前一集保持在“已暂停且持有进度”的状态,再往前的基本直接释放。

PageView 的滚动监听也要处理。我用 controller.page 的变化来判断当前页,但实际开发中快速滑动会连续触发多次监听,每次都去暂停旧播放器会浪费性能。我在代码里加了一层节流:只有当页码变化超过一定值时,才真正执行播放器切换逻辑。这个规则要结合 Flutter 的 Future 微任务队列理解,后面在 2.3 里细说。

2.2 下拉刷新与手势冲突

推荐流下拉刷新本来是很常规的需求,但在视频播放器场景里会撞上一个物理冲突:下拉手势和 PageView 的上下滑动手势由同一个方向识别。如果直接把 RefreshIndicator 包在 PageView 外面,用户一上滑,刷新组件会误以为要触发刷新,结果就是视频流和刷新动画打架。

我的解决方案有两层。第一层,给 PageView 设置一个非可滚动的 ScrollPhysics,让 PageView 自己不管下拉手势;第二层,用 NotificationListener 监听 OverscrollNotification,只有当列表滑到顶部并且用户继续下拉时,才通知 RefreshIndicator 进入刷新状态。这样一来,上滑切换视频的手势完全由页面内部处理,下拉刷新只出现在边界场景。

还有一个细节是刷新期间要不要暂停播放。我一开始没有暂停,结果一边播放一边刷新,列表布局重建,播放器纹理短暂黑屏。后来在刷新回调里强制把当前播放器切到 paused,刷新完成后再根据用户是否停在当前页决定要不要恢复。这个判断不能省,否则用户明明在看第三集,刷新后却自动回到第一集,体验非常错乱。

2.3 自动播放触发与 Dart 微任务队列

自动播放的触发逻辑看起来只是“等 PageView 停稳后调用 play”,但这里藏着 Dart 异步模型的功课。Flutter 的 Future.then 回调是放进微任务队列的,微任务队列里的任务会在事件循环每次处理完一个事件后被清空。也就是说,如果你在滚动监听完立刻 Future.microtask(() => play()),这段代码不一定等页面真正停下来才执行,它可能在当前帧结束时就被执行了。

影悦的做法是双重判断:第一,在 page changed 事件里记录最新的目标索引;第二,用一个 DelayTimer 兜底,比如页面停稳 300 毫秒后才真正触发播放。300 毫秒这个值我试过几个档位,太短会导致切换过程中误触,太长又会让预加载优势浪费掉。实测下来 250 到 350 毫秒是一个比较稳的区间。

这个场景里我不建议把所有逻辑都塞进 Future.then,因为推荐流里同时存在多个待处理任务,一旦微任务队列堆积,UI 线程就会出现明显的卡顿。更合理的方式是:把“判定当前页是否稳定”作为一个独立逻辑,稳定之后再向播放器派发指令,播放指令本身走 MethodChannel,不走微任务队列。

3. 播放器渲染底层:Impeller 与 PlatformView 的那些坑

3.1 为什么必须用原生播放器内核

有一些刚接触 Flutter 的开发者会问:能不能用纯 Dart 写解码器?答案很现实:不能。视频解码涉及硬件加速、内存拷贝、各种容器格式解析,纯 Dart 实现既不可能达到性能要求,也几乎没有维护价值。影悦上线目标包含低端机,播放器内核必须走系统原生的 AVPlayer。

这就意味着 Flutter 层必须依赖 PlatformView 或者纹理合成的方式去承载原生播放器画面。但原生视图和 Flutter 渲染视图是两个完全不同的合成体系,如果处理不好,会出现视频画面盖住 Flutter 弹窗、进度条无法悬浮、切换页面时黑屏等问题。所以选渲染方案这件事,比选播放器内核还要提前。

3.2 Impeller 渲染管线的收益和坑

HarmonyOS 6.0 上跑 Flutter,现在的 Flutter 引擎默认走的是 Impeller 渲染管线。Impeller 的好处是预编译着色器,能避免原来 Skia 管线在首次播放视频时突然卡一下的问题,整体画面合成的稳定性提升非常明显。我在影悦里最直观的感受是:长列表滑动的掉帧明显减少,推荐流里视频纹理和 UI 控件叠加时也没有以前那种“参数突然失控”的感觉。

但 Impeller 不是银弹。在部分低端设备上,PlatformView 会和 Flutter 自绘内容发生合成冲突,表现就是视频画面可以播放但不显示,或者显示出来一个纯黑区域。我排查下来的经验是:遇到这类问题,优先切换到纹理合成方案,也就是让原生播放器不走原生 View,而是把视频帧写入 external texture,再由 Flutter 纹理面板渲染。

Texture 方案还有一个额外好处:它天然支持圆角、遮罩、浮层这些 Flutter 控件的剪裁效果,而 PlatformView 在这些场景下实现对开发者来说几乎是灾难。说实话,如果推荐流是纯沉浸式滚动,我会建议直接用 Texture 而非嵌入 PlatformView。只有遇到需要输入框、焦点管理等特殊交互时,才需要认真考虑 PlatformView。

3.3 内存与播放器实例管理

推荐流如果每个 item 都保留一个播放器实例,内存迟早爆掉。影悦的规则是:当前页和后一页各保留一个播放器实例,前一页降级为“只保留最后一帧截图和播放进度”。切走后,我会把播放器释放,但进度值通过 EventChannel 同步回 Dart 层,方便用户滑回来时恢复进度。

释放时机要非常谨慎。因为 PageView 的 item 销毁不等于用户一定切走了,可能只是缓存回收。我在 onPageChanged 里记录当前索引,再在 item 的 dispose 回调里判断当前索引和 item 索引的距离,只有距离大于等于 2 才真正 release。这一步避免了正在播放时被系统回收的崩溃问题。

如果你在真机上看到“播放视频时内存突然涨了上百 MB”,大概率就是播放器实例没有及时释放。建议用 AS 自带的内存分析器拉一次 dump,看看每个播放器相关的内存对象是否随着滑动不断累积。这个排查方法对鸿蒙的真机调试同样适用。

4. Flutter 与鸿蒙侧通信:EventChannel / MethodChannel / PlatformView 实战

4.1 三个通道怎么分工

整个项目最核心的桥接层,就是 Flutter 和鸿蒙侧的通道设计。很多人在这一步乱掉,是因为不知道 MethodChannel、EventChannel、PlatformView 到底该分别干吗。我直接用一张表说明白:

通道类型使用场景数据方向影悦里的实际用途
MethodChannel一次性请求/响应双向发起播放、暂停、跳转、获取播放器信息
EventChannel持续单向事件流原生到 Dart播放进度、缓冲状态、错误信息推送
PlatformView原生视图嵌入 Flutter双向输入框、广告位等需要原生控件的场景
Texture视频帧纹理合成原生到 Dart播放器画面渲染,替代 PlatformView

这条分工原则很重要:不要试图用 MethodChannel 做高频推送。进度回调每秒几十次,如果每次都用 invokeMethod,Dart 侧和原生侧都要做序列化,性能完全扛不住。事件流就交给 EventChannel,它是长连接式的,只在首次订阅时穿一次通道名,之后的数据直接单向流过来。

4.2 从 Dart 发起播放到原生渲染:完整链路

影悦里一段视频从点击到出画面,完整链路是下面这个流程。

第一步,Dart 侧调用 MethodChannel 的 prepare 方法,传入视频地址和需要的参数。第二步,鸿蒙侧收到 prepare 后,创建 AVPlayer 实例,并让它绑定一个纹理输出源。第三步,原生侧把纹理 ID 返回给 Dart,Dart 侧拿到一个非零的 textureId,就把它交给 Textrue 组件渲染。第四步,原生侧播放器状态变化时,通过 EventChannel 把状态码、错误码、当前进度推给 Dart。第五步,Dart 侧订阅这些事件,刷新进度条、切换加载动画、弹出错误提示。

我在 Dart 侧封装了 PlayerManager,它的初始化代码大致长这样:

class PlayerManager { static final PlayerManager _instance = PlayerManager._(); factory PlayerManager() => _instance; final MethodChannel _control = const MethodChannel('yingyue/player'); final EventChannel _event = const EventChannel('yingyue/player/events'); StreamSubscription<dynamic>? _sub; Future<int> prepare(String url) async { final textures = await _control.invokeMethod('prepare', {'url': url}); final textureId = textures is int ? textures : 0; if (textureId <= 0) throw Exception('prepare failed'); _sub ??= _event.receiveBroadcastStream().listen(_onRawEvent); return textureId; } void _onRawEvent(dynamic event) { // 将事件转成状态流,UI 层只订阅这个状态流 } Future<void> play() => _control.invokeMethod('play'); Future<void> pause() => _control.invokeMethod('pause'); }

这段代码里最容易忽略的是 prepare 里对 textureId 的判断。很多黑屏问题不是播放器没创建,而是原生侧把 0 当成了有效 ID 返回,Dart 侧又没做校验,结果渲染出来永远是一个空纹理。我在影悦里加了一行判断后,黑屏问题直接少了一半。

4.3 原生能力类插件如何快速迁移到鸿蒙

热词里有人提到身份认证 SDK 适配鸿蒙流程,这其实是另一类典型的桥接需求。我接过类似的 SDK,它本身的业务能力在 Android 上有完整实现,切换到鸿蒙时不能重写 UI,只能把原生能力做一层统一抽象。

做法和我上面说的 PlayerAgent 是一样的。先在公共层定义一个 identity 服务接口,Dart 侧只依赖接口;Android 和鸿蒙各自实现同一套 Handler。Dart 侧不再感知底层是哪个平台。整个过程没有动一行 Flutter UI 层代码,只新增了一个鸿蒙的 plugin 实现。这个模式对视频播放器、推送、支付、分析 SDK 全都适用。

鸿蒙侧实现这类 Handler 时,要特别注意注册时机。插件必须和 Flutter Engine 绑定,确保通道注册发生在 Dart 侧 create 引擎之后。如果注册时机不对,Dart 侧调用 invokeMethod 会一直等不到响应,也没有任何超时提示,排查起来非常隐蔽。

4.4 EventChannel 的细节与性能陷阱

EventChannel 我必须要多说几句,因为这是影悦上线前踩坑最多的通道。第一个陷阱是回调线程。原生侧在音频信息回调线程里推事件,但 Dart 侧有时候需要在 UI 线程更新控件,跨线程直接改控件会异常。正确的做法是:只在主线程的 handler 里调用 eventSink,其他线程的数据统一通过 runOnUiThread 切回主线程再推。如果发现 UI 偶现崩溃,先查这一条。

第二个陷阱是日志风暴。如果你在 onProgress 回调里写日志,真机上每秒几十行日志会把系统线程拖得很卡。我的经验是:Debug 模式可以做节流打印,Release 模式必须完全关闭,即使关闭事件回调也不能停,因为它要用于进度恢复。

第三个陷阱是订阅生命周期。当推荐流页面销毁时,一定要取消 EventChannel 订阅并通知原生侧清理。否则会出现“页面没了但原生播放器还在后台跑音频”的问题。影悦的解法是在 dispose 里统一执行 unsubscribe 和 resolve 方法。

5. 页面跳转与全局状态:切 Tab、返回不丢播放

5.1 Navigator 状态丢失的根因

热词里有人问“Navigator 切换页面后会不会丢失状态”,我的回答是:不一定,但视频播放器这种大对象大概率会丢。原因在于,Navigator.push 会把当前页面压入栈底,如果栈低页面被系统裁剪掉,Flutter 会把它从 Widget 树里移除,所有逻辑组件 dispose。播放器实例如果只是页面里的局部变量,离开页面就等于销毁。

我在影悦早期版本里就吃过这个亏:用户从推荐流点进去看详情,返回时推荐流重新加载,原来看到第五集的位置回到了第一集。那时候播放器实例是写在 PageView 的 item 内部的,页面一重建,实例就没了。

5.2 用全局管理器持有播放器

要解决这个问题,必须先想明白一件事:播放器实例的生命周期不应和某个页面绑定,而应该和 App 绑定。我把 PlayerManager 改成全局单例后,推荐流页面只是订阅者,只负责监听状态变化并渲染界面。页面销毁时,播放器继续存在,只是不再被任何界面持有。

这个改动还有连接续上一次的 4.2 代码。PlayerManager 里维护一个当前视频 ID,推荐流页面回到前台时,会先向 Manager 查询当前正在播的视频,如果它还在列表里,就不重建纹理,直接复用。这个状态恢复过程不需要重新加载视频,恢复速度快得多。

需要提醒的是,全局单例并不意味着永不释放。当切到非视频页面时间超过一定阈值,或者系统内存告警时,还是要主动调用 release。我在影悦里结合 AppLifecycleListener 做了一套机制,App 进后台超过五分钟,自动暂停并释放播放器,只保留最后进度值。这样既省电,也避免后台播放造成非预期流量消耗。

5.3 前后台切换与恢复策略

App 从后台回前台时,播放器往往已经暂停,但 UI 进度条可能还停在离开那一刻。影悦的恢复逻辑是:回前台后读取播放器当前真实位置,如果离暂停位置超过两三秒,就主动 seek 到暂停位置。否则继续播放会让人看得非常跳跃。

这个“两三秒”的判断是因为播放器在后台有时仍会继续缓冲或者推进进度,不能完全依赖本地记录值。具体阈值根据项目播放器行为可以调,但建议不要设成零,否则频繁前后台切换会造成反复 seek,反而影响播放流畅度。

恢复逻辑还要配合推荐流当前索引。如果用户从前台切到后台前正好在推荐流第二集,回到前台时系统要先确认推荐流还定位在第二集,再恢复播放。如果用户在后台时长太长,推荐流可能已经因为内容时效性被刷新,这时候不要再播放旧视频,应该回到第一集重新开始。这个产品逻辑要在 Dart 层做,不能依赖原生播放器。

6. 打包集成与工程化:AS 创建工程到鸿蒙构建

6.1 怎么快速起一个新 Flutter 工程

热词里有“如何 AS 创建 Flutter 项目”,这里顺便说一下。用 Android Studio 创建 Flutter 项目其实是最常规的路径:New Project 里选择 Flutter,填好项目名和 SDK 路径,IDE 会自动生成 android、ios 和 lib 目录。如果后面要加鸿蒙支持,再单独引入鸿蒙的 Flutter 工程适配模块。

创建工程后建议立刻做两件事:第一,确定 Dart SDK 和 Flutter 版本与鸿蒙适配表对齐,避免版本不一致导致后面桥接层 API 对不上;第二,把 as 生成的原生目录和 Flutter 插件 module 分开维护。影悦就是按这个结构组织的,原生能力插件和业务逻辑分开,后面换平台时不会牵一发动全身。

6.2 鸿蒙侧集成 Flutter Module

鸿蒙侧集成 Flutter 时,不能直接把 Android 工程搬过去,而是要把 Flutter Module 作为依赖嵌入鸿蒙主工程。我需要说明的是,实际适配时不同的 Flutter 版本对应的鸿蒙 Flutter Engine 版本不完全一样,所以第一步永远是先核对版本映射表。

集成后原有的通道代码不需要改。Dart 侧注册的通道名、事件流名称在鸿蒙侧必须保持一致,否则会出现“Android 正常但鸿蒙调用无响应”的典型问题。我在开发影悦时专门写了一个通道名常量文件,两边共用,避免手写字符串不一致。

6.3 打包构建的三个高频错误

第一个高频错误是 Gradle 脚本用命令式方式应用 Flutter 主插件。报错信息里会提示不要用 apply 方式,而应该改为按插件 ID 方式引用。这个问题通常会出现在老工程升级 Flutter 版本之后,修复方法并不难,把 build.gradle 里的 apply 语句改成 plugins 块声明即可。

第二个高频错误是打包时出现 java.lang.AssertionError: could not close input 这类异常。我一开始以为是代码问题,后来排查发现多数是构建缓存文件被占用,常见于 Windows 环境。解决方式是在 gradle.properties 里关闭文件系统监听,然后 clean 之后重新构建。如果问题依旧,再检查是不是安全工具实时扫描了构建目录,扫描会导致 zip 输出流被干扰,加入例外目录后就能恢复。

第三个高频错误是 Kotlin 或 AGP 版本与 Flutter 插件不兼容。升级 Flutter 后很容易撞上,做完一次全量依赖版本对齐就能解决。遇到这种报错不要先怀疑自己的代码,先查版本矩阵,比瞎改代码高效得多。

还有一个我在真机上最常见的日志:E/flutter 开头的 Unhandled Exception,来自 Dart VM 初始化器。这个报错和原生侧不一定有关系,绝大多数情况是 Dart 业务层抛了未捕获异常。排查时要先去自己的异步回调里找,不要第一反应跑去鸿蒙侧查。这个经验我吃过大亏,现在每次带队都会强调一遍。

6.4 签名与真机调试

鸿蒙真机调试和 Android 类似,需要先在开发者后台申请调试证书和配置文件,再在工程配置里把签名文件关联上。注意发布和调试的签名要分开,不要把调试证书签成发布包,否则用户升级时会提示签名不一致无法覆盖安装。

真机调试时我习惯先把 Flutter 的 hot reload 打开,因为推荐流这种长交互页面,真机上的滑动感受和模拟器完全不同。边改边看热点区域的布局和手势响应,比一口气写完再上机效率高很多。

7. 高频问题排查实录与个人避坑清单

7.1 问题速查表

这里把影悦开发过程中我遇到的高频问题整理成一页速查表,适合打印出来贴在工位上,每次遇到类似现象先对照一遍。

现象可能原因解决思路
视频画面黑屏但音频正常textureId 为 0 或为空检查 prepare 返回值,Dart 层增加非法 ID 判断
视频画面不显示PlatformView 与 Impeller 合成冲突改成 Texture 纹理合成方案
返回页面后视频从头播播放器实例被页面销毁改用全局 PlayerManager 持有播放器
切页后进度条跳动前后台位置恢复逻辑太激进只在偏差超过阈值时执行 seek
下栏手势和视频滑动手势冲突RefreshIndicator 与 PageView 同轴争抢关闭 PageView 滚动,用 NotificationListener 判断边界
EventChannel 偶发不回调注册时机晚于 Dart 调用在引擎创建后先建立 Channel 再发起调用
打包报 could not close input构建缓存被占用/扫描干扰关闭文件监听,clean 后重新构建
E/flutter Unhandled ExceptionDart 层未捕获异常排查 Dart 异步回调,暂不怀疑原生层
低端机播放卡顿播放器实例未及时释放按索引距离释放远端播放器实例
鸿蒙调用 MethodChannel 无响应通道名不一致或插件未绑定核对双方通道名常量,确认插件注册时机

7.2 排查思路:日志先分端,不要一上来就瞎改

遇到播放器类问题,我强烈建议先分辨问题发生在 Dart 层、桥接层还是原生层。最简单的方法是在三个位置各打一条日志:Dart 调用前、MethodChannel 到达原生、原生执行完毕后返回。哪一端缺了,问题就在哪一段。

我在影悦里把这个日志做了一个总开关,平时不打开,出现问题时就打开。推荐流场景下日志量很大,如果不做总开关,反而会因为刷屏把真正的问题淹没掉。尤其是 EventChannel 的高频进度回调,必须单独禁用。

排查时还有一个经验是“往前想两步”。比如用户反馈推荐流滑动后画面会闪一下,很多人会先去调 textureId,但其实问题可能出在 PageView 的 item 被释放导致纹理从 Flutter 侧移除。先看生命周期,再看渲染,顺序反了会浪费时间。

7.3 关于鸿蒙侧适配的独家心得

我最后想单独聊一点鸿蒙适配的心得,因为这部分是网上资料最少、实际踩坑最密集的地方。

鸿蒙的 Flutter 生态还在快速迭代中,很多插件不是不支持,而是版本跟得太慢。影悦在适配时遇到几个第三方组件在鸿蒙上表现不一致,我的处理原则是“能不依赖就不依赖,能收口就收口”。播放器通道、事件通道、缓存管理这些能力全部自己封装,不直接依赖社区插件,这样每次鸿蒙适配版本升级时,受影响面可控。

另一个心得是:不要过度设计。一开始我想把推荐流做成通用组件,各种配置项铺得很开,结果鸿蒙适配时好多次都是这些多余配置导致原生侧行为不一致。后来砍掉了一半参数,代码反而更稳。越是跨平台项目,接口越要克制。

如果你正打算把 Flutter 项目往鸿蒙迁移,我建议先做一个最小的视频播放器 demo,把 MethodChannel、EventChannel、Texture 渲染这三条链路跑通,再谈业务迁移。这三条链路是地基,地基稳了,推荐流、详情页、搜索页这些都只是往上堆砖的事。

写在最后

影悦这个项目做下来,我自己最深的体感是:跨平台项目真正难的不是 Flutter 语法,而是对平台能力的抽象能力。凡是能用一个接口收口的能力,就不要让 UI 层去感知平台差异;凡是高频数据,就不要走请求响应模型,一定要用事件流;凡是会重建的页面,就不要把重要对象放在页面局部变量里。

这也是为什么后来鸿蒙二次适配只花了预期不到一半的时间。因为 PlayerAdapter 从 Android 换到鸿蒙时,推荐流页面一行代码都没改。这个结果比任何架构图都更能说明问题。希望这篇复盘能帮你少走一点路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询