电影全栈系统的前几篇,我们把爬虫、后端接口、数据库、推荐服务都搭了起来,服务端的数据管道已经能稳定输出。但整个系统始终缺一个真正面向用户的东西,数据在接口里躺着,后台能看到,却没有任何一个贴近日常使用场景的入口。直到我开始动手做App这一层,整个系统才算真正闭合。
这一篇就是整个系列的第七篇,主题很明确:用Flutter把影视App的移动端做出来,对接前面搭好的后端服务,实现首页Feed流、电影详情、搜索、视频播放、个人中心这几个核心模块。全程从工程初始化开始讲,中间包括技术选型、目录结构设计、数据层封装、页面实现、播放器集成、性能优化和打包发布,基本覆盖一个Flutter项目从零到能上手的完整路径。
如果你手头也有一套后端API,正打算做一个跨平台App来做展示层,或者你只是想看看Flutter在实际项目里到底怎么组织代码、怎么踩坑、怎么优化,这篇应该能给你一份可以照抄的作业。
1. 项目概览与Flutter选型思考
1.1 整套系统的角色分配
在展开App开发之前,有必要把整个电影全栈系统的结构重新摆一下,因为App不是孤立存在的,它只是这套链路最外层的一个壳。
整套系统由这几层组成:最底层是数据采集模块,负责从公开互联网上抓取电影基础信息、海报、演员表、预告片地址等原始数据,经过清洗和结构化之后写入存储层。存储层用的是MySQL存关系型数据,MongoDB存文档型的信息,比如影评和用户行为日志。再往上是后端API服务,提供电影列表、详情、搜索、用户注册登录、收藏记录这些RESTful接口。再往上是推荐服务,基于用户的浏览和收藏行为做简单的协同过滤。管理后台用于维护数据源和审核内容。
App所处的位置,就是直接面向用户的最后一层。它消费的是后端API已经暴露好的能力,不在本地存核心业务数据,所有数据都通过网络请求获得。这个定位决定了App端的架构不需要考虑本地数据库,主要精力放在网络层封装、页面状态管理和体验细节上。
1.2 为什么选了Flutter,而不是React Native或者双端原生
先说结论:对于这个项目来说,Flutter是性价比最高的选择,没有之一。
当时摆在面前的选择有三个:一套代码跨平台的React Native、Flutter,以及iOS和Android各写一套原生。React Native的优势是生态成熟、JS/TS技术栈通用,但它有个老问题——桥接层的性能瓶颈,长列表滚动和视频播放这些高频场景容易掉帧。双端原生当然性能最好,但一个人维护两套代码库,排期直接翻倍,显然不现实。
Flutter的核心优势在于它不走系统原生控件,而是用自带的渲染引擎在每一帧上直接绘制UI。同一套代码在不同手机上渲染出来的效果几乎完全一致,不会出现同一段样式在iOS和Android上表现各异的情况。再加上Dart语言的AOT编译,运行性能在跨平台方案里属于第一梯队,列表滚动流畅度基本接近原生体验。
代价也很明显:包体积比原生大不少,简单App装下来也要20MB往上;一些依赖系统能力的场景,比如地图、支付、推送,还是得写原生插件做桥接。但对于影视App这种以展示和播放为主、不重度依赖系统能力的场景,Flutter可以说是量身定做。
1.3 这个App要覆盖什么功能
动手之前,我先把App的功能范围画了出来,确认不是做一个玩具Demo,而是能真正承接后端能力、走通核心流程的成品。
整体分五个Tab:首页、分类、搜索、我的。首页是Feed流,负责展示后端推荐接口输出的内容,支持下拉刷新和上拉加载更多;分类页按类型筛电影;搜索页对接后端的搜索接口,带防抖和搜索历史;我的页面承载登录、收藏列表、观看记录。点击任意一部电影进入详情页,详情页包含基本信息、评分、演员表、剧照,以及最重要的播放入口。播放页对接视频源地址,支持清晰度切换、倍速播放、横竖屏切换。
功能范围确认之后,我按照优先级拆成了两个里程碑。第一个里程碑先跑通"首页到详情到播放"这条核心链路,不追求页面的精美程度,确保数据能通、视频能放。第二个里程碑再做搜索、收藏、登录和性能优化。事实证明,这个节奏是明智的——先跑通主流程,后面加功能时心态会稳很多。
2. App工程的架构设计与基础搭建
2.1 环境准备与工具链
Flutter的环境搭建确实比一般框架要繁琐一些,主要是下载的依赖比较大。我这边直接用稳定版分支,保证第三方库的兼容性。
整个工具链包括:Flutter SDK、Dart SDK(SDK自带,不用单独装)、Android Studio(用于编译Android版本)、Xcode(只在需要打包iOS时用)、Visual Studio Code加Flutter插件(平时写代码用)。iOS端的构建我是在一台搭载macOS的机器上完成的,Windows环境下只能构建Android版本,这个要提前心里有数。
创建项目的命令很简单,flutter create movie_app,生成的标准工程会带一个counter Demo,先跑一次flutter run确认环境没问题,然后把lib目录下的demo代码清空,开始按自己的结构组织。
2.2 目录结构与代码分层
工程化搭建的第一步,不是急着写页面,而是把目录结构定下来。这个项目的目录结构参考了MVVM的分层思想,但以轻量为主,不引入过多抽象。
lib/ ├── main.dart # 应用入口 ├── core/ # 核心基础设施 │ ├── config/ # 环境配置、常量 │ ├── network/ # Dio封装、拦截器、错误处理 │ └── utils/ # 工具类 ├── models/ # 数据模型 ├── services/ # 接口服务层 ├── providers/ # 状态管理 ├── pages/ # 页面 │ ├── home/ │ ├── detail/ │ ├── search/ │ ├── profile/ │ └── player/ └── widgets/ # 通用组件这个结构的好处是边界清晰。models层只负责数据的序列化和反序列化,不掺业务逻辑;providers层负责状态和业务逻辑;pages层只做界面渲染和事件转发;services层统一管理API调用。任何一层出现问题,定位的时候能直接缩小到对应的目录。
我自己在项目开发中最常用的一条原则:如果某个文件超过300行,就要开始考虑拆分。首页的Feed列表如果全部塞在一个文件里,到后期改起来会非常痛苦,所以后面会把列表项、头部轮播、状态组件全部拆成独立文件。
2.3 依赖选型的完整清单
Flutter的生态虽然不如npm丰富,但核心库的质量整体不错。我最终确定的依赖清单如下,每个库都有明确的使用场景,不盲目引入。
dependencies: flutter: sdk: flutter dio: ^5.4.0 # 网络请求 provider: ^6.1.0 # 状态管理 cached_network_image: ^3.3.1 # 图片缓存 video_player: ^2.8.0 # 视频播放 chewie: ^1.7.0 # 播放器UI组件 shared_preferences: ^2.2.0 # 本地轻量存储 extended_image: ^8.1.0 # 图片扩展能力 pull_to_refresh: ^2.0.0 # 下拉刷新/上拉加载 intl: ^0.19.0 # 日期格式化这里解释几个选型细节。网络层选择Dio而不是直接用HttpClient,是因为Dio的拦截器机制对统一处理token注入和错误拦截很有帮助,拦截器这个功能在复杂项目中几乎人人都会用到。状态管理选择Provider而不是Bloc或GetX,核心原因是Provider的API最简单,没有代码生成,没有额外的模板代码,对于我这个体量的项目来说用Bloc反而是徒增负担。
播放器这里用video_player加chewie的组合,后者提供了现成的播放控制UI,省去自己写播放器控制条的麻烦。首次接入时需要注意,video_player在Android上默认支持的视频格式有限,对HLS的支持取决于底层的播放器实现,这个后面在播放模块会详细展开。
3. 首页Feed流与核心页面的实现
3.1 首页的信息架构
首页是整个App的门面,用户打开App的第一眼看到的就是它。我把首页设计成三段式结构:顶部是编辑推荐的轮播Banner区,展示当前热门的几部电影;中间是"正在热映"的横向滑动列表,按评分排序;底部是"即将上映"的竖向Feed流,支持滚动加载更多。
这个设计参考了主流视频App的通用布局,但也有一个关键的取舍:底部Feed流没有做成分页Tab,而是作为同一个页面的连续滚动,目的是减少用户的操作层级,让用户手指上下滑动就能浏览所有内容。对于移动端来说,滚动浏览的沉浸感远比点击切换Tab的方式好。
数据来源上,轮播和热映列表都来自后端的推荐接口,即将上映列表来自电影列表接口,按上映日期倒序排列。后端接口当时已经支持分页参数,这里直接透传page和size两个参数即可。
3.2 首页列表项的性能关键:拆分子组件
首页最核心的部分其实是那个竖向Feed流。如果用最粗暴的写法,在一个ListView.builder的itemBuilder里把整个卡片布局全部写完,项目小的时候没事,一旦列表项变复杂,滚动帧率就会出现肉眼可见的下降。
正确的做法是把列表项拆成独立的Widget,也就是PosterCard组件。海报图用CachedNetworkImage渲染,配合预加载机制,让下一屏的图片提前开始下载。标题和评分区域的文字用maxLines约束行数,配ellipsis省略号,避免长标题把布局撑坏。
另一个容易被忽略的点是:列表项Widget的构造函数要尽量少的非const参数。在Flutter中,如果Widget的构造是const的,框架就能复用同一份配置,减少重建开销。我在PosterCard的构造函数里加了const PosterCard({Key? key, ...}),并且在所有子组件前加上const关键字,让编译器尽可能做常量折叠。
实测下来,经过子组件拆分和const优化之后,在模拟器上进滑动Feed流基本能稳定在55帧以上,真机上体感也很流畅。
3.3 电影详情页与骨架屏
电影详情页是数据加载最复杂的页面,因为它在一个页面里要展示来自多个接口的数据:基本信息、评分、演职员列表、剧照、相关推荐。如果一次性请求全部数据,首屏空白时间会很长,所以我把详情页拆成两个数据阶段。
第一阶段只请求基本信息接口,拿到电影标题、海报、年份、评分这些核心字段,页面立刻渲染出主体框架。第二阶段再异步请求演职员列表和剧照,等数据到达后插入到页面对应区域。这个策略在弱网环境下优势非常明显,用户从首页点击进入详情页后几乎不会感受到白屏。
为了避免数据加载过程中的闪烁,我还给详情页加了一个骨架屏组件。骨架屏的实现并不复杂,用一个带灰色渐变动画的容器模拟排版结构,数据到达后渐隐淡出切换为真实内容。Flutter里用AnimatedSwitcher就能平滑地实现这个过渡。很多开发者在详情页直接用FutureBuilder包全屏,结果数据没到就转圈,数据到了整页刷新,这种体验是硬伤。
3.4 搜索页的防抖与历史记录
搜索页的功能看起来简单,里面有两个细节很值得琢磨。
第一个是搜索防抖。用户每敲一个字母就发一次请求,既浪费流量又给后端造成无谓的压力。实现方式是在输入框的onChanged回调里启动一个300毫秒的Timer,每次触发先把上一个Timer取消,只有300毫秒内没有新的输入才真正发起搜索请求。这样用户连续输入的时候只发一次请求,效果很理想。
第二个是搜索历史。我用shared_preferences做持久化,以字符串列表的形式存取,每次搜索成功后把关键词插入到列表头部,同时去重、限制最多10条。点历史标签能直接触发搜索,旁边的清除按钮一键清空。这个功能虽然简单,但对于提高用户的搜索效率帮助很大,属于投入产出比很高的功能。
热搜榜原本也打算做,后来一想后端还没有专门的热搜统计接口,临时加的话又得改服务端,就先砍掉了。做App的时候经常会遇到这种"前端想做但后端没接口"的情况,暴露出的其实是前后端接口规划的问题。如果追求完整,应该在系统设计阶段就定义好热搜榜的接口。
4. 前后端接口对接与数据层落地
4.1 接口约定与统一返回结构
前后端联调最头疼的就是接口不规范。我在写App之前,先跟后端接口约定了一套统一的返回格式,所有的接口都遵守,这样前端处理起来非常轻松。
约定的结构是这样:
{ "code": 0, "message": "ok", "data": {} }code为0表示成功,非0表示业务异常,message携带错误描述,data是真正的业务数据。分页接口的data是一整个对象,包含list、page、size、total四个字段。这套约定在后面的开发中几乎没给我添过麻烦,前后端联调非常顺畅。
接口定义方面,核心的几个是:GET /api/v1/movies?page=1&size=10获取分页电影列表,GET /api/v1/movies/{id}获取详情,GET /api/v1/search?keyword=xxx搜索,POST /api/v1/user/login登录,GET /api/v1/user/favorites获取收藏列表。视频播放地址也在详情接口里一起返回,方便客户端根据清晰度选择对应的播放链接。
4.2 Dio封装与统一异常处理
网络层是App能不能稳定运行的地基,我把Dio的封装写得很仔细。
核心思路是:创建一个单例Dio实例,配置BaseUrl和超时时间,再挂上拦截器。拦截器承担三件事:请求拦截时自动注入token头;响应拦截时统一解析code,非0直接抛业务异常;日志拦截时打印请求地址和耗时,方便联调定位问题。
超时设置我调成了连接超时10秒、接收超时15秒。电影详情和海报图的数据量比较大,弱网环境下太短的超时时间会频繁误报失败。这里也踩过一个坑:Dio的接收超时是"从开始接收到最后一个字节"的总时长,大图片下载时如果超时时间太短,图片没下完就会断掉,所以接收超时要比连接超时长一些。
错误处理方面,我在网络层定义了一个统一的ApiException,包含错误码和错误消息。异常类型的映射关系分成三类:网络不可用(SocketException)、请求超时(DioExceptionType.receiveTimeout)、服务端返回业务错误(code != 0)。UI层只需要捕获ApiException,根据错误类型显示不同提示文案,不用关心底层到底是哪个库抛的异常。
4.3 数据模型与状态管理的实践
数据模型层是所有逻辑的根基。Movie模型的fromJson和toJson用最原始的手写方式,没用json_serializable这类代码生成库,原因很简单——项目规模不大,手写模型文件总共也没多少,反而少了一步build_runner的生成步骤,依赖更少,编译更快。
class Movie { final int id; final String title; final String posterUrl; final String coverUrl; final double rating; final List<String> actors; final String description; Movie({...}); factory Movie.fromJson(Map<String, dynamic> json) { return Movie( id: json['id'], title: json['title'], posterUrl: json['poster_url'], coverUrl: json['cover_url'], rating: (json['rating'] as num?)?.toDouble() ?? 0, actors: (json['actors'] as List?)?.cast<String>() ?? [], description: json['description'] ?? '', ); } }注意rating用num转double的写法,这是Dart反序列化JSON时最容易踩的坑。JSON里的4.5解析出来是double,但如果是整数4解析出来是int,直接as double会崩,用num接收再转就稳多了。
状态管理这块,我以Provider为核心,按业务域划分了几个Provider。HomeProvider管理首页三个区块的数据和加载状态;SearchProvider管理搜索关键词、结果列表和防抖Timer;UserProvider管理登录状态、用户信息和收藏列表。每个Provider用ChangeNotifier配合notifyListeners驱动界面更新。这套组合拳打下来状态流转非常清晰,没有出现状态满天飞的情况。
5. 视频播放模块的实现与调优
5.1 播放器方案选型对比
视频播放是整个影视App最核心的功能,这块如果崩了,用户会直接卸载App。我在选型上花了不少时间调研。
Flutter生态里成熟的播放方案有几个:video_player是官方插件,底层在Android上是ExoPlayer,在iOS上是AVPlayer,稳定性和格式兼容性都是保障;chewie是基于video_player封装的带UI的播放器组件;better_player在video_player基础上增加了更多高级功能,比如缓存、倍速、手势控制,但个人维护的第三方库长期可靠性要打个问号。
我最初用chewie直接上,发现它的控制条UI定制起来非常繁琐,清晰度切换这种功能用chewie很难优雅实现。最终决定用video_player处理解码,UI控制层自己写,这样既能完全掌控播放器的交互体验,又能按需添加功能和样式。
选型的一个核心原则是:播放器的解码能力决定用户能不能看到画面,控制层决定用户用得舒不舒服,两层拆开选择各取所需比绑定一体的方案要灵活得多。
5.2 播放页的核心实现细节
播放页的实现有几个绕不开的难点。
生命周期管理是第一个。视频播放器跟普通页面不同,它占用的是系统媒体资源,播放中一旦页面被切走、App退到后台,不释放播放器就会黑屏、无声、甚至崩溃。我在播放器的initState里初始化VideoPlayerController,在dispose里强制调用dispose方法。同时监听App生命周期事件,切到后台时暂停播放,回到前台时恢复。这套逻辑看起来基础,但漏了任何一环都会出现诡异的现象。
清晰度切换是第二个难点。后端的视频源分蓝光、高清、标清三个档位,每个档位对应不同的URL。播放器支持在线换源的方式是重新设置VideoPlayerController的数据源,然后调用initialize和play。切换时先记录当前播放进度,换源完成后seek到原位置继续播放。这样切换清晰度不会中断观看体验,实测切换耗时在1到2秒的范围内。
手势控制是第三个难点。我在播放器上方覆盖了一层GestureDetector,通过onVerticalDragUpdate和onHorizontalDragUpdate监听手势。左侧上下滑动调亮度,右侧上下滑动调音量,水平滑动拖进度,单击暂停/播放,双击全屏。亮度音量通过系统通道控制,这个需要写一小段原生代码做MethodChannel的桥接。
5.3 播放性能与编码知识
视频能不能放得流畅,很大程度上取决于编码格式和网络环境的匹配。
后端返回的视频源有MP4和HLS两种格式。MP4适合即点即播,但拖动进度条时需要服务端支持range请求,否则拖动就会卡死;HLS是流媒体格式,天然支持进度拖动,因为是切片分发,弱网下的适应性也更好。对移动端来说,HLS是比MP4更优先的选择。
清晰度策略上,我做了区分:WiFi环境下默认选择高清源,移动网络环境下默认选择标清源,并允许用户在播放页手动切换。这个策略的基本逻辑是:清晰度本质上是码率和网络带宽的博弈,与其让用户看着转圈,不如自动选一个流畅的清晰度。实测2500kbps的高清源在4G网络下基本流畅,1000kbps的标清源在弱网也能稳住。
帧率和掉帧监控可以简单打印出来,用Flutter的PerformanceOverlay或者DevTools里的性能图表,开发阶段重点排查。线上用户反馈播放卡顿,多半是网络波动导致缓冲,这个依赖自适应码流才能根治,目前还没有大规模改造的计划。
6. 性能优化与体验细节打磨
6.1 图片加载与缓存策略
影视App的页面充斥着大图,海报图、剧照图、背景图,如果每次进入页面都重新下载,用户流量消耗巨大,加载速度也会受到严重影响。图片这一层不优化,其他优化做得再好都白搭。
我用cached_network_image统一处理网络图片,它内置了内存缓存和磁盘缓存两级缓存机制,图片下载一次之后,再次展示直接走缓存,速度从几百毫秒降到几毫秒。在详情页的背景大图场景,我额外调用了cached_network_image的宽高裁剪参数,让后端图片服务配合返回适当尺寸的压缩图,避免下载2MB原图只展示在手机屏幕上。
这里有一个经验:图片缓存的key就是URL,所以URL里尽量不要带动态的token参数,否则每次请求都是新URL,缓存永远命中不了。当时排查过一个详情页背景图反复闪烁的问题,最后发现问题出在URL带了时间戳参数,去掉之后立刻就稳定了。
6.2 列表滚动流畅度的优化实战
Flutter的列表性能优化,核心是减少不必要的Widget重建。
最基础的一条:能用const构造函数就不要省略。Flutter在进行Widget重建时会对比前后的config,const Widget的identical判断让框架直接跳过diff,省掉大量的构建时间。我在首页列表项、详情页的标签订阅、通用组件里全面应用了这一点。
第二条是RepaintBoundary的应用。当列表滚动时,如果某个子组件的绘制内容比较复杂,加上RepaintBoundary可以让它在滚动中不触发重绘,而是直接复用已经绘制好的图层。我在PosterCard的海报图外层包了RepaintBoundary,在播放页的播放器画面外层也包了,实测滚动流畅度提升非常明显。
第三条是ListView.builder的懒加载机制。它只构建当前屏幕范围可见的item,滑出屏幕的item会被回收,内存占用非常可控。这里特别强调,千万不要用ListView(children: [...])的写法来加载大批量数据,那会把所有子组件一次性构建出来,数据量一大直接卡死。
6.3 启动速度与包体积优化
启动速度直接影响用户对App的第一印象。我做了两件事:第一件是精简启动时的初始化任务,只保留必要项,其他模块改成首次使用时懒加载;第二件是给App设置了启动图,Flutter引擎在启动图显示期间完成初始化,用户至少不会看到白屏。
包体积方面,Flutter默认的release包体积在20MB左右。我通过flutter build apk --obfuscate --split-debug-info=build/symbols来做代码混淆和符号分离,混淆后包体积能再缩小10%到15%。另外,把资源文件里用不到的字体删掉,图片素材做压缩,多存一些这类功夫,整体包体积就下来了。
对于纯粹的内容展示类App,还有一个减少包体积的应该走的路子:使用App Bundle而不是APK上传。Google Play的App Bundle会按设备架构分发对应的二进制文件,用户实际下载的时候只需要下载当前设备需要的部分。
下载安装包的时候,我一度犹豫要不要深入研究ABI分裂、动态特性模块这些。后来想通了,包体节约20%比想象中的影响更大,用户下载成本是转化漏斗的第一环,数字背后全是真金白银的留存率。
7. 打包发布与上架注意事项
7.1 Android签名与打包
Android打包的第一步是生成签名文件。用keytool命令生成jks密钥库,注意保存好密码和有效期。然后写一个key.properties文件存签名信息,并在build.gradle里读取,这样打包时自动使用统一签名,不用每次手动输入。
storePassword=xxx keyPassword=xxx keyAlias=release storeFile=release-key.jks构建release包直接执行flutter build apk --release,产物在build/app/outputs/flutter-apk/app-release.apk。签名验证可以用apksigner verify,确保包已经正确签名。没有签名的包在手机上装不了,这是个低级但常见的错误。
上架的时候优先传AAB格式的app-release.aab,体积比APK小,也更符合官方分发规范。国内的部分应用市场不支持AAB,只认APK,所以国内渠道包我也保留了APK构建的流程。
7.2 版本管理与渠道包
版本管理两个关键值:versionCode是给机器看的,必须递增,每次发版都要比上一次大;versionName是给用户看的,遵循语义化版本号,比如1.0.0。我把这两个值统一维护在pubspec.yaml里面,Flutter会自动映射到对应平台的配置中,改动一处即可全局生效。
渠道包的问题主要在国内分发场景。如果要在多个应用市场上架,又要统计不同渠道的下载转化数据,就需要打渠道包。渠道包的做法通常是用不同的applicationId或者打包参数区分。因为我的项目只在少数几个渠道发布,暂时没有做复杂的多渠道打包,用一个统一的release包就行。
iOS打包这边,如果要在模拟器外真机运行或上架,需要开发者在苹果开发者账号中申请证书和描述文件。整个流程相对繁琐,这也是跨平台开发比原生开发体感好的例外场景之一——Android侧一键编译安装的流程确实比iOS正则签名验证要省心不少。
8. 开发中踩过的坑与经验速查
8.1 常见问题速查表
开发过程中踩过的坑不少,大部分当时花了很长时间排查,我把它们整理成了一个速查表,遇到问题直接按图索骥排查,效率会高很多。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 播放视频只有声音没有画面 | 视频编码格式不被底层解码器支持 | 换HLS源,或者在原生层配置备用的解码器 |
| 列表快速滚动时图片错位或闪白 | 图片加载未完成导致组件复用混乱 | 给Image组件设置固定的宽高,配合cached_network_image的占位图 |
| 播放页退出后声音还在响 | 播放器没有在dispose时释放 | 在dispose生命周期中强制调用controller.dispose() |
| Release包访问接口报错 | 网络配置或混淆规则问题 | 检查AndroidManifest的usesCleartextTraffic,以及proguard规则是否保留了模型类 |
| 详情页数据加载时页面闪烁 | 整个页面用了FutureBuilder导致整页重建 | 把数据加载拆分到子模块,用AnimatedSwitcher做局部过渡 |
| 真机调试时App闪退 | 可能是64位库不匹配 | 确认产物目录下同时保留了armeabi-v7a和arm64-v8a的so库 |
| 上架被拒:目标API级别过低 | 没有适配新的Android版本 | 把targetSdkVersion升到合规版本,并检查隐私权限 |
8.2 一轮开发下来最想说的几句经验
整个App从初始化到能跑通核心链路,再到打磨细节和打包,整个过程下来,有几个特别深的体会。
第一,不要一开始就追求架构的完美。最早的版本可以先用最笨的写法把功能跑通,等页面多了、逻辑复杂了,再回头看哪个部分需要重构。我在第二周就已经把首页重构过一次,因为第一次写得太简陋,但这次重构发生在对业务逻辑彻底理解之后,重构效率极高,改动范围也被控制在最小。
第二,网络层和错误处理值得花最多的时间。App的崩溃和卡顿有相当比例来自网络层,把Dio封装、异常映射、失败重试机制做扎实,后面所有页面都会受益。我见过有的开发者每个页面都写一套try-catch加自己的加载逻辑,结果错误提示风格五花八门,这就是网络层没做好的典型症状。
第三,善用热重载。Flutter的热重载在开发阶段几乎是无敌的存在,但注意有状态的代码改动它不会生效,比如修改了Provider的初始化逻辑。遇到"改了代码但没效果"的情况,先按小写r热重载,不行就大写R完全重启,再不行就冷启动。
第四,日志是整个联调阶段最好的朋友。我在Dio的拦截器里全量打印了请求地址、入参和响应耗时,排查问题时一眼就能看出是参数错了、还是后端返回慢、又或者是数据解析崩了。等到上架前再统一把这些日志输出关闭,避免敏感信息泄露。
从最初的爬虫搭建到今天的Flutter客户端,整个电影全栈系统到这里算是走完了一个完整的闭环。数据在采集端被源源不断地收进来,经过清洗、入库、接口、推荐,最后在手机屏幕上以流畅的信息流和播放体验呈现给用户。这一路上的每个环节单独拎出来都不算高深,但把它们串在一起,并且稳定地跑起来,这个过程本身就是最好的学习方式。
最后再分享一个小技巧:调试播放器的时候,把VideoPlayerController的日志级别调到debug,它能打出当前播放器的缓冲进度、比特率这些底层信息,排查卡顿问题比纯靠猜要高效得多。这个技巧在文档里不容易翻到,但对实际排查帮助非常大。