提示信息
# 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*