← API | 列表 | SDK接入指南
提示信息
# SDK 接入指南 v2

> 新建项目时参考本文档。每个 SDK 的接入步骤、关键配置、常见坑点均按实际踩过的经验整理。  
> **v2 更新**:全面同步新依赖版本(Riverpod 3.x、freezed 3.x、go_router 17.x、freerasp 7.x、envied 1.x 等)。

---

## 目录

1. [基础依赖(必接)](#一基础依赖必接)
2. [状态管理 · Flutter Riverpod 3.x](#二状态管理--flutter-riverpod-3x)
3. [路由 · go_router 17.x](#三路由--go_router-17x)
4. [本地存储 · MMKV](#四本地存储--mmkv)
5. [即时通讯 · OpenIM SDK](#五即时通讯--openim-sdk)
6. [微信 SDK · fluwx](#六微信-sdk--fluwx)
7. [支付宝 SDK · tobias](#七支付宝-sdk--tobias)
8. [应用内购 · in_app_purchase](#八应用内购--in_app_purchase)
9. [阿里云推送 · AlicloudPush](#九阿里云推送--alicloudpush)
10. [阿里云反馈 · AlicloudFeedback](#十阿里云反馈--alicloudFeedback)
12. [友盟统计 · umeng_common_sdk](#十二友盟统计--umeng_common_sdk)
13. [穿山甲广告 · Pangle (CSJ)](#十三穿山甲广告--pangle-csj)
14. [腾讯广告联盟 · GDT](#十四腾讯广告联盟--gdt)
15. [广告瀑布流架构说明](#十五广告瀑布流架构说明)
16. [图片/相机选择器](#十六图片相机选择器)
17. [音频录制 · record](#十七音频录制--record)
18. [权限管理 · permission_handler](#十八权限管理--permission_handler)
19. [iOS Podfile 通用配置模板](#十九ios-podfile-通用配置模板)
20. [AppConfig 统一配置说明](#二十appconfig-统一配置说明)
21. [动画增强 · flutter_animate](#二十一动画增强--flutter_animate)
21B. [骨架屏 · skeletonizer 2.x](#二十一b骨架屏--skeletonizer-2x)
22. [代码生成 · freezed 3.x + json_serializable](#二十二代码生成--freezed-3x--json_serializable)
23. [开发钩子 · flutter_hooks](#二十三开发钩子--flutter_hooks)
24. [样式框架 · VelocityX](#二十四样式框架--velocityx)
25. [色彩系统 · flex_color_scheme](#二十五色彩系统--flex_color_scheme)
26. [调试神器 · talker 5.x](#二十六调试神器--talker-5x)
27. [运行环境安全检测 · freerasp 7.x](#二十七运行环境安全检测--freerasp-7x) (详见 [API 认证与签名规范.md](./API认证与签名规范.md))
28. [iOS 2024 隐私合规 (Privacy Manifest)](#二十八ios-2024-隐私合规-privacy-manifest)
29. [密钥安全进阶 · envied 1.x](#二十九密钥安全进阶--envied-1x)
30. [IM 与推送补充:iOS 后台模式](#三十im-与推送补充ios-后台模式)
31. [国际化 (i18n) 规范说明](#三十一国际化-i18n-规范说明)
32. [架构进阶:SDK 深度联动](#三十二架构进阶sdk-深度联动)
33. [安全加固:密钥保护深度实践](#三十三安全加固密钥保护深度实践)

---

## 一、基础依赖(必接)

> ⚠️ **v2 重大版本变更**:所有包版本已全面升级,以下清单为当前唯一基准,禁止混用旧版。

```yaml
environment:
  sdk: '>=3.8.0 <4.0.0'   # ⚠️ 必须 >=3.8.0,json_serializable 6.11+ 要求

dependencies:
  # 核心架构(三件套必须版本一致)
  flutter_riverpod: ^3.3.1
  hooks_riverpod: ^3.3.1
  riverpod_annotation: ^4.0.2
  flutter_hooks: ^0.21.3
  go_router: ^17.1.0
  dio: ^5.4.0
  mmkv: ^2.0.1

  # UI 与动画
  shimmer: ^3.0.0
  skeletonizer: ^2.1.3   # ⚠️ 必须 2.x,Flutter 3.29+ 新 Canvas API 要求
  easy_refresh: ^3.3.4
  flutter_animate: ^4.5.0
  cached_network_image: ^3.4.1
  flutter_cache_manager: any
  flutter_svg: ^2.0.9
  qr_flutter: ^4.1.0
  flutter_widget_from_html: ^0.17.1
  lottie: ^3.0.0

  # 图标
  remixicon: ^1.4.0

  # 主题与敏捷布局
  flex_color_scheme: ^8.4.0
  velocity_x: ^4.0.0

  # 数据模型
  freezed_annotation: ^3.1.0
  json_annotation: ^4.11.0

  # 工具
  connectivity_plus: ^7.0.0
  path_provider: ^2.1.2
  sqflite: ^2.3.0
  path: ^1.9.0
  permission_handler: ^12.0.1
  intl: ^0.20.2
  uuid: ^4.3.3
  crypto: ^3.0.7
  url_launcher: ^6.3.2
  screen_protector: ^1.5.1
  image_gallery_saver_plus: ^4.0.1   # ⚠️ 原 image_gallery_saver 已停更,改用 plus 版

  # 媒体
  wechat_assets_picker: ^10.1.1
  wechat_camera_picker: ^4.4.0
  image_cropper: ^11.0.0
  flutter_image_compress: ^2.4.0
  record: ^6.2.0
  just_audio: ^0.10.5

  # 业务 SDK
  flutter_openim_sdk: ^3.8.3
  fluwx: ^5.7.5
  tobias: ^5.3.4
  in_app_purchase: ^3.2.3
  umeng_common_sdk: ^1.3.0

  # 安全
  freerasp: ^7.5.0
  device_info_plus: ^12.3.0
  flutter_udid: ^4.1.2
  envied: ^1.3.3

  # 调试
  talker_flutter: ^5.1.15
  talker_dio_logger: ^5.1.15
  talker_riverpod_logger: ^5.1.14

  # WebView
  flutter_inappwebview: ^6.1.5

  # 国际化
  flutter_localizations:
    sdk: flutter

  # 隐私(iOS ATT)
  app_tracking_transparency: ^2.0.6+1

dev_dependencies:
  riverpod_generator: ^4.0.3   # 必须与 riverpod_annotation 版本严格配套
  build_runner: ^2.12.2
  freezed: ^3.2.5
  json_serializable: ^6.11.2
  json_annotation: ^4.11.0
  envied_generator: ^1.3.3
  flutter_launcher_icons: ^0.14.4
  flutter_lints: ^6.0.0
```

---

## 二、状态管理 · Flutter Riverpod 3.x

**版本**:`flutter_riverpod ^3.3.1` + `hooks_riverpod ^3.3.1` + `riverpod_annotation ^4.0.2` + `riverpod_generator ^4.0.3`

### ⚠️ Riverpod 3.x 最重要的 Breaking Change

**废除了独立的 `XxxRef` 类型,统一使用通用 `Ref`。**

```dart
// ❌ riverpod 2.x 旧写法(编译报 Undefined class)
Dio mainDio(MainDioRef ref) { ... }
GoRouter router(RouterRef ref) { ... }
AppStartApi appStartApi(AppStartApiRef ref) { ... }

// ✅ riverpod 3.x 新写法
Dio mainDio(Ref ref) { ... }
GoRouter router(Ref ref) { ... }
AppStartApi appStartApi(Ref ref) { ... }
```

**`@riverpod class` 写法不变**(`AsyncNotifier`、`Notifier` 均向后兼容):

```dart
@riverpod
class XxxController extends _$XxxController {
  @override
  Future<List<XxxModel>> build() async {
    return ref.watch(xxxRepositoryProvider).fetchList();
  }
}
```

### 接入步骤

1. 在 `main.dart` 用 `ProviderScope` 包裹整个应用,挂载 `TalkerRiverpodObserver`:

```dart
final talker = TalkerFlutter.init();

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await MMKV.initialize();

  runApp(
    ProviderScope(
      observers: [TalkerRiverpodObserver(talker: talker)],
      child: const App(),
    ),
  );
}
```

2. 代码生成(⚠️ `flutter pub run` 已废弃,改用 `dart run`):

```bash
dart run build_runner build --delete-conflicting-outputs
# 监听模式
dart run build_runner watch --delete-conflicting-outputs
```

### ⚠️ 注意

- `riverpod_annotation ^4.0.2` + `riverpod_generator ^4.0.3` + `hooks_riverpod ^3.3.1` 三包版本必须严格对应,升级时一起升
- 旧代码中的 `MainDioRef`、`RouterRef`、`AppStartApiRef` 等类型一律改为 `Ref`

---

## 三、路由 · go_router 17.x

**版本**:`^17.1.0`

### ⚠️ go_router 17.x Breaking Change

**`redirect` 回调签名从同步改为异步**,旧写法编译报错:

```dart
// ❌ go_router 13.x 旧写法
redirect: (context, state) {
  return '/login';
},

// ✅ go_router 17.x 新写法(必须 async)
redirect: (BuildContext context, GoRouterState state) async {
  // 现在可以直接 await 异步操作
  final token = AppStorage.getString(StorageKeys.accessToken);
  if (token == null) return AppRoutes.login;
  return null;
},
```

### 接入步骤

1. `lib/routes/app_routes.dart` — 统一管理路由常量字符串
2. `lib/routes/app_router.dart` — 定义 `GoRouter` 实例

```dart
// ✅ go_router 17.x + Riverpod 3.x 完整写法
import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'app_router.g.dart';

@riverpod
GoRouter router(Ref ref) {   // ← Riverpod 3.x: 通用 Ref
  return GoRouter(
    initialLocation: AppRoutes.splash,
    observers: [TalkerRouteObserver(talker)],
    redirect: (BuildContext context, GoRouterState state) async {
      // ← go_router 17.x: 异步签名
      final location = state.matchedLocation;
      if (location == AppRoutes.splash || location == AppRoutes.privacy) {
        return null;  // 放行,避免 redirect loop
      }
      final agreed = AppStorage.getBool(StorageKeys.privacyAgreed) ?? false;
      if (!agreed) return AppRoutes.privacy;
      final token = AppStorage.getString(StorageKeys.accessToken);
      if (token == null) return AppRoutes.login;
      return null;
    },
    routes: [...],
  );
}
```

3. 在 `MaterialApp.router` 中使用 `routerConfig: router`

### ⚠️ 注意

- Tab 页用 `StatefulShellRoute.indexedStack` 保持各 Tab 状态(API 不变)
- Deep Link 参数通过 `state.uri.queryParameters` 读取,`extra` 传复杂对象(不变)
- `redirect` 函数**必须加 `async`**,否则编译报类型不匹配

---

## 四、本地存储 · MMKV

**版本**:`^2.0.1`(无 breaking changes)

### 接入步骤

```dart
// main.dart — 必须是 WidgetsFlutterBinding.ensureInitialized() 后的第一个 await
await MMKV.initialize();
```

### ⚠️ 注意

- **必须是第一个 await**,在 `WidgetsFlutterBinding.ensureInitialized()` 之后立即调用
- iOS 会自动使用 App Group 路径;Android 需保证存储权限
- 不适合存储大型数据(限制 128KB 以内),大文件用 SQLite / 磁盘文件

---

## 五、即时通讯 · OpenIM SDK

**版本**:`flutter_openim_sdk ^3.8.3`(较旧版 `^3.5.1` 升级,API 向后兼容)  
**后端地址**:`https://im-api.tongban.wang` / `wss://im-ws.tongban.wang/msg_gateway`

### 接入步骤

1. `pubspec.yaml` 添加依赖
2. iOS `Podfile` 无需额外 pod(Flutter 插件自动引入)
3. Android 确保 `minSdkVersion >= 21`

### Flutter 侧初始化顺序(封装在 ImAdapter)

```dart
// 初始化 SDK(只调用一次)
await OpenIM.iMManager.initSDK(
  platformID: 5,               // ⚠️ 固定写 5(自定义端),禁止动态检测
  apiAddr: 'https://im-api.tongban.wang',
  wsAddr: 'wss://im-ws.tongban.wang/msg_gateway',
  dataDir: path,               // getApplicationDocumentsDirectory()/im_db
  logLevel: 6,
  listener: OnConnectListener(...)
);

// 登录(每次启动从 AppStorage 读取 imToken)
await OpenIM.iMManager.login(userID: uid, token: imToken);
```

### ⚠️ 注意

- **Token 过期**:SDK 回调 `onUserTokenExpired` → 重新请求业务后端获取新 imToken → 重新调用 `login()`。错误码 `10004` / `10005` = token 过期
- **网络恢复自动重连**:监听 `connectivity_plus ^7.0.0`,断网恢复时若 `!_isLoggedIn` 则重试 login
- **platformID 必须写 5**,不要动态检测

---

## 六、微信 SDK · fluwx

**版本**:`^5.7.5`(6.0 还在 preview,稳定版保持 5.x,API 无变化)

### pubspec.yaml 配置

```yaml
fluwx:
  app_id: "wx146a6f748a99a0da"
  universal_link: "https://www.tongban.wang/app/"
```

### iOS 配置

1. `Info.plist` 添加 `LSApplicationQueriesSchemes` → `weixin`、`weixinULAPI`
2. `URL Types` 添加 Scheme = 微信 AppId
3. Associated Domains 添加 `applinks:www.tongban.wang`
4. **Podfile 环境变量**(必须,否则 pod install 报 Ruby 错误):

```ruby
ENV['FLUWX_SKIP_SETUP'] = 'true'
pod 'WechatOpenSDK-XCFramework', :modular_headers => true
```

### Android 配置

1. `AndroidManifest.xml` 添加 `.wxapi.WXEntryActivity`
2. 新建 `java/[包名]/wxapi/WXEntryActivity.java` 继承 `WXCallbackActivity`

### ⚠️ 注意

- Universal Link 必须在微信开放平台配置,且服务器 `.well-known/apple-app-site-association` 必须可访问
- `FLUWX_SKIP_SETUP=true` 跳过 fluwx 自带的 Ruby setup 脚本

---

## 七、支付宝 SDK · tobias

**版本**:`^5.3.4`(6.0 还在 preview,稳定版保持 5.x,API 无变化)

### pubspec.yaml 配置

```yaml
tobias:
  url_scheme: "com.xiaopai.match"
```

### iOS 配置

1. `Info.plist` 添加 `LSApplicationQueriesSchemes` → `alipay`、`alipays`
2. `URL Types` 添加 Scheme = `com.xiaopai.match`

### ⚠️ 注意

- `ENV['TOBIAS_SKIP_SETUP'] = 'true'` 写在 Podfile 顶部,同 fluwx 原因
- 支付完成回调通过 `listenAliPayResult()` 监听,注意在页面 `dispose` 时取消订阅

---

## 八、应用内购 · in_app_purchase

**版本**:`^3.2.3`(无 breaking changes)

### 接入步骤

1. iOS:Xcode → Signing & Capabilities → 添加 `In-App Purchase`
2. Android:`build.gradle` 确认 `com.android.billingclient` 依赖由插件自动引入

### ⚠️ 注意

- 沙盒环境测试需在 App Store Connect 创建沙盒账号
- `purchaseStream` 要在整个应用生命周期保持活跃(推荐在 `ProviderScope` 级别维护)

---

## 九、阿里云推送 · AlicloudPush

**接入方式**:纯原生 Native,Flutter 侧通过 MethodChannel `com.xiaopaix.app/push` 交互(无变化)

### iOS Podfile

```ruby
source 'https://github.com/aliyun/aliyun-specs.git'   # 必须加,位于 CocoaPods 官方 source 之后
pod 'AlicloudPush', '~> 3'
```

### iOS AppDelegate.swift

```swift
CloudPushSDK.start(withAccessKey: "YOUR_KEY", secretKey: "YOUR_SECRET") { result in }
CloudPushSDK.setPushTokenUploadHandler { token in /* 上传 token */ }
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { ... }
```

### Android build.gradle

```groovy
manifestPlaceholders = [
    PUSH_APPKEY: "YOUR_KEY",
    PUSH_APPSECRET: "YOUR_SECRET"
]
```

### ⚠️ 注意

- `source 'https://github.com/aliyun/aliyun-specs.git'` **必须放在 CocoaPods 官方 source 之后**
- `post_install` 中 neutralize 含 `Strip` / `archs` 的 shell script
- `ENABLE_BITCODE = NO`

---

## 十、阿里云反馈 · AlicloudFeedback

**接入方式**:纯原生 Native(无变化)

### iOS Podfile

```ruby
pod 'AlicloudFeedback', '~> 3.4.2'
```

### ⚠️ 注意

- 与 AlicloudPush 共享阿里云 AppKey/Secret(同一应用)
- 同样存在 `PhaseScriptExecution` 失败问题,Podfile `post_install` neutralize 处理

---

## 十二、友盟统计 · umeng_common_sdk

**版本**:`^1.3.0`(无 breaking changes)

### 接入步骤

**iOS Podfile**:

```ruby
pod 'UMCommon', :modular_headers => true
pod 'UMDevice', :modular_headers => true
```

**Podfile post_install — 注入头文件搜索路径**:

```ruby
if ['fluwx', 'umeng_common_sdk'].include?(target.name)
  config.build_settings['HEADER_SEARCH_PATHS'] ||= '$(inherited) '
  config.build_settings['HEADER_SEARCH_PATHS'] << '"${PODS_ROOT}/Headers/Public/UMCommon" '
  config.build_settings['HEADER_SEARCH_PATHS'] << '"${PODS_ROOT}/Headers/Public/UMDevice" '
end
```

**Flutter 侧初始化(必须在隐私协议同意后)**:

```dart
UmengCommonSdk.initCommon(androidAppKey, iosAppKey, channel);
UmengCommonSdk.setPageCollectionModeManual();
```

### ⚠️ 注意

- **绝对不能在隐私协议弹窗前初始化**(违规,可能下架)
- Android 的 `channel` 渠道包名必须与应用后台配置一致

---

## 十三、穿山甲广告 · Pangle (CSJ)

**接入方式**:原生 Native SDK + MethodChannel `com.xiaopaix.app/ads`(无变化)

### iOS Podfile

```ruby
pod 'Ads-CN'
```

### MethodChannel 调用

| 方法名 | 参数 | 说明 |
|-------|-----|-----|
| `initPangle` | `{appId}` | 初始化 SDK(AppId 从 MMKV 读) |
| `showCSJSplashAd` | `{slotId, timeoutMs: 3000}` | 开屏广告,3s 超时自动降级 |
| `loadRewardAd` | `{slotId}` | 预加载激励视频 |
| `showRewardAd` | `{slotId}` | 展示激励视频 |
| `requestATT` | — | 请求 iOS 14+ 追踪授权(只弹一次) |
| `getSdkVersion` | — | 返回 SDK 版本号 |

> ⚠️ **AppId 和 SlotId 由后端 `/app/start` 接口动态下发**(`ad_config.app_ids.csj`),保存到 MMKV 后下次启动生效,**禁止硬编码**。

### ⚠️ 注意

- iOS 13+ 才支持 `Ads-CN`,需在 Podfile 设置 `platform :ios, '13.0'`
- ATT 授权结果记录到 `StorageKeys.attRequested`,只弹一次
- 开屏广告 3s 超时内若未展示成功自动降级,不阻塞应用进入

---

## 十四、腾讯广告联盟 · GDT

**接入方式**:原生 Native SDK + MethodChannel(与穿山甲共享 `com.xiaopaix.app/ads`)

### iOS Podfile

```ruby
pod 'GDTMobSDK'
```

### MethodChannel 调用

| 方法名 | 参数 | 说明 |
|-------|-----|-----|
| `initGDT` | `{appId}` | 初始化(AppId 从 MMKV 读) |
| `showGDTSplashAd` | `{slotId, timeoutMs}` | 开屏广告 |
| `showGDTRewardAd` | `{slotId}` | 激励视频(load + show 合并) |
| `getGDTSdkVersion` | — | 返回 SDK 版本 |

### ⚠️ 注意

- GDT 的 ATT 复用穿山甲的 `requestATT()`,无需单独处理
- AppId 同样由 `/app/start` 下发,`ad_config.app_ids.gdt` 字段

---

## 十五、广告瀑布流架构说明

`/app/start` 接口返回的 `ad_config` 完整结构:

```json
{
  "ad_config": {
    "app_ids": {
      "csj": "穿山甲_AppId",
      "gdt": "腾讯_AppId"
    },
    "strategies": {
      "splash": [
        {"source": "self",  "priority": 1, "image_url": "...", "target_url": "...", "duration": 5},
        {"source": "csj",   "priority": 2, "id": "slot_xxx"},
        {"source": "gdt",   "priority": 3, "id": "slot_yyy"}
      ],
      "reward_video": [
        {"source": "csj", "priority": 1, "id": "slot_aaa"},
        {"source": "gdt", "priority": 2, "id": "slot_bbb"}
      ]
    }
  }
}
```

**执行逻辑**(`AdWaterfallManager.runSplashAd`):

1. 按 `priority` 顺序依次尝试每个广告源
2. 某源超时(>3s)/ 失败 → 自动降级到下一源
3. 全部失败 → 触发 `onDismiss` 回调,跳过广告

**初始化时机**:`SplashPage` 中读取 MMKV 缓存的 `ad_config`,按 `app_ids` 初始化对应 SDK,再展示广告。首次冷启动无缓存 → 跳过广告,进入后台请求 `/app/start` 更新配置,**下次启动生效**。

---

## 十六、图片/相机选择器

**版本**:`wechat_assets_picker ^10.1.1` + `wechat_camera_picker ^4.4.0` + `image_cropper ^11.0.0` + `flutter_image_compress ^2.4.0`

> ⚠️ `wechat_assets_picker` 从 `^9.4.1` 升至 `^10.1.1`,`wechat_camera_picker` 从 `^4.3.0` 升至 `^4.4.0`,请检查 API 兼容性。

### iOS Info.plist

```xml
<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问相册以选择图片</string>
<key>NSCameraUsageDescription</key>
<string>需要使用相机拍摄照片</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要使用麦克风录制视频</string>
```

### 图片压缩规则

```dart
// >200KB 时压缩,quality=80,输出 JPEG
if (fileSize > 200 * 1024) {
  await FlutterImageCompress.compressAndGetFile(path, targetPath, quality: 80);
}
```

### ⚠️ 注意

- 选择器返回 `AssetEntity`,需调用 `.file` 转换为 `File`
- 压缩后的临时文件存 `getTemporaryDirectory()`,上传完成后删除
- OSS 预签名下载:用纯净 `Dio()`,**不带 Authorization 头**

---

## 十七、音频录制 · record

**版本**:`^6.2.0`(无 breaking changes)

### ⚠️ 注意

- 录制返回 `.m4a` 文件路径,上传前直接用原始文件(无需压缩)
- 播放使用 `just_audio ^0.10.5`(从 `^0.9.x` 升级,请确认 API 兼容性)
- `AudioPlayer` 在 `dispose` 时必须 `await player.dispose()`,否则内存泄漏

---

## 十八、权限管理 · permission_handler

**版本**:`^12.0.1`(无 breaking changes)

### iOS Podfile(必须声明所需权限,否则编译报错)

```ruby
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
  '$(inherited)',
  'PERMISSION_NOTIFICATIONS=1',
  'PERMISSION_MICROPHONE=1',
  'PERMISSION_CAMERA=1',
  'PERMISSION_PHOTOS=1',
]
```

> ⚠️ **只声明实际使用的权限**,声明未使用的权限会导致 App Store 审核被拒

---

## 十九、iOS Podfile 通用配置模板

```ruby
source 'https://github.com/CocoaPods/Specs.git'
source 'https://github.com/aliyun/aliyun-specs.git'   # 阿里云 SDK 必须

platform :ios, '13.0'
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
ENV['FLUWX_SKIP_SETUP'] = 'true'    # 微信 SDK,跳过 Ruby setup 脚本
ENV['TOBIAS_SKIP_SETUP'] = 'true'   # 支付宝 SDK,跳过 Ruby setup 脚本

target 'Runner' do
  use_frameworks!

  # 广告 SDK(穿山甲 + GDT)
  pod 'Ads-CN'
  pod 'GDTMobSDK'

  # 阿里云
  pod 'AlicloudPush', '~> 3'
  pod 'AlicloudFeedback', '~> 3.4.2'

  # 友盟(modular_headers 解决头文件找不到问题)
  pod 'UMCommon', :modular_headers => true
  pod 'UMDevice', :modular_headers => true

  # 微信(modular_headers 解决头文件找不到问题)
  pod 'WechatOpenSDK-XCFramework', :modular_headers => true

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end

post_install do |installer|
  installer.pods_project.build_configurations.each do |config|
    config.build_settings.delete 'VALID_ARCHS'
    config.build_settings['ARCHS'] = 'arm64 x86_64'
  end

  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)

    # 中和阿里云 SDK 的 Strip 脚本(否则 CI 打包失败)
    target.shell_script_build_phases.each do |phase|
      if phase.name.include?("Strip") || phase.name.include?("archs")
        phase.shell_script = "echo 'neutralized'"
      end
    end

    target.build_configurations.each do |config|
      config.build_settings.delete 'VALID_ARCHS'
      config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
      config.build_settings['ENABLE_BITCODE'] = 'NO'
      config.build_settings['GCC_WARN_INHIBIT_ALL_WARNINGS'] = 'YES'
      config.build_settings['CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES'] = 'YES'

      # 友盟 + 微信头文件搜索路径
      if ['fluwx', 'umeng_common_sdk'].include?(target.name)
        config.build_settings['HEADER_SEARCH_PATHS'] ||= '$(inherited) '
        config.build_settings['HEADER_SEARCH_PATHS'] << '"${PODS_ROOT}/Headers/Public/UMCommon" '
        config.build_settings['HEADER_SEARCH_PATHS'] << '"${PODS_ROOT}/Headers/Public/UMDevice" '
        config.build_settings['HEADER_SEARCH_PATHS'] << '"${PODS_ROOT}/Headers/Public/WechatOpenSDK-XCFramework" '
      end

      # permission_handler 权限声明
      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
        '$(inherited)',
        'PERMISSION_NOTIFICATIONS=1',
        'PERMISSION_MICROPHONE=1',
        'PERMISSION_CAMERA=1',
        'PERMISSION_PHOTOS=1',
      ]
    end
  end
end
```

---

## 二十、AppConfig 统一配置说明

所有 SDK Key 通过 `--dart-define-from-file=env.json` 注入,开发/生产分别用不同文件:

```json
// env.prod.json(加入 .gitignore,绝对不提交代码仓库)
{
  "isProd": "true",
  "API_BASE_URL_PROD": "https://api.tongban.wang/",
  "APP_KEY_IOS_PROD": "your_ios_prod_key",
  "APP_KEY_ANDROID_PROD": "your_android_prod_key",
  "APP_SECRET_PROD": "your_prod_secret",
  "WECHAT_APP_ID": "wx146a6f748a99a0da",
  "WECHAT_UNIVERSAL_LINK": "https://www.tongban.wang/app/",
  "ALIPAY_URL_SCHEME": "com.xiaopai.match",
  "UMENG_APP_KEY_IOS": "xxx",
  "UMENG_APP_KEY_ANDROID": "xxx",
  "ALICLOUD_PUSH_APP_KEY_IOS": "xxx",
  "ALICLOUD_PUSH_APP_SECRET_IOS": "xxx"
}
```

**运行命令**:

```bash
flutter run --dart-define-from-file=env.dev.json
flutter build ipa --dart-define-from-file=env.prod.json
flutter build apk --dart-define-from-file=env.prod.json
```

> ⚠️ `env.*.json` 加入 `.gitignore`,**绝对不提交到代码仓库**

---

## 二十一、动画增强 · flutter_animate

**版本**:`^4.5.0`(无 breaking changes)

```dart
// 链式调用:淡入 + 平移
Text("Hello").animate()
  .fadeIn(duration: 500.ms)
  .slideY(begin: 1, end: 0);

// 列表交错动画
Column(
  children: [...].animate(interval: 400.ms).fade().slideX(),
)
```

---

## 二十一B、骨架屏 · skeletonizer 2.x

**版本**:`^2.1.3`(⚠️ 必须 2.x,1.x 不支持 Flutter 3.29+ 新 Canvas API)

### ⚠️ Flutter 3.29+ 兼容问题

Flutter 3.29 在 `Canvas` 接口新增了 `clipRSuperellipse` / `drawRSuperellipse` 方法。
`skeletonizer 1.x` 的 `SkeletonizerCanvas` 和 `UnitingCanvas` 均实现了 `Canvas`,但缺少这两个方法,导致编译报错:

```
Error: The non-abstract class 'SkeletonizerCanvas' is missing implementations for:
 - Canvas.clipRSuperellipse
 - Canvas.drawRSuperellipse
```

**解决方案**:将 `pubspec.yaml` 中 `skeletonizer` 从 `^1.4.x` 升至 `^2.1.3`。

### 命名冲突处理

`ShimmerEffect` 在 `flutter_animate` 和 `skeletonizer` 中都有定义,需在 `shared.dart` 中用 `hide` 规避:

```dart
// shared.dart — skeletonizer 的 ShimmerEffect 优先
export 'package:flutter_animate/flutter_animate.dart' hide ShimmerEffect;
export 'package:skeletonizer/skeletonizer.dart';
```

### 用法(通过 AppSkeleton 封装,禁止直接使用 Skeletonizer)

```dart
// ✅ 推荐:包裹真实 Widget,enabled 开关切换骨架/正常状态
AppSkeleton.wrap(
  enabled: isLoading,
  child: UserCard(user: isLoading ? User.empty() : user),
)

// ✅ 推荐:loading 分支中使用
AppSkeleton.fromWidget(
  child: _buildList(List.generate(5, (_) => User.empty())),
)

// 兜底:无法复用真实 Widget 时
AppSkeleton.listTile()       // 列表骨架(头像 + 两行文字)
AppSkeleton.card()           // 卡片骨架
AppSkeleton.circle(size: 48) // 圆形骨架

// ❌ 禁止:直接使用 Skeletonizer,绕过封装
Skeletonizer(child: ...)
```

### ShimmerEffect 自定义色

```dart
AppSkeleton.wrap(
  enabled: isLoading,
  // 默认已配置 AppColors.divider / bgSecondary,无需手动传 effect
  child: ...,
)
```

---

## 二十二、代码生成 · freezed 3.x + json_serializable

**版本**:`freezed ^3.2.5` + `freezed_annotation ^3.1.0` + `json_serializable ^6.11.2` + `json_annotation ^4.11.0`

### ⚠️ freezed 3.x 升级注意

freezed 3.x 的 `@freezed` class **语法与 2.x 兼容**,但生成的 mixin 内部结构有变化。升级后必须重新生成:

```bash
# ⚠️ flutter pub run 已废弃,改用 dart run
dart run build_runner build --delete-conflicting-outputs
```

### 数据模型写法(语法兼容)

```dart
@freezed
class UserState with _$UserState {
  const factory UserState({
    required String id,
    @Default(false) bool isLoading,
    String? nickname,
  }) = _UserState;

  factory UserState.fromJson(Map<String, dynamic> json) =>
      _$UserStateFromJson(json);
}

// 使用:state.copyWith(isLoading: true) 即可
```

### ⚠️ json_serializable 6.11 注意

`json_serializable ^6.11.2` 要求 `sdk: '>=3.8.0'`,pubspec.yaml 中 `environment.sdk` 必须设置:

```yaml
environment:
  sdk: '>=3.8.0 <4.0.0'
```

---

## 二十三、开发钩子 · flutter_hooks

**版本**:`flutter_hooks ^0.21.3` + `hooks_riverpod ^3.3.1`

### ⚠️ hooks_riverpod 3.x

`hooks_riverpod` 跟随 `riverpod` 升至 3.x,`HookConsumerWidget` API 无变化:

```dart
class LoginScreen extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 自动处理初始化与销毁,不需要 dispose
    final phoneCtrl = useTextEditingController();
    final scrollCtrl = useScrollController();

    return TextField(controller: phoneCtrl);
  }
}
```

- `useMemoized` / `useCallback` 必须正确传入依赖项,否则会导致 reload loop

---

## 二十四、样式框架 · VelocityX

**版本**:`^4.0.0`(无 breaking changes)

```dart
"Hello World".text.red500.bold.make().p16().box.rounded.shadow.make();
// 等同于 Container(Padding(Text(style: ...)))
```

> ⚠️ 禁止使用 VelocityX 内置色(`.red500` 等)替代主题色,必须从 `AppColors` 取色

---

## 二十五、色彩系统 · flex_color_scheme

**版本**:`^8.4.0`(无 breaking changes)

```dart
class AppTheme {
  static ThemeData get light => FlexThemeData.light(
    colors: FlexSchemeColor.from(primary: AppColors.primary),
    useMaterial3: true,
  );

  static ThemeData get dark => FlexThemeData.dark(
    colors: FlexSchemeColor.from(primary: AppColors.primary),
    useMaterial3: true,
  );
}
```

---

## 二十六、调试神器 · talker 5.x

**版本**:`talker_flutter ^5.1.15` + `talker_dio_logger ^5.1.15` + `talker_riverpod_logger ^5.1.14`

> ⚠️ talker 从 `^4.x` 升至 `^5.x`,**`TalkerRiverpodObserver` 构造函数参数名变化**:

```dart
// ❌ talker 4.x 旧写法
TalkerRiverpodObserver(talker)

// ✅ talker 5.x 新写法(参数改为命名参数)
TalkerRiverpodObserver(talker: talker)
```

### 三件套联动接入

```dart
// main.dart
final talker = TalkerFlutter.init();

runApp(
  ProviderScope(
    observers: [TalkerRiverpodObserver(talker: talker)],  // ← 命名参数
    child: const App(),
  ),
);

// app_router.dart
GoRouter(
  observers: [TalkerRouteObserver(talker)],
  ...
)

// dio_client.dart
dio.interceptors.add(TalkerDioLogger(talker: talker));
```

### 功能特色

- **自动全链路监控**:路由跳转、Provider 重建、网络请求、异常均自动记录,无需手动打 log
- **应用内日志面板**:Debug 模式下悬浮按钮呼出 `TalkerScreen`,脱离电脑排查问题
- **日志分级**:Release 自动关闭 verbose / debug 级别

---

## 二十七、运行环境安全检测 · freerasp 7.x

**版本**:`^7.5.0`(从 `^6.x` 升级,有重要 breaking change)

### ⚠️ freerasp 7.x 启动阻塞问题(新增)

`Talsec.instance.start()` 在某些机型或 freerasp 7.x 版本下可能**永久阻塞**,导致 App 第二次启动黑屏(`main()` 卡死)。**必须加超时兜底**:

```dart
// security_adapter.dart — 强制加 timeout
@override
Future<void> initialize() async {
  try {
    final config = TalsecConfig(
      androidConfig: AndroidConfig(
        packageName: 'com.xiaopai.match',
        signingCertHashes: ['your_cert_hash'],
        supportedStores: ['com.android.vending'],
      ),
      iosConfig: IOSConfig(
        bundleIds: ['com.xiaopai.match'],
        teamId: 'your_team_id',
      ),
      watcherMail: 'security@yourapp.com',
    );

    await Talsec.instance.start(config).timeout(
      const Duration(seconds: 5),
      onTimeout: () {
        talker.warning('freerasp start timeout, continuing silently');
      },
    );
  } catch (e, st) {
    // 启动失败不阻塞 App,静默记录
    talker.error('freerasp init error', e, st);
  }
}
```

### 风控处理最佳实践

- **不要在本地弹出拦截提示**:引导黑客研究绕过
- **静默标记**:在 Dio 请求 Header 中带上 `X-Device-Risk-Flag: 1`
- **后端降权**:服务器收到标记后对该账号采取降权措施

### 其余检测包

- **device_info_plus `^12.3.0`**(从 `^10.1.0` 升级):基础硬件特征排查,API 向后兼容
- **flutter_udid `^4.1.2`**(从 `^3.0.0` 升级):设备唯一硬件 ID,API 向后兼容

```dart
// flutter_udid 4.x 用法不变
final udid = await FlutterUdid.udid;
```

---

## 二十八、iOS 2024 隐私合规 (Privacy Manifest)

无变化,步骤同旧版:

1. Runner 目录下新建 `PrivacyInfo.xcprivacy`
2. 添加 **Collected Data Types** 和 **Accessed API Types**(至少包含 `NSPrivacyAccessedAPICategoryFileTimestamp` 和 `NSPrivacyAccessedAPICategoryUserDefaults`)
3. 确保该文件勾选了 `Runner` 为 Target

---

## 二十九、密钥安全进阶 · envied 1.x

**版本**:`envied ^1.3.3` + `envied_generator ^1.3.3`(从 `^0.5.4` 重大升级)

### ⚠️ envied 1.x Breaking Changes

1. **`@Envied(path:)` 现在可以直接读取 JSON 文件**(不只是 `.env` 文本文件):

```dart
// lib/env.dart
import 'package:envied/envied.dart';
part 'env.g.dart';

// ✅ envied 1.x:path 指向 JSON 配置文件
@Envied(path: 'env.dev.json', obfuscate: true)
abstract class Env {
  @EnviedField(varName: 'APP_SECRET_DEV')
  static final String appSecret = _Env.appSecret;

  @EnviedField(varName: 'WECHAT_APP_ID')
  static final String wechatAppId = _Env.wechatAppId;

  @EnviedField(varName: 'ALICLOUD_PUSH_APP_KEY_IOS')
  static final String alicloudPushKeyIos = _Env.alicloudPushKeyIos;
}
```

2. 生成命令(必须用 `dart run`):

```bash
dart run build_runner build --delete-conflicting-outputs
```

3. `lib/env.g.dart` 加入 `.gitignore`(生成文件含混淆字节码,不提交)

### ⚠️ 注意

- `obfuscate: true` 是核心,将密钥打散成字节码,防止逆向
- `.env.dev.json`、`.env.prod.json`、`lib/env.g.dart` 全部加入 `.gitignore`
- CI/CD 构建时动态生成 `env.prod.json`,代码库只保留抽象类

---

## 三十、IM 与推送补充:iOS 后台模式

无变化:

1. Xcode → `Signing & Capabilities` → `+ Capability` → `Background Modes`
2. 必须勾选:**Background fetch** + **Remote notifications**

---

## 三十一、国际化 (i18n) 规范说明

无变化:

```yaml
# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_zh.arb
output-localization-file: app_localizations.dart
```

```bash
flutter gen-l10n
```

---

## 三十二、架构进阶:SDK 深度联动

talker 5.x + Riverpod 3.x + go_router 17.x 三者联动,实现全链路自动化监控:

```dart
// main.dart
final talker = TalkerFlutter.init();

runApp(
  ProviderScope(
    observers: [TalkerRiverpodObserver(talker: talker)],  // ← talker 5.x 命名参数
    child: const App(),
  ),
);

// app_router.dart
@riverpod
GoRouter router(Ref ref) {   // ← Riverpod 3.x 通用 Ref
  return GoRouter(
    observers: [TalkerRouteObserver(talker)],
    redirect: (context, state) async { ... },   // ← go_router 17.x async
    routes: [...],
  );
}

// dio_client.dart
dio.interceptors.add(TalkerDioLogger(talker: talker));
```

无需手动打 log,路由跳转、Provider 重建、网络请求、异常均自动记录。

---

## 三十三、安全加固:密钥保护深度实践

### 1. envied 1.x 安全收尾

- **必加 GitIgnore**:`.env.dev.json`、`.env.prod.json`、`lib/env.g.dart` 全部加入 `.gitignore`
- **动态生成**:代码库仅保留 `env.dart` 抽象类,JSON 配置文件在本地或 CI/CD 构建时动态提供

### 2. Native 层密钥保护

- 尽量通过 MethodChannel 将 Dart 层 `Env` 中的密钥传递给原生层
- `build.gradle` / `AppDelegate.swift` 中禁止硬编码生产密钥

---

## 常见编译错误速查

| 错误 | 原因 | 解决方案 |
|-----|------|---------|
| `Undefined class 'XxxRef'`(如 `MainDioRef`、`RouterRef`) | Riverpod 3.x 废除了独立 Ref 类型 | 将所有 `XxxRef` 替换为通用 `Ref` |
| `redirect must return FutureOr<String?>` | go_router 17.x redirect 签名变化 | 给 `redirect` 函数加 `async` |
| `sdk constraint mismatch` | pubspec.yaml sdk 约束太低 | 改为 `sdk: '>=3.8.0 <4.0.0'` |
| `SkeletonizerCanvas missing clipRSuperellipse / drawRSuperellipse` | skeletonizer 1.x 不支持 Flutter 3.29+ 新 Canvas API | 升级 `skeletonizer` 到 `^2.1.3` |
| `TalkerRiverpodObserver positional argument` | talker 5.x 参数改为命名参数 | 改为 `TalkerRiverpodObserver(talker: talker)` |
| `第二次启动黑屏` | freerasp 7.x `start()` 永久阻塞 | 加 `.timeout(Duration(seconds: 5))` + try-catch |
| `'UMCommon/UMConfigure.h' file not found` | 友盟头文件路径未注入 | Podfile 加 `:modular_headers => true` + 搜索路径注入 |
| `Command PhaseScriptExecution failed (AlicloudUtils)` | 阿里云 Strip 脚本失败 | `post_install` 中 neutralize 含 Strip 的 shell script |
| `OpenIM Login Error 10004/10005` | IM Token 过期 | 刷新 imToken,重新调用 `login()` |
| `OSS 签名 URL 请求 403` | 用 Auth Dio 请求预签名 URL | 使用纯净 `Dio()` 不带 Authorization 头 |
| `GoRouter redirect loop` | 路由守卫逻辑错误 | 检查 `redirect` 里对 `splash` / `privacy` 路由的放行条件 |
| `MMKV crash on first launch` | `MMKV.initialize()` 未在最前面调用 | 确保在任何 MMKV 读写前 `await MMKV.initialize()` |
| `flutter_hooks reload loop` | `useMemoized` 忘记传 keys | 确保 `useMemoized` / `useCallback` 正确传入依赖项 |
| `flutter pub run build_runner Deprecated` | 旧命令已废弃 | 改用 `dart run build_runner build --delete-conflicting-outputs` |
| `pod install Ruby 报错 (fluwx/tobias)` | fluwx/tobias 自带 Ruby setup 脚本失败 | Podfile 顶部加 `ENV['FLUWX_SKIP_SETUP']='true'` 和 `ENV['TOBIAS_SKIP_SETUP']='true'` |
| `riverpod 版本冲突` | 三包版本不一致 | `riverpod_annotation ^4.0.2` + `hooks_riverpod ^3.3.1` + `riverpod_generator ^4.0.3` 一起升 |

---

*文档版本:v2.1 · 基于 Flutter 3.41.x / Dart 3.11.x · 2026-03 · 新增 skeletonizer 2.x、image_gallery_saver_plus、remixicon 1.4*