213 lines
9.1 KiB
Markdown
213 lines
9.1 KiB
Markdown
# 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 <device>
|
||
```
|
||
|
||
`adb reverse` 将手机的 `127.0.0.1:8081` 转发到电脑;USB 断开或重新连接后需要
|
||
重新执行。此时用调试条把地址改成 `http://127.0.0.1:8081`(见下节)。
|
||
|
||
Android 模拟器访问宿主机时,同样在调试条里填 `http://10.0.2.2:8081`。
|
||
|
||
### iOS
|
||
|
||
iOS 模拟器直接访问 Mac 本机服务,默认地址为 `http://127.0.0.1:8081`:
|
||
|
||
```bash
|
||
flutter run -d <simulator-udid>
|
||
```
|
||
|
||
iPhone 真机不能用 `127.0.0.1` 访问 Mac;真机联调时需让手机与 Mac 位于同一网络,
|
||
并通过调试入口临时填写 Mac 的局域网地址。
|
||
|
||
### 服务地址配置
|
||
|
||
地址是代码里的一个开关,**不做任何网络探测**,也不用传构建参数。两套环境各占
|
||
一个文件:
|
||
|
||
| 文件 | 用途 | 默认值 |
|
||
| --- | --- | --- |
|
||
| `lib/core/config/dev_config.dart` | 开发 / 联调 | `http://127.0.0.1:8081` |
|
||
| `lib/core/config/prod_config.dart` | 生产 | `http://39.162.50.99:8081` |
|
||
|
||
切换环境只改 `lib/core/config/app_config.dart` 里的一个布尔值:
|
||
|
||
```dart
|
||
static const bool useDevConfig = true; // false 即用生产地址
|
||
```
|
||
|
||
真机或 Android 模拟器联调时,通过调试入口覆盖地址,不修改默认开发配置。打生产包前把
|
||
`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`:
|
||
|
||
```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 <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)
|
||
|
||
```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 掩盖闪屏,切换瞬间的进度以相对位置续播。
|