xcconfig 增加 Pods 引用(PiP 等插件依赖),Podfile 纳入版本管理, 保证任何机器拉取代码后可直接构建 iOS。
FruitShorts(ReelShort)客户端
FruitShorts 是面向印尼市场的短剧 Flutter 客户端,已连接 FruitServer 的
/app/** 接口。功能覆盖:四 tab 主页(推荐视频流 / 找剧 / 追剧 / 我的)、
沉浸式播放与全屏剧集播放器、清晰度切换、剧单与排行榜、新剧页、搜索、观看记录、
收藏短剧、评论与点赞、站内通知、钱包与商品、下载落盘与离线播放、中英印尼三语,
以及 Android / iOS 双端画中画(退到后台自动进小窗)。
技术栈
- Flutter 3.47.7 / Dart 3.13.5(
environment.sdk: ^3.12.2) - Provider 状态管理与依赖注入
- dio 网络请求
- video_player 播放
- shared_preferences 与
SharedPreferencesAsync本地配置 - path_provider + dart:io 下载落盘(先写
.part,完成后原子改名) - flutter_localizations,支持
en、zh、id - 画中画:Android 用
floating+permission_handler(需悬浮窗权限); iOS 用原生AVPictureInPictureController+ 影子播放器(ios/Runner/AppDelegate.swift)
目录结构
lib/
core/
config/ 地址、语言和运行时配置
data/ models、remote API、repository、controller
formatting/ 文案与日期格式化
platform/ 平台能力:画中画通道、设备号、触觉
theme/ 颜色与主题
widgets/ 跨页面复用组件
l10n/ 三语 ARB 与生成代码
pages/ 业务页面
test/ 单元、组件、页面和联调测试
integration_test/ 设备侧集成测试
android/ Android 工程
ios/ iOS 工程与 SPM 配置
assets/ 图标、图片和本地媒体文件
已接接口
以下接口均在 lib/core/data/remote/ 中有实际调用,共 47 个不同路径。
| 分组 | 接口 |
|---|---|
| 账号 | POST /app/auth/register、/login、/refresh、/logout、/otp;GET /app/me、/app/me/devices、/app/me/export;POST /app/me/deactivate、/app/me/deactivate/cancel |
| 内容 | GET /app/home/feed、/app/feed、/app/discover/feed、/app/categories、/app/categories/{id}/dramas、/app/drama-categories、/app/dramas/page、/app/dramas/{id}、/app/tags、/app/search、/app/playlists、/app/playlists/{id} |
| 播放 | GET /app/episodes/{id}/play、/app/episodes/{id}/stats;POST /app/episodes/{id}/unlock |
| 观看记录 | POST/GET /app/watch/progress、GET /app/watch/recent、DELETE /app/watch/progress/{dramaId}、DELETE /app/watch/progress |
| 收藏 | POST /app/favorites/toggle、GET /app/favorites、DELETE /app/favorites/{dramaId}、POST /app/favorites/remove |
| 下载 | GET /app/downloads/policy、PUT /app/downloads/settings、POST/GET /app/downloads、GET /app/downloads/{id}/url、POST /app/downloads/{id}/progress、DELETE /app/downloads/{id}、DELETE /app/downloads |
| 互动 | GET/POST /app/comments、DELETE /app/comments/{id}、GET /app/comments/count、POST /app/likes/toggle、POST /app/reports |
| 商业化 | GET /app/wallet、/app/products、/app/orders |
| 公告 | GET /app/notices |
客户端没有调用 /app/config 和创作者入驻接口。
运行
安装依赖
flutter pub get
Android USB
手机开启 USB 调试并连接电脑后:
adb devices
adb reverse tcp:8081 tcp:8081
flutter run -d <device>
adb reverse 将手机的 127.0.0.1:8081 转发到电脑;USB 断开或重新连接后需要
重新执行。此时用调试条把地址改成 http://127.0.0.1:8081(见下节)。
Android 模拟器访问宿主机时,同样在调试条里填 http://10.0.2.2:8081。
iOS
手机与 Mac 需在同一网络,首次启动允许“本地网络”权限:
flutter run -d <udid>
服务地址配置
地址是代码里的一个开关,不做任何网络探测,也不用传构建参数。两套环境各占 一个文件:
| 文件 | 用途 | 默认值 |
|---|---|---|
lib/core/config/dev_config.dart |
开发 / 联调 | http://192.168.31.243:8081 |
lib/core/config/prod_config.dart |
生产 | http://39.162.50.99:8081 |
切换环境只改 lib/core/config/app_config.dart 里的一个布尔值:
static const bool useDevConfig = true; // false 即用生产地址
换 Wi-Fi 或换网段时只改 dev_config.dart 的 baseUrl。打生产包前把
useDevConfig 改成 false;release 构建若仍为 true 会在启动时直接报错,
避免线上包连到开发机。
debug 包在找剧页加载失败时会显示调试条,点「修改服务器地址」可临时改成当前 电脑 IP,保存后立即重载、不需要重新打包,该覆盖保存在本地偏好里(仅 debug 生效,release 不读取)。
画中画
| 平台 | 行为 |
|---|---|
| Android | 首次点播放页小窗按钮时申请悬浮窗权限;授权后退出 App 自动进入系统小窗。沉浸流与独立播放页共用一份引用计数,任一处活跃都算「在播」,避免互相顶掉 |
| iOS | 原生影子播放器承载 AVPictureInPictureController,只有播放页处于活跃状态时退到后台才自动进小窗;停在剧单等页面退后台不会触发 |
- 小窗内切集直接换源,不走页面翻页,可连续自动播放
- iOS 从系统小窗还原时用原生过渡层盖住纹理黑帧,等首帧就绪后再淡出
- 播放页「更多」面板里有「退出自动小窗」开关(仅支持自动小窗的平台显示),关掉后 退到后台不再进入小窗
环境注意
iOS Swift 版本
pubspec.yaml 将 shared_preferences_foundation 固定为 2.5.6。该版本兼容
Xcode 16.2 / Swift 6.0.3;2.6.0 的生成代码使用 Swift 6.1 才支持的 trailing
comma。不要手工升级该依赖,flutter pub get 后应显示
shared_preferences_foundation 2.5.6 (overridden)。
iOS SPM 包缺失
如果 Xcode 报 Missing package product FlutterGeneratedPluginSwiftPackage:
rm -rf ~/Library/Developer/Xcode/DerivedData/Runner-*
flutter clean
flutter pub get
xcodebuild -resolvePackageDependencies \
-project ios/Runner.xcodeproj \
-scheme Runner
随后重新打开工程构建。Runner.xcscheme 的 PreAction 会先执行
flutter pub get,再执行 Flutter prepare。
Android libdartjni
若启动时报 libdartjni.so 加载失败,通常是旧安装中的原生库残留:
adb uninstall com.stormg.lumadrama
flutter run -d <device>
生产打包(Android)
两个渠道出两种产物,源码同一套:
| 渠道 | 产物 | 命令 | 体积 |
|---|---|---|---|
| Google Play | AAB(按设备下发) | flutter build appbundle --release |
上传 28MB,用户下载约 8MB |
| 国内各大市场 | 通用 APK(arm64 + v7) | flutter build apk --release |
约 13MB |
说明:
android/app/build.gradle.kts的release已配置abiFilters,只保留arm64-v8a与armeabi-v7a,插件也不会再带入x86_64原生库。android/gradle.properties里固定了split-debug-info=build/symbols与dart-obfuscation=true,IDE 里点 Build APK / Build App Bundle 也会生效:libapp.so从 ~8MB/ABI 降到 ~1.1MB/ABI。useLegacyPackaging = true:原生库以压缩形式存储,这是国内 APK 从 40MB 降到 13MB 的主要原因;代价是安装时解压。- 每次发版都要归档
build/symbols,线上崩溃栈需要它还原。 - release 仍在用 debug 签名(
build.gradle.kts的 TODO),上架前必须换成正式 keystore。 - 不要用
--split-per-abi:与abiFilters冲突;Play 的 AAB 已经按设备拆好。
生产打包(iOS)
# 正式签名(需要 iOS Distribution 证书 + App Store 描述文件)
flutter build ipa --release
# 只有 Apple Development 证书时:出开发签名 IPA,只能装已注册设备
flutter build ipa --release --export-method development
产物在 build/ios/ipa/*.ipa。画中画依赖 UIBackgroundModes 里的 audio 与
AVPictureInPictureController,改动 ios/Runner/Info.plist 或 AppDelegate.swift
后要重新构建,热重载不会生效。
门禁
dart analyze
flutter test
flutter build apk --debug
当前基线:dart analyze 仅 1 条既存 info(intl 未在 pubspec.yaml 声明);
flutter test 全量 775 例通过。测试文件按模块放在 test/ 下,用例名使用
should_<期望>_when_<条件>。真实后端联调测试在服务未启动时会跳过。
当前边界
- 下载会落盘并支持离线播放,但不支持断点续传:中途失败会丢弃
.part重下。 - 支付与金币:客户端已有钱包、商品、下单接口与页面,商店票据校验依赖服务端配置凭据。
- 站内通知没有已读/未读和红点状态。
- 播放清晰度切换会保留旧画面 500ms 掩盖闪屏,切换瞬间的进度以相对位置续播。