# 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`) ## 目录结构 ```text 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` 和创作者入驻接口。 ## 运行 ### 安装依赖 ```bash flutter pub get ``` ### Android USB 手机开启 USB 调试并连接电脑后: ```bash adb devices adb reverse tcp:8081 tcp:8081 flutter run -d \ --dart-define=API_BASE_URL=http://127.0.0.1:8081 ``` `adb reverse` 将手机的 `127.0.0.1:8081` 转发到电脑;USB 断开或重新连接后需要 重新执行。 Android 模拟器访问宿主机时使用: ```bash flutter run --dart-define=API_BASE_URL=http://10.0.2.2:8081 ``` ### iOS 手机与 Mac 需在同一网络,首次启动允许“本地网络”权限: ```bash flutter run -d \ --dart-define=API_BASE_URL=http://:8081 ``` ### Debug 运行时地址 debug 包在找剧页加载失败时会显示调试条,点其中的「修改服务器地址」可直接改成 当前电脑 IP,保存后立即重载,不用重新打包。地址优先级: ```text 本地运行时设置 > --dart-define(API_BASE_URL) > AppConfig.defaultBaseUrl ``` `devHostBaseUrl` 默认写死为 `http://192.168.31.243:8081` (`lib/core/config/app_config.dart`)。debug 包启动时按 「环回 `127.0.0.1` → `devHostBaseUrl` → 公网」顺序探测可用地址;release 包不探测, 固定使用公网地址。换 Wi-Fi、路由器换网段后 `devHostBaseUrl` 会失效,此时要么传 `--dart-define`,要么改这个常量。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`: ```bash 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` 加载失败,通常是旧安装中的原生库残留: ```bash adb uninstall com.stormg.lumadrama flutter run -d \ --dart-define=API_BASE_URL=http://127.0.0.1:8081 ``` ## 生产打包(Android) 生产包只包含以下两个 ABI,**打成一个 APK**,不做 split: | ABI | 适用设备 | | --- | --- | | `arm64-v8a` | 现代手机(64 位) | | `armeabi-v7a` | 老 32 位机 | ```bash flutter build apk --release --target-platform android-arm64,android-arm # 产出单个:build/app/outputs/flutter-apk/app-release.apk(约 37MB) ``` 说明: - `android/app/build.gradle.kts` 的 `release` 已配置 `abiFilters`,只保留 `arm64-v8a` 与 `armeabi-v7a`,插件也不会再带入 `x86_64` 原生库。 - `x86_64` 只用于模拟器,不进入生产包。 - 不要直接运行 `flutter build apk --release`:默认会编译三套原生库,包体约 57MB。 - 只有需要进一步压缩单机下载体积时,才考虑 `--split-per-abi` 出分架构包(arm64 约 21MB、armv7 约 19MB),默认不用。 ## 生产打包(iOS) ```bash # 正式签名(需要 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` 后要重新构建,热重载不会生效。 ## 门禁 ```bash dart analyze flutter test flutter build apk --debug ``` 当前基线:`dart analyze` 仅 1 条既存 info(`intl` 未在 `pubspec.yaml` 声明); `flutter test` 全量 775 例通过。测试文件按模块放在 `test/` 下,用例名使用 `should_<期望>_when_<条件>`。真实后端联调测试在服务未启动时会跳过。 ## 当前边界 - 下载会落盘并支持离线播放,但不支持断点续传:中途失败会丢弃 `.part` 重下。 - 支付与金币:客户端已有钱包、商品、下单接口与页面,商店票据校验依赖服务端配置凭据。 - 站内通知没有已读/未读和红点状态。 - 播放清晰度切换会保留旧画面 500ms 掩盖闪屏,切换瞬间的进度以相对位置续播。