1. 新增TDashboard翻译类型与多语言文案 2. 完善macOS权限管理与Impeller渲染适配 3. 更新服务器部署配置与协议文件上传脚本 4. 修复翻译导入服务与根类型编译问题
1227 lines
50 KiB
Markdown
1227 lines
50 KiB
Markdown
# iOS / macOS 开发者指南
|
||
|
||
> 本项目同时支持 iOS、macOS、Android、鸿蒙(HarmonyOS) 四端。
|
||
> 鸿蒙端使用定制 Flutter SDK(`flutter-ohos`),在 `TargetPlatform` 枚举中新增了 `TargetPlatform.ohos`,
|
||
> 并对大量三方库做了本地化适配。iOS/macOS 开发者需了解以下关键事项,避免踩坑。
|
||
|
||
---
|
||
|
||
## 文档更新日志
|
||
|
||
| 日期 | 版本 | 变更内容 |
|
||
|---|---|---|
|
||
| 2026-06-26 | v14 | **macOS 权限动态申请**:①新建 `PermissionManager.swift` 原生权限管理器(AVFoundation/Photos/UserNotifications),通过 MethodChannel 暴露给 Flutter;②AppDelegate 新增 `checkPermission`/`requestPermission`/`openPermissionSettings` 三个 channel 方法;③MacosPlatformService 新增权限管理方法;④PermissionService macOS 分支改为调用原生 API,实现相机/麦克风/相册/通知权限的动态申请(替代 permission_handler_apple 无 macOS 实现的问题);⑤AppPermission 枚举新增 `macosPermissionName` getter |
|
||
| 2026-06-26 | v13 | **macOS Impeller 开关修复**:①修复「通用设置 → Impeller 渲染引擎」开关不生效的严重 bug(原 `setenv("FLUTTER_ENGINE_SWITCH_0")` 方式 macOS 桌面 embedder 不读取,改用 `FlutterDartProject.commandLineArguments` 传递 `--enable-impeller`/`--no-enable-impeller`);②x86_64 端开启 Impeller 前增加二次确认警告对话框(说明 Intel Mac 上的 Metal 驱动渲染资源累积风险);③Apple Silicon (arm64) 在重启对话框中显示「推荐开启」提示卡片;④新增 5 个翻译键(覆盖全部 14 种语言) |
|
||
| 2026-06-26 | v12 | **macOS App Store 审核修复**:①flutter_webrtc 1.4.0→1.5.2(对齐 WebRTC-SDK 144.7559.09,解决 CocoaPods 版本冲突);②新增 §2.8.9 flutter_webrtc 特殊包说明;③新增 §2.8.10 permission_handler_apple macOS 缺失实现说明(macOS 端权限改由 entitlement + Info.plist 自动管理);④新增 §6.x macOS entitlement 与权限管理适配说明 |
|
||
| 2026-06-15 | v11 | 同步三方库升级:删除custom_lint/riverpod_lint;新增analyzer/test_api/test/xml/pointycastle overrides;record降级到^6.2.1(7.0.0需Dart3.12+);更新差异对照表和dependency_overrides行数 |
|
||
| 2026-06-07 | v10 | 修正 §2.3 dependency_overrides 行数(4→5行/40+→46行);修正 §2.6 补丁引用(§2.8→§2.9);简化 §2.8.1 pro_image_editor 过时回退建议;删除 §5.4 pro_image_editor 本地包条目和 bitsdojo_window 废弃条目;简化 §3.3 pubspec.yaml 处理策略(git stash → 双模板脚本生成);更新 §3.2/§3.5/§6 与双模板机制对齐 |
|
||
|
||
|
||
---
|
||
|
||
## 一、环境准备与项目拉取
|
||
|
||
### 1.1 Flutter SDK 选择
|
||
|
||
| 平台 | 推荐SDK | 说明 |
|
||
|---|---|---|
|
||
| iOS / macOS | **官方 Flutter SDK** | 标准 stable/beta 渠道即可 |
|
||
| 鸿蒙 | flutter-ohos 定制SDK | 包含 `TargetPlatform.ohos` 等扩展,仅在鸿蒙开发机上使用 |
|
||
| Android | 官方 Flutter SDK | 同 iOS |
|
||
|
||
> ⚠️ **iOS/macOS 开发使用官方 Flutter SDK 即可**,无需安装鸿蒙定制SDK。
|
||
> 不要用鸿蒙SDK编译iOS/macOS,反之亦然,SDK不兼容会导致编译错误。
|
||
|
||
### 1.2 克隆仓库
|
||
|
||
```bash
|
||
# 克隆项目仓库
|
||
git clone <仓库URL> xianyan
|
||
cd xianyan
|
||
|
||
# 查看当前分支
|
||
git branch -a
|
||
|
||
# 切换到开发分支(如需)
|
||
git checkout feature/xxx
|
||
# 或直接在 main 分支开发
|
||
git checkout main
|
||
```
|
||
|
||
### 1.3 生成 pubspec.yaml 并安装依赖
|
||
|
||
> ⚠️ **`pubspec.yaml` 不再提交到 Git**。项目使用双模板机制:
|
||
> - `pubspec.ohos.yaml` — 鸿蒙端模板(使用本地 packages/ 目录)
|
||
> - `pubspec.macos.yaml` — MacBook Pro 端模板(使用远程版本号)
|
||
> - `pubspec.yaml` — 由脚本自动生成,已加入 `.gitignore`
|
||
|
||
```bash
|
||
# 1. 确认使用官方 Flutter SDK
|
||
flutter --version # 应显示官方版本,非 flutter-ohos
|
||
|
||
|
||
|
||
# 3. 获取依赖
|
||
flutter pub get
|
||
|
||
# 4. iOS 编译验证
|
||
flutter build ios --no-codesign
|
||
|
||
# 5. macOS 编译验证
|
||
flutter build macos
|
||
```
|
||
|
||
鸿蒙端开发者:
|
||
```bash
|
||
# 1. 确认使用 flutter-ohos SDK
|
||
flutter --version # 应显示 ohos 版本
|
||
|
||
|
||
|
||
# 3. 获取依赖
|
||
flutter pub get
|
||
```
|
||
|
||
---
|
||
|
||
## 二、pubspec.yaml 双模板机制
|
||
|
||
### 2.1 架构概述
|
||
|
||
> **`pubspec.yaml` 不再提交到 Git**,两端各自维护独立的模板文件。
|
||
|
||
```
|
||
项目根目录/
|
||
├── pubspec.ohos.yaml ← 鸿蒙端模板(tracked,使用本地 packages/ 目录)
|
||
├── pubspec.macos.yaml ← MacBook Pro 端模板(tracked,使用远程版本号)
|
||
├── pubspec.yaml ← 本地生成文件(.gitignore,不提交)
|
||
└── tools/
|
||
└── setup_pubspec.ps1 ← 自动生成脚本
|
||
```
|
||
|
||
**为什么这样做?**
|
||
|
||
之前鸿蒙端和 MacBook Pro 端共用一个 `pubspec.yaml`,MacBook Pro 端每次 `git pull` 后需要手动替换 82 行本地包引用,容易出错且经常互相覆盖。
|
||
|
||
现在:
|
||
- 鸿蒙端模板 `pubspec.ohos.yaml` 包含所有 `path: packages/` 引用
|
||
- MacBook Pro 端模板 `pubspec.macos.yaml` 使用远程版本号
|
||
- `pubspec.yaml` 由脚本自动生成,两边都不提交
|
||
|
||
### 2.2 MacBook Pro 端的核心原则
|
||
|
||
> **MacBook Pro 端不需要 `packages/` 目录,直接使用远程三方库即可。**
|
||
|
||
`pubspec.macos.yaml` 已将所有本地包引用替换为远程版本号,无需手动修改。
|
||
|
||
**为什么不能直接用鸿蒙端的本地包?**
|
||
|
||
本地包中包含 `TargetPlatform.ohos` 引用(鸿蒙SDK新增的枚举值),官方 SDK 没有此值,会导致编译报错:
|
||
|
||
```
|
||
Error: The getter 'ohos' isn't defined for the class 'TargetPlatform'
|
||
```
|
||
|
||
受影响的本地包有 5 个:
|
||
|
||
| 包名 | 引用次数 | 引用类型 |
|
||
|---|---|---|
|
||
| flex_color_picker | 2 | `case TargetPlatform.ohos:` (switch) |
|
||
| flutter_quill | 2 | `case TargetPlatform.ohos:` (switch) |
|
||
| flutter_local_notifications | 8 | `== TargetPlatform.ohos` (if比较) |
|
||
| mobile_scanner | 3 | `== / != TargetPlatform.ohos` (if比较) |
|
||
| audioplayers | 1 | `!= TargetPlatform.ohos` (if比较) |
|
||
|
||
**解决方案**:MacBook Pro 端使用远程版本,远程版本不含 `TargetPlatform.ohos`,编译正常。
|
||
|
||
### 2.3 双模板差异对照表
|
||
|
||
| 区域 | pubspec.ohos.yaml(鸿蒙端) | pubspec.macos.yaml(MacBook Pro端) |
|
||
|---|---|---|
|
||
| shared_preferences | `path: packages/shared_preferences` | `^2.5.5` |
|
||
| flutter_secure_storage | `path: packages/flutter_secure_storage` | `^10.3.0` |
|
||
| hive_ce_flutter | `path: packages/hive_flutter` | `^2.3.4` |
|
||
| path_provider | `path: packages/path_provider` | `^2.1.5` |
|
||
| package_info_plus | `path: packages/package_info_plus` | `^10.1.0` |
|
||
| connectivity_plus | `path: packages/connectivity_plus` | `^7.1.1` |
|
||
| device_info_plus | `path: packages/device_info_plus` | `^13.1.0` |
|
||
| permission_handler | `path: packages/permission_handler` | `^12.0.1` |
|
||
| app_tracking_transparency | `^2.0.6` | `^2.0.6` |
|
||
| flutter_local_notifications | `path: packages/flutter_local_notifications` | `^22.0.0` |
|
||
| url_launcher | `path: packages/url_launcher` | `^6.3.2` |
|
||
| app_links | `path: packages/app_links` | `^7.0.0` |
|
||
| home_widget | `git: gitcode.com/...` | `^0.9.3` |
|
||
| file_picker | `path: packages/file_picker` | `^12.0.0-beta.5` |
|
||
| image_picker | `path: packages/image_picker` | `^1.2.2` |
|
||
| share_plus | `path: packages/share_plus` | `^13.1.0` |
|
||
| gal | `path: packages/gal` | `^2.3.0` |
|
||
| flutter_quill | `path: packages/flutter_quill` | `^11.5.0` |
|
||
| flex_color_picker | `path: packages/flex_color_picker` | `^3.8.0` |
|
||
| flutter_image_compress | `path: packages/flutter_image_compress` | `^2.4.0` |
|
||
| wakelock_plus | `path: packages/wakelock_plus` | `^1.6.0` |
|
||
| audioplayers | `path: packages/audioplayers` | `^6.5.0` |
|
||
| record | `path: packages/record` | `^6.2.1` |
|
||
| video_compress | `path: packages/video_compress` | `^3.1.4` |
|
||
| video_player | `path: packages/video_player` | `^2.11.0` |
|
||
| local_auth | `path: packages/local_auth` | `^3.0.1` |
|
||
| battery_plus | `path: packages/battery_plus` | `^7.0.0` |
|
||
| network_info_plus | `path: packages/network_info_plus` | `^8.1.0` |
|
||
| flutter_webrtc | `path: packages/flutter_webrtc` | `^1.5.2` |
|
||
| mobile_scanner | `path: packages/mobile_scanner` | `^7.2.0` |
|
||
| wifi_iot | `path: packages/wifi_iot` | `^0.3.19` |
|
||
| nearby_service | `path: packages/nearby_service` | `^0.2.1` |
|
||
| sqflite | `path: packages/sqflite` | `^2.4.1` |
|
||
| workmanager | `path: packages/workmanager` | `^0.9.0` |
|
||
| flutter_tts | `path: packages/flutter_tts` | `^4.2.5` |
|
||
| speech_to_text | `path: packages/speech_to_text` | `^7.4.0` |
|
||
| live_activities | `path: packages/live_activities` | `^2.4.9` |
|
||
| dependency_overrides | 49 行(含本地包覆盖 + ohos 子包 + analyzer/test_api/test/xml/pointycastle) | 10 行(版本号覆盖 + win32 + quill_native_bridge_windows + analyzer/test_api/test/xml/pointycastle) |
|
||
|
||
### 2.4 ⚠️ 新增三方库变更流程(必读)
|
||
|
||
> **铁律:新增三方库时,必须同时更新两个模板 + 本文档。**
|
||
|
||
```
|
||
新增三方库流程:
|
||
1. 在 pubspec.ohos.yaml 添加依赖(鸿蒙端优先)
|
||
2. 在 pubspec.macos.yaml 添加对应远程版本
|
||
3. 在本文档 §2.3 差异对照表添加一行
|
||
4. 在本文档顶部更新日志记录变更
|
||
5. 在 CHANGELOG.md 记录变更
|
||
6. git push 后通知另一端开发者
|
||
```
|
||
|
||
**鸿蒙端新增本地化包时额外步骤:**
|
||
- 将包放入 `packages/` 目录
|
||
- 在 `pubspec.ohos.yaml` 的 `dependency_overrides` 中添加覆盖
|
||
- 在 `pubspec.macos.yaml` 中使用远程版本号
|
||
- 更新本文档 §2.3 差异对照表
|
||
|
||
**升级三方库版本时:**
|
||
- 在对应模板中更新版本号
|
||
- 如果是鸿蒙本地包升级,同步更新 `pubspec.ohos.yaml` 中的版本注释
|
||
- 更新本文档 §2.3 差异对照表中的版本号
|
||
|
||
### 2.5 setup_pubspec.ps1 脚本说明
|
||
|
||
```powershell
|
||
# 鸿蒙端
|
||
.\tools\setup_pubspec.ps1 -Platform ohos
|
||
|
||
# MacBook Pro 端
|
||
.\tools\setup_pubspec.ps1 -Platform macos
|
||
|
||
# 自动检测(根据 flutter --version 或 packages/ 目录是否存在)
|
||
.\tools\setup_pubspec.ps1
|
||
```
|
||
|
||
脚本功能:
|
||
- 将对应模板复制为 `pubspec.yaml`
|
||
- 自动备份现有 `pubspec.yaml` → `pubspec.yaml.bak`
|
||
- 验证模板内容(鸿蒙端检查 `path: packages/`,MacBook Pro 端检查无本地包引用)
|
||
- 输出后续操作提示
|
||
|
||
### 2.6 MacBook Pro 端完整操作流程
|
||
|
||
```bash
|
||
# 1. 克隆仓库
|
||
git clone <仓库URL> xianyan
|
||
cd xianyan
|
||
|
||
# 2. 生成 pubspec.yaml
|
||
.\tools\setup_pubspec.ps1 -Platform macos
|
||
|
||
# 3. 获取依赖
|
||
flutter pub get
|
||
|
||
# 4. 应用 pub cache 补丁(见 §2.9)
|
||
bash scripts/patch_pub_cache.sh
|
||
|
||
# 5. 编译验证
|
||
flutter build ios --no-codesign
|
||
flutter build macos
|
||
```
|
||
|
||
### 2.7 鸿蒙端完整操作流程
|
||
|
||
```bash
|
||
# 1. 克隆仓库
|
||
git clone <仓库URL> xianyan
|
||
cd xianyan
|
||
|
||
# 2. 确保 packages/ 目录已准备好(从 zip 解压或 git submodule)
|
||
|
||
# 3. 生成 pubspec.yaml
|
||
.\tools\setup_pubspec.ps1 -Platform ohos
|
||
|
||
# 4. 获取依赖
|
||
flutter pub get
|
||
```
|
||
|
||
### 2.8 特殊包说明
|
||
|
||
#### 2.8.1 pro_image_editor(已迁移至远程版本)
|
||
|
||
`pro_image_editor` 已从本地包迁移为远程版本 `^12.4.4`,两端均使用远程版本:
|
||
|
||
```yaml
|
||
# 两端均使用远程版本
|
||
pro_image_editor: ^12.4.4
|
||
```
|
||
|
||
#### 2.8.2 file_picker(API 变更 + win32 6.x 兼容)
|
||
|
||
`file_picker` 已从 `^11.0.0` 升级为 `^12.0.0-beta.5`,原因:
|
||
|
||
1. **API 变更**(11.x → 12.x 通用):
|
||
|
||
```dart
|
||
// ❌ 旧版 API(file_picker ^8.x)
|
||
final result = await FilePicker.platform.pickFiles();
|
||
|
||
// ✅ 新版 API(file_picker ^11.x / ^12.x)
|
||
final result = await FilePicker.pickFiles();
|
||
```
|
||
|
||
2. **win32 6.x 兼容**(12.x 专属):
|
||
- `file_picker ^11.0.0` 依赖 `win32: ^5.9.0`,与 `dependency_overrides` 中的 `win32: ^6.0.1` 冲突
|
||
- `file_picker ^12.0.0-beta.5` 兼容 `win32 ^6.0.1`,解决 macOS 构建失败
|
||
- ⚠️ 12.x 为 beta 版本,如遇问题可回退到 11.x(需同时降级 win32 override,见§2.6)
|
||
|
||
项目代码已更新为新版 API。鸿蒙端本地包版本已升级为 `11.0.0-ohos.1`(基于 file_picker 11.x 适配),
|
||
API 已与远程版本对齐,无需额外处理。
|
||
|
||
#### 2.8.3 flutter_secure_storage(版本差异说明)
|
||
|
||
MacBook Pro 端使用远程版本 `^10.3.0`,其 Windows 平台实现兼容 `win32 ^6.0.1`,
|
||
解决了之前版本与 `win32 6.x` 的编译冲突。
|
||
|
||
> **注意**:鸿蒙端本地包版本为 `9.2.4-ohos.1`(基于 9.x 适配),
|
||
> 与 MacBook Pro 端远程版本 `10.3.0` 存在主版本号差异。
|
||
> 两端 API 兼容,`lib/` 代码无需特殊处理。
|
||
|
||
#### 2.8.4 receive_sharing_intent(gitcode 引用)
|
||
|
||
`receive_sharing_intent` 在 pub.dev 上的版本与鸿蒙端不兼容,MacBook Pro 端需使用 gitcode 引用:
|
||
|
||
```yaml
|
||
# MacBook Pro 端使用 gitcode 引用
|
||
receive_sharing_intent:
|
||
git:
|
||
url: "https://gitcode.com/openharmony-sig/fluttertpc_receive_sharing_intent.git"
|
||
ref: "br_v1.8.1_ohos"
|
||
```
|
||
|
||
> **注意**:gitcode 版本的 `SharedMediaFile` 构造函数可能包含 `ohosPath` 等鸿蒙特有参数,
|
||
> 官方 SDK 不存在这些参数。项目代码中已通过 `pu.isOhos` 条件分支隔离。
|
||
|
||
#### 2.8.5 flutter_vibrate(gitcode 引用)
|
||
|
||
`flutter_vibrate` 在 pub.dev 上的版本不支持鸿蒙,MacBook Pro 端需使用 gitcode 引用:
|
||
|
||
```yaml
|
||
# MacBook Pro 端使用 gitcode 引用
|
||
flutter_vibrate:
|
||
git:
|
||
url: https://gitcode.com/openharmony-sig/fluttertpc_flutter_vibrate.git
|
||
```
|
||
|
||
#### 2.8.6 home_widget(pub.dev 远程版本)
|
||
|
||
`home_widget` 的 gitcode 版本依赖 `path_provider` 的 git 版本,会与远程 `path_provider` 冲突。
|
||
MacBook Pro 端使用 pub.dev 版本 `^0.9.3`,鸿蒙端的 `ohosName` 参数通过 `pu.isOhos` + `dynamic` 调用隔离:
|
||
|
||
```yaml
|
||
# MacBook Pro 端使用 pub.dev 远程版本
|
||
home_widget: ^0.9.3
|
||
```
|
||
|
||
#### 2.8.7 nearby_connections(已移除)
|
||
|
||
`nearby_connections`(Google Nearby Connections API)已从项目中完全移除。
|
||
原因:该库仅支持 Android/iOS,鸿蒙端需要本地 stub 包,但本地包会影响 Android 端构建。
|
||
近场通信功能由 `nearby_service` 统一承担(Android Wi-Fi Direct / iOS MultipeerConnectivity / 鸿蒙)。
|
||
|
||
> **注意**:移除后,蓝牙 P2P 发现功能不再可用,设备配对通过配对码/扫码/雷达/Wi-Fi Direct 进行。
|
||
|
||
#### 2.8.8 app_tracking_transparency(两端均使用远程版本)
|
||
|
||
`app_tracking_transparency` 是 iOS 专属权限库(App Tracking Transparency),
|
||
两端均使用远程版本 `^2.0.6`,无需本地适配。代码中通过 `Platform.isIOS` 条件守卫,
|
||
非 iOS 平台直接返回授权成功,不影响鸿蒙/Android/macOS 编译。
|
||
|
||
```yaml
|
||
# 两端配置相同
|
||
app_tracking_transparency: ^2.0.6
|
||
```
|
||
|
||
#### 2.8.9 flutter_webrtc(macOS WebRTC-SDK 版本对齐)
|
||
|
||
macOS 端 `flutter_webrtc` 已从 `^1.4.0` 升级为 `^1.5.2`,原因:
|
||
|
||
1. **CocoaPods 版本冲突**:
|
||
- macOS `Podfile` 通过本地 podspec `macos/WebRTC-SDK.podspec.json` 声明 WebRTC 二进制库版本 `144.7559.09`(Intel Mac 渲染修复补丁,使用 ghfast.top 镜像下载)
|
||
- `flutter_webrtc 1.4.0` 的 macOS podspec 依赖 `WebRTC-SDK 144.7559.01`(`.01` 版本),与本地 podspec 的 `.09` 版本冲突,`pod install` 报错:`CocoaPods could not find compatible versions for pod "WebRTC-SDK"`
|
||
- `flutter_webrtc 1.5.2` 的 macOS podspec 已对齐声明 `s.dependency 'WebRTC-SDK', '144.7559.09'`,与本地 podspec 一致,冲突解决
|
||
|
||
2. **API 兼容性**:
|
||
- 1.4.0 → 1.5.2 为兼容性升级,项目使用的 WebRTC API(`RTCPeerConnection` / `MediaStream` / `VideoRenderer`)均未变更
|
||
- 已通过 `flutter analyze` 验证:`screen_share_page.dart` / `webrtc_service.dart` / `screen_share_provider.dart` 无需修改
|
||
|
||
3. **鸿蒙端注意**:
|
||
- 鸿蒙端使用本地包 `packages/flutter_webrtc`(v1.4.0-ohos.1),**无法直接升级到 1.5.2**
|
||
- 鸿蒙端本地包待后续同步升级到 `1.5.2-ohos` 版本
|
||
- 远程端升级不影响鸿蒙端编译(两端 pubspec 独立)
|
||
|
||
```yaml
|
||
# MacBook Pro 端(pubspec.macos.yaml)
|
||
flutter_webrtc: ^1.5.2 # WebRTC音视频通信(1.5.2 对齐 WebRTC-SDK 144.7559.09)
|
||
|
||
# 鸿蒙端(pubspec.ohos.yaml)
|
||
flutter_webrtc: # v1.4.0-ohos.1 | 本地化-鸿蒙适配;远程端已升至1.5.2,鸿蒙本地包待后续同步
|
||
path: packages/flutter_webrtc
|
||
```
|
||
|
||
> **macOS Podfile 本地 podspec 说明**:
|
||
> ```ruby
|
||
> # macos/Podfile 第 39 行
|
||
> pod 'WebRTC-SDK', :podspec => 'WebRTC-SDK.podspec.json'
|
||
> ```
|
||
> 该 podspec 声明从 `ghfast.top` 镜像下载 `WebRTC.xcframework.zip`(v144.7559.09),
|
||
> 避免直连 github.com 超时。升级 flutter_webrtc 时需确保其 podspec 声明的 WebRTC-SDK 版本与本地 podspec 一致。
|
||
|
||
#### 2.8.10 permission_handler_apple(macOS 无实现 — 权限由 entitlement 管理)
|
||
|
||
`permission_handler_apple` 是 `permission_handler` 在 iOS/macOS 平台的实现包,但 **9.4.9 版本仅支持 iOS,不支持 macOS**:
|
||
|
||
```yaml
|
||
# permission_handler_apple 9.4.9 的 pubspec.yaml
|
||
flutter:
|
||
plugin:
|
||
implements: permission_handler
|
||
platforms:
|
||
ios: # ⚠️ 仅声明 ios,无 macos
|
||
pluginClass: PermissionHandlerPlugin
|
||
```
|
||
|
||
**影响**:
|
||
- macOS 端 `GeneratedPluginRegistrant.swift` 不会注册 `PermissionHandlerApplePlugin`
|
||
- 调用 `Permission.camera.status` / `Permission.photos.request()` / `openAppSettings()` 等方法会抛出 `MissingPluginException(No implementation found for method checkPermissionStatus on channel flutter.baseflow.com/permissions/methods)`
|
||
- 这是 macOS App Store 审核被拒 Guideline 2.1(a) 的直接原因(权限管理页点击相册/麦克风显示错误信息)
|
||
|
||
**解决方案**:
|
||
- macOS sandbox 下,权限由 **entitlement + Info.plist 用法说明** 自动管理
|
||
- 系统在 App 首次访问受保护资源(相机/麦克风/相册)时弹出授权对话框(由 OS 触发,不由 App 调用)
|
||
- `permission_service.dart` 中 `checkStatus()` / `requestPermission()` 已添加 macOS 早返回逻辑,直接返回 `granted`,避免调用未注册的方法通道
|
||
- `openSettings()` 已添加 macOS 原生跳转:`Process.run('open', ['x-apple.systempreferences:com.apple.preference.security?Privacy'])`
|
||
|
||
**macOS 必需的 entitlement 与 Info.plist 配置**:
|
||
|
||
| 权限 | Entitlement | Info.plist Key | 说明 |
|
||
|---|---|---|---|
|
||
| 相机 | `com.apple.security.device.camera` | `NSCameraUsageDescription` | 扫码 / 拍照 / OCR |
|
||
| 麦克风 | `com.apple.security.device.audio-input` | `NSMicrophoneUsageDescription` | 语音录制 / 语音转文字 |
|
||
| 相册 | `com.apple.security.personal-information.photos-library` | (无需)| 保存图片/视频到相册 |
|
||
| 本地服务器 | `com.apple.security.network.server` | (无需)| LocalSend 局域网文件传输 |
|
||
|
||
> **注意**:`permission_handler_apple` 后续版本可能新增 macOS 支持,届时可移除 `permission_service.dart` 中的 macOS 早返回逻辑。检查方法:查看 `pub-cache/hosted/pub.dev/permission_handler_apple-X.Y.Z/` 是否存在 `macos/` 目录。
|
||
|
||
### 2.9 ⚠️ 鸿蒙端升级 Tips(2026-06-15,完成后删除本节)
|
||
|
||
> **本节为鸿蒙端开发者提供升级指引,鸿蒙端完成适配后请删除此节。**
|
||
|
||
#### 2.9.1 需要升级的鸿蒙本地包
|
||
|
||
| 本地包目录 | 当前版本 | 需升级到 | 升级说明 |
|
||
|---|---|---|---|
|
||
| `packages/wakelock_plus` | v1.4.0-ohos.1 | v1.6.0-ohos | API不变,仅平台接口版本提升 |
|
||
| `packages/record` | v6.0.0-ohos.1 | v6.2.1-ohos | 降级到6.2.1(7.0.0需Dart3.12+,当前SDK为3.11.5) |
|
||
| `packages/flutter_local_notifications` | v21.0.0-ohos | v22.0.0-ohos | 需Flutter 3.38.1+,API已使用命名参数 |
|
||
| `packages/flutter_secure_storage` | v9.2.4-ohos.1 | v10.3.0-ohos | 主版本差异(9→10),需评估API兼容性 |
|
||
| `packages/mobile_scanner` | v7.1.4-ohos.1 | v7.2.0-ohos | 次版本更新 |
|
||
| `packages/video_player` | v2.10.0-ohos.1 | v2.11.0-ohos | 次版本更新 |
|
||
| `packages/hive_flutter` | v1.1.0-ohos.2 | v2.3.4-ohos | 迁移到hive_ce_flutter,导入路径变更 |
|
||
| `packages/sqflite` | v2.4.1-ohos.1 | v2.4.3-ohos | 补丁更新 |
|
||
|
||
#### 2.9.2 EOL 包迁移(鸿蒙端)
|
||
|
||
| 旧包 | 新包 | 鸿蒙端操作 |
|
||
|---|---|---|
|
||
| `sqlite3_flutter_libs` | `sqlite3 v3.x` | 鸿蒙端使用sqflite_ohos桥接,不受影响,但需移除`sqlite3_flutter_libs`引用。所有平台均配置`hooks.user_defines.sqlite3.source: system`使用系统SQLite,避免从GitHub下载预编译库失败 |
|
||
| `hive_flutter` | `hive_ce_flutter` | 将`packages/hive_flutter`升级为hive_ce版本,导入路径从`package:hive_flutter/`改为`package:hive_ce_flutter/` |
|
||
| `device_calendar` | `device_calendar_plus` | 鸿蒙端通过MethodChannel桥接,不受影响,但需移除`device_calendar`引用 |
|
||
|
||
#### 2.9.3 鸿蒙端升级步骤
|
||
|
||
```bash
|
||
# 1. 拉取最新代码
|
||
git pull
|
||
|
||
# 2. 重新生成 pubspec.yaml
|
||
.\tools\setup_pubspec.ps1 -Platform ohos
|
||
|
||
# 3. 逐一升级本地包
|
||
# 对每个需要升级的包:
|
||
# a. 下载新版本源码
|
||
# b. 添加 ohos 平台适配代码
|
||
# c. 更新 packages/xxx 目录
|
||
# d. 更新 pubspec.ohos.yaml 中的版本注释
|
||
|
||
# 4. 特别注意 hive_flutter → hive_ce_flutter 迁移
|
||
# 本地包 packages/hive_flutter 需要更新导入路径
|
||
# lib/ 代码中已将 import 'package:hive_flutter/' 改为 import 'package:hive_ce_flutter/'
|
||
# 鸿蒙端本地包也需同步修改导出路径
|
||
|
||
# 5. 获取依赖
|
||
flutter pub get
|
||
|
||
# 6. 构建验证
|
||
flutter build hap --debug
|
||
```
|
||
|
||
#### 2.9.4 dio_cache_interceptor 4.x 变更(鸿蒙端注意)
|
||
|
||
`dio_cache_interceptor` 已从 3.x 升级到 4.x,API 变更:
|
||
- `hitCacheOnErrorExcept: [401, 403]` → `hitCacheOnNetworkFailure: true`
|
||
- `Nullable<Duration>` → `Duration?`
|
||
- `CacheResponse` 新增 `statusCode` 字段
|
||
|
||
鸿蒙端如果使用远程版本,需同步更新缓存配置代码。
|
||
|
||
### 2.10 ⚠️ pub cache 补丁(MacBook Pro 端必读)
|
||
|
||
> **关键问题**:`dependency_overrides` 中 `win32: ^6.0.1` 导致部分依赖 `win32 ^5.x` 的三方包编译失败。
|
||
> 这些三方包的 Windows 平台代码在 macOS 构建时也会被编译(Dart 编译器不区分平台)。
|
||
> 需要手动修补 pub cache 中的文件,使它们兼容 win32 6.x API。
|
||
|
||
#### 2.9.1 需要修补的包
|
||
|
||
| 包名 | 版本 | 问题 | 修补文件 |
|
||
|---|---|---|---|
|
||
| quill_native_bridge_windows | 0.0.2 | 使用 win32 5.x API(`TEXT()`、`OpenClipboard(NULL)==FALSE` 等) | `lib/quill_native_bridge_windows.dart` + `lib/src/clipboard_html_format.dart` |
|
||
| flutter_vibrate | gitcode | `TARGET_OS_SIMULATOR` 在 Xcode 16+/Swift 6 中不可用 | `ios/Classes/SwiftVibratePlugin.swift` |
|
||
|
||
#### 2.9.2 补丁应用流程
|
||
|
||
```bash
|
||
# 1. 确保 flutter pub get 已执行
|
||
flutter pub get
|
||
|
||
|
||
|
||
# 3. 验证构建
|
||
flutter build macos
|
||
flutter build ios --no-codesign
|
||
```
|
||
|
||
> ⚠️ `flutter clean` 或 `flutter pub cache repair` 会清除补丁,需重新执行步骤 2。
|
||
|
||
#### 2.9.3 补丁脚本
|
||
|
||
在项目根目录创建 `scripts/patch_pub_cache.sh`(已包含在仓库中),内容如下:
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
# patch_pub_cache.sh — 修补 pub cache 中的兼容性问题
|
||
# 创建时间: 2026-06-01
|
||
# 作用: 修补 quill_native_bridge_windows (win32 6.x) 和 flutter_vibrate (Swift 6)
|
||
|
||
set -e
|
||
|
||
echo "🔧 Applying pub cache patches..."
|
||
|
||
# ── Patch 1: quill_native_bridge_windows — clipboard_html_format.dart ──
|
||
QNBW_DIR="$HOME/.pub-cache/hosted/pub.dev/quill_native_bridge_windows-0.0.2"
|
||
if [ -d "$QNBW_DIR" ]; then
|
||
echo " Patching clipboard_html_format.dart..."
|
||
cat > "$QNBW_DIR/lib/src/clipboard_html_format.dart" << 'DART_EOF'
|
||
import 'package:ffi/ffi.dart';
|
||
import 'package:win32/win32.dart';
|
||
|
||
import '../quill_native_bridge_windows.dart';
|
||
|
||
const _kHtmlFormatName = 'HTML Format';
|
||
|
||
int? _cfHtml;
|
||
|
||
extension ClipboardHtmlFormatExt on QuillNativeBridgeWindows {
|
||
int? get cfHtml {
|
||
_cfHtml ??= _registerHtmlFormat();
|
||
return _cfHtml;
|
||
}
|
||
|
||
int? _registerHtmlFormat() {
|
||
final htmlFormatPointer = _kHtmlFormatName.toPcwstr();
|
||
final result = RegisterClipboardFormat(htmlFormatPointer);
|
||
free(htmlFormatPointer);
|
||
|
||
if (result.error.isError) {
|
||
return null;
|
||
}
|
||
return result.value;
|
||
}
|
||
}
|
||
DART_EOF
|
||
echo " ✅ clipboard_html_format.dart patched"
|
||
else
|
||
echo " ⚠️ quill_native_bridge_windows not found in pub cache"
|
||
fi
|
||
|
||
# ── Patch 2: quill_native_bridge_windows — quill_native_bridge_windows.dart ──
|
||
if [ -d "$QNBW_DIR" ]; then
|
||
echo " Patching quill_native_bridge_windows.dart..."
|
||
cat > "$QNBW_DIR/lib/quill_native_bridge_windows.dart" << 'DART_EOF'
|
||
// Patched for win32 6.x compatibility (2026-06-01)
|
||
// Changes: Win32Result API, HGLOBAL/HANDLE extension types, TEXT() → toPcwstr()
|
||
|
||
import 'dart:ffi';
|
||
import 'dart:io';
|
||
|
||
import 'package:ffi/ffi.dart';
|
||
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:quill_native_bridge_platform_interface/quill_native_bridge_platform_interface.dart';
|
||
import 'package:win32/win32.dart';
|
||
|
||
import 'src/clipboard_html_format.dart';
|
||
import 'src/html_cleaner.dart';
|
||
import 'src/html_formatter.dart';
|
||
import 'src/image_saver.dart';
|
||
|
||
class QuillNativeBridgeWindows extends QuillNativeBridgePlatform {
|
||
static void registerWith() {
|
||
QuillNativeBridgePlatform.instance = QuillNativeBridgeWindows();
|
||
}
|
||
|
||
@override
|
||
Future<bool> isSupported(QuillNativeBridgeFeature feature) async => {
|
||
QuillNativeBridgeFeature.getClipboardHtml,
|
||
QuillNativeBridgeFeature.copyHtmlToClipboard,
|
||
QuillNativeBridgeFeature.saveImage,
|
||
}.contains(feature);
|
||
|
||
@override
|
||
Future<String?> getClipboardHtml() async {
|
||
final openResult = OpenClipboard(null);
|
||
if (openResult.error.isError) {
|
||
assert(false, 'Unknown error while opening the clipboard. Error code: ${GetLastError()}');
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
final htmlFormatId = cfHtml;
|
||
if (htmlFormatId == null) {
|
||
assert(false, 'Failed to register clipboard HTML format.');
|
||
return null;
|
||
}
|
||
|
||
final availableResult = IsClipboardFormatAvailable(htmlFormatId);
|
||
if (availableResult.error.isError) {
|
||
return null;
|
||
}
|
||
|
||
final getDataResult = GetClipboardData(htmlFormatId);
|
||
if (getDataResult.error.isError) {
|
||
assert(false, 'Failed to get clipboard data. Error code: ${GetLastError()}');
|
||
return null;
|
||
}
|
||
|
||
final clipboardDataHandle = getDataResult.value;
|
||
final hglobal = HGLOBAL(clipboardDataHandle);
|
||
final lockResult = GlobalLock(hglobal);
|
||
if (lockResult.error.isError) {
|
||
assert(false, 'Failed to lock global memory. Error code: ${GetLastError()}');
|
||
return null;
|
||
}
|
||
|
||
final lockedMemoryPointer = lockResult.value;
|
||
final windowsHtmlWithMetadata = lockedMemoryPointer.cast<Utf8>().toDartString();
|
||
GlobalUnlock(hglobal);
|
||
|
||
final cleanedHtml = stripWindowsHtmlDescriptionHeaders(windowsHtmlWithMetadata);
|
||
return cleanedHtml;
|
||
} finally {
|
||
CloseClipboard();
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<void> copyHtmlToClipboard(String html) async {
|
||
final openResult = OpenClipboard(null);
|
||
if (openResult.error.isError) {
|
||
assert(false, 'Unknown error while opening the clipboard. Error code: ${GetLastError()}');
|
||
return;
|
||
}
|
||
|
||
final windowsClipboardHtml = constructWindowsHtmlDescriptionHeaders(html);
|
||
final htmlPointer = windowsClipboardHtml.toNativeUtf8();
|
||
|
||
try {
|
||
final emptyResult = EmptyClipboard();
|
||
if (emptyResult.error.isError) {
|
||
assert(false, 'Failed to empty the clipboard. Error code: ${GetLastError()}');
|
||
return;
|
||
}
|
||
|
||
final htmlFormatId = cfHtml;
|
||
if (htmlFormatId == null) {
|
||
assert(false, 'Failed to register clipboard HTML format. Error code: ${GetLastError()}');
|
||
return;
|
||
}
|
||
|
||
final unitSize = sizeOf<Uint8>();
|
||
final htmlSize = (htmlPointer.length + 1) * unitSize;
|
||
|
||
final allocResult = GlobalAlloc(GMEM_MOVEABLE, htmlSize);
|
||
if (allocResult.error.isError) {
|
||
assert(false, 'Failed to allocate memory for the clipboard content. Error code: ${GetLastError()}');
|
||
return;
|
||
}
|
||
|
||
final clipboardMemoryHandle = allocResult.value;
|
||
final lockResult = GlobalLock(clipboardMemoryHandle);
|
||
if (lockResult.error.isError) {
|
||
GlobalFree(clipboardMemoryHandle);
|
||
assert(false, 'Failed to lock global memory. Error code: ${GetLastError()}');
|
||
return;
|
||
}
|
||
|
||
final lockedMemoryPointer = lockResult.value;
|
||
final targetMemoryPointer = lockedMemoryPointer.cast<Uint8>();
|
||
final sourcePointer = htmlPointer.cast<Uint8>();
|
||
|
||
for (var i = 0; i < htmlPointer.length; i++) {
|
||
targetMemoryPointer[i] = (sourcePointer + i).value;
|
||
}
|
||
(targetMemoryPointer + htmlPointer.length).value = 0;
|
||
|
||
GlobalUnlock(clipboardMemoryHandle);
|
||
|
||
final setResult = SetClipboardData(htmlFormatId, HANDLE(clipboardMemoryHandle));
|
||
if (setResult.error.isError) {
|
||
GlobalFree(clipboardMemoryHandle);
|
||
assert(false, 'Failed to set the clipboard data: ${GetLastError()}');
|
||
}
|
||
} finally {
|
||
CloseClipboard();
|
||
calloc.free(htmlPointer);
|
||
}
|
||
}
|
||
|
||
@visibleForTesting
|
||
static ImageSaver imageSaver = ImageSaver();
|
||
|
||
@override
|
||
Future<ImageSaveResult> saveImage(Uint8List imageBytes, {required ImageSaveOptions options}) async {
|
||
final typeGroup = XTypeGroup(label: 'Images', extensions: [options.fileExtension]);
|
||
final saveLocation = await imageSaver.fileSelector.getSaveLocation(
|
||
options: SaveDialogOptions(
|
||
suggestedName: '${options.name}.${options.fileExtension}',
|
||
initialDirectory: imageSaver.picturesDirectoryPath,
|
||
),
|
||
acceptedTypeGroups: [typeGroup],
|
||
);
|
||
final imageFilePath = saveLocation?.path;
|
||
if (imageFilePath == null) {
|
||
return ImageSaveResult.io(filePath: null);
|
||
}
|
||
final imageFile = File(imageFilePath);
|
||
await imageFile.writeAsBytes(imageBytes);
|
||
return ImageSaveResult.io(filePath: imageFile.path);
|
||
}
|
||
|
||
@override
|
||
Future<void> openGalleryApp() async {
|
||
final uriPtr = 'ms-photos:'.toPcwstr();
|
||
final openPtr = 'open'.toPcwstr();
|
||
ShellExecute(null, openPtr, uriPtr, null, null, SW_SHOWNORMAL);
|
||
free(uriPtr);
|
||
free(openPtr);
|
||
}
|
||
}
|
||
DART_EOF
|
||
echo " ✅ quill_native_bridge_windows.dart patched"
|
||
fi
|
||
|
||
# ── Patch 3: flutter_vibrate — SwiftVibratePlugin.swift ──
|
||
FV_DIR=$(find "$HOME/.pub-cache/git" -path "*/fluttertpc_flutter_vibrate*" -maxdepth 2 -type d 2>/dev/null | head -1)
|
||
if [ -n "$FV_DIR" ]; then
|
||
echo " Patching SwiftVibratePlugin.swift..."
|
||
cat > "$FV_DIR/ios/Classes/SwiftVibratePlugin.swift" << 'SWIFT_EOF'
|
||
import Flutter
|
||
import UIKit
|
||
import AudioToolbox
|
||
|
||
// Patched for Xcode 16+/Swift 6 compatibility (2026-06-01)
|
||
// TARGET_OS_SIMULATOR removed; using #if targetEnvironment(simulator)
|
||
|
||
#if targetEnvironment(simulator)
|
||
private let isDevice = false
|
||
#else
|
||
private let isDevice = true
|
||
#endif
|
||
|
||
public class SwiftVibratePlugin: NSObject, FlutterPlugin {
|
||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||
let channel = FlutterMethodChannel(name: "vibrate", binaryMessenger: registrar.messenger())
|
||
let instance = SwiftVibratePlugin()
|
||
registrar.addMethodCallDelegate(instance, channel: channel)
|
||
}
|
||
|
||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||
switch (call.method) {
|
||
case "canVibrate":
|
||
if isDevice { result(true) } else { result(false) }
|
||
case "vibrate":
|
||
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
|
||
case "impact":
|
||
if #available(iOS 10.0, *) {
|
||
let impact = UIImpactFeedbackGenerator()
|
||
impact.prepare()
|
||
impact.impactOccurred()
|
||
} else { AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) }
|
||
case "selection":
|
||
if #available(iOS 10.0, *) {
|
||
let selection = UISelectionFeedbackGenerator()
|
||
selection.prepare()
|
||
selection.selectionChanged()
|
||
} else { AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) }
|
||
case "success":
|
||
if #available(iOS 10.0, *) {
|
||
let notification = UINotificationFeedbackGenerator()
|
||
notification.prepare()
|
||
notification.notificationOccurred(.success)
|
||
} else { AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) }
|
||
case "warning":
|
||
if #available(iOS 10.0, *) {
|
||
let notification = UINotificationFeedbackGenerator()
|
||
notification.prepare()
|
||
notification.notificationOccurred(.warning)
|
||
} else { AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) }
|
||
case "error":
|
||
if #available(iOS 10.0, *) {
|
||
let notification = UINotificationFeedbackGenerator()
|
||
notification.prepare()
|
||
notification.notificationOccurred(.error)
|
||
} else { AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) }
|
||
case "heavy":
|
||
if #available(iOS 10.0, *) {
|
||
let generator = UIImpactFeedbackGenerator(style: .heavy)
|
||
generator.prepare()
|
||
generator.impactOccurred()
|
||
} else { AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) }
|
||
case "medium":
|
||
if #available(iOS 10.0, *) {
|
||
let generator = UIImpactFeedbackGenerator(style: .medium)
|
||
generator.prepare()
|
||
generator.impactOccurred()
|
||
} else { AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) }
|
||
case "light":
|
||
if #available(iOS 10.0, *) {
|
||
let generator = UIImpactFeedbackGenerator(style: .light)
|
||
generator.prepare()
|
||
generator.impactOccurred()
|
||
} else { AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) }
|
||
default:
|
||
result(FlutterMethodNotImplemented)
|
||
}
|
||
}
|
||
}
|
||
SWIFT_EOF
|
||
echo " ✅ SwiftVibratePlugin.swift patched"
|
||
else
|
||
echo " ⚠️ flutter_vibrate not found in pub cache"
|
||
fi
|
||
|
||
echo "🎉 All patches applied!"
|
||
echo "⚠️ Note: Run this script again after 'flutter clean' or 'flutter pub cache repair'"
|
||
```
|
||
|
||
#### 2.9.4 win32 5.x → 6.x API 迁移参考
|
||
|
||
如需修补其他使用 win32 的三方包,以下是关键 API 变更:
|
||
|
||
| win32 5.x | win32 6.x | 说明 |
|
||
|---|---|---|
|
||
| `OpenClipboard(NULL) == FALSE` | `OpenClipboard(null).error.isError` | 返回 `Win32Result<bool>` |
|
||
| `CloseClipboard()` | `CloseClipboard()` | 返回 `Win32Result<bool>`,忽略即可 |
|
||
| `EmptyClipboard() == FALSE` | `EmptyClipboard().error.isError` | 返回 `Win32Result<bool>` |
|
||
| `IsClipboardFormatAvailable(id) == FALSE` | `IsClipboardFormatAvailable(id).error.isError` | 返回 `Win32Result<bool>` |
|
||
| `GetClipboardData(id) == NULL` | `GetClipboardData(id).error.isError` | 返回 `Win32Result<HANDLE>` |
|
||
| `GlobalAlloc(flags, size) == nullptr` | `GlobalAlloc(flags, size).error.isError` | 返回 `Win32Result<HGLOBAL>` |
|
||
| `GlobalLock(handle) == nullptr` | `GlobalLock(HGLOBAL(handle)).error.isError` | 返回 `Win32Result<Pointer>` |
|
||
| `GlobalUnlock(handle)` | `GlobalUnlock(HGLOBAL(handle))` | 返回 `Win32Result<bool>` |
|
||
| `GlobalFree(handle)` | `GlobalFree(HGLOBAL(handle))` | 返回 `Win32Result<HGLOBAL>` |
|
||
| `SetClipboardData(id, addr) == NULL` | `SetClipboardData(id, HANDLE(hglobal)).error.isError` | 返回 `Win32Result<HANDLE>` |
|
||
| `TEXT('str')` | `'str'.toPcwstr()` | `TEXT()` 已移除 |
|
||
| `RegisterClipboardFormat(ptr)` 返回 `int` | `RegisterClipboardFormat(ptr)` 返回 `Win32Result<int>` | 检查 `.error.isError` |
|
||
| `ShellExecute(NULL, ...)` | `ShellExecute(null, ...)` | `NULL` → `null`,`nullptr` → `null`(PCWSTR? 类型) |
|
||
| `HGLOBAL` 为 `int` | `HGLOBAL` 为 `extension type const HGLOBAL(Pointer _)` | 需类型转换 |
|
||
| `HANDLE` 为 `int` | `HANDLE` 为 `extension type const HANDLE(Pointer _)` | 需类型转换 |
|
||
|
||
> **类型转换**:`HANDLE` 和 `HGLOBAL` 都 `implements Pointer`,可互相转换:
|
||
> - `HANDLE → HGLOBAL`:`HGLOBAL(handle)`
|
||
> - `HGLOBAL → HANDLE`:`HANDLE(hglobal)`
|
||
|
||
---
|
||
|
||
## 三、Git 提交与合并规范
|
||
|
||
### 3.1 分支策略
|
||
|
||
```
|
||
main (受保护) ← 所有平台共用
|
||
├── feature/xxx ← 功能开发(所有平台共用)
|
||
├── fix/xxx ← Bug修复
|
||
└── hotfix/xxx ← 紧急修复
|
||
```
|
||
|
||
> **不需要按平台创建长期分支**。`lib/` 代码所有平台共用,独立平台分支会导致大量合并冲突。
|
||
> 平台原生代码已天然隔离(`ios/`、`macos/`、`ohos/` 各自独立),不会互相污染。
|
||
|
||
### 3.2 iOS/macOS 开发者提交规则
|
||
|
||
**✅ 应该提交的文件:**
|
||
- `lib/` 目录下的所有 Dart 代码
|
||
- `ios/` 目录下的原生代码
|
||
- `macos/` 目录下的原生代码
|
||
- `assets/` 目录下的资源文件
|
||
- `test/` 目录下的测试代码
|
||
|
||
**❌ 不要提交的文件:**
|
||
- `pubspec.yaml`(MacBook Pro 端已替换为远程版本,**绝不能提交**)
|
||
- `packages/` 目录(已在 `.gitignore` 中排除)
|
||
- `ohos/` 目录下的鸿蒙原生代码
|
||
- 鸿蒙SDK特有的配置文件
|
||
|
||
**⚠️ pubspec.yaml 提交铁律:**
|
||
1. **`pubspec.yaml` 已加入 `.gitignore`,不应提交** — 由 `setup_pubspec.ps1` 脚本从模板生成
|
||
2. **修改依赖时编辑模板文件** — `pubspec.macos.yaml`(MacBook Pro 端)或 `pubspec.ohos.yaml`(鸿蒙端)
|
||
3. **模板文件必须提交** — 确保两端依赖配置同步
|
||
|
||
### 3.3 pubspec.yaml 处理策略(⭐ 重点)
|
||
|
||
> **双模板机制下,`pubspec.yaml` 由脚本生成,不提交到 Git。**
|
||
> MacBook Pro 端开发者无需手动替换本地包引用,也无需 git stash 隔离。
|
||
|
||
#### 3.3.1 日常操作
|
||
|
||
```bash
|
||
# git pull 后,重新生成 pubspec.yaml 即可
|
||
.\tools\setup_pubspec.ps1 -Platform macos
|
||
flutter pub get
|
||
|
||
# 编译验证
|
||
flutter build ios --no-codesign
|
||
```
|
||
|
||
#### 3.3.2 需要新增依赖时
|
||
|
||
新增依赖时,必须在**两端模板**分别操作:
|
||
|
||
```bash
|
||
# 1. 在 pubspec.macos.yaml 添加远程版本依赖
|
||
# 2. 在 pubspec.ohos.yaml 添加对应依赖(本地包或远程版本)
|
||
# 3. 更新本文档 §2.3 差异对照表
|
||
# 4. 重新生成 pubspec.yaml
|
||
.\tools\setup_pubspec.ps1 -Platform macos
|
||
flutter pub get
|
||
|
||
# 5. 通知鸿蒙开发者评估适配
|
||
```
|
||
|
||
#### 3.3.3 git pull 后 pubspec.yaml 被覆盖
|
||
|
||
```bash
|
||
# pubspec.yaml 在 .gitignore 中,git pull 不会覆盖
|
||
# 如果误删或需要重新生成:
|
||
.\tools\setup_pubspec.ps1 -Platform macos
|
||
flutter pub get
|
||
```
|
||
|
||
#### 3.3.4 减少冲突的最佳实践
|
||
|
||
| 做法 | 说明 |
|
||
|---|---|
|
||
| **不提交 pubspec.yaml** | 已在 .gitignore,从根本上避免冲突 |
|
||
| **新增依赖时通知鸿蒙开发者** | 让鸿蒙端同步评估适配 |
|
||
| **版本号升级单独提交** | 不要和功能代码混在一起提交 |
|
||
| **修改模板而非 pubspec.yaml** | 直接编辑 `pubspec.macos.yaml` / `pubspec.ohos.yaml` |
|
||
|
||
### 3.4 其他合并注意事项
|
||
|
||
1. **不要删除 `pu.isOhos` 相关代码**:这些条件分支在 iOS/macOS 上不会执行,但删除会导致鸿蒙端编译失败
|
||
2. **不要修改 `OhosAppShell`、`OhosNavBridge` 等鸿蒙专用类**:这些类仅在鸿蒙端使用
|
||
3. **新增页面路由时**:需同时在 `app_router.dart`(GoRouter路由表)和 `ohos_nav_bridge.dart`(鸿蒙路由映射表)中注册
|
||
|
||
### 3.5 PR 审查要点
|
||
|
||
- [ ] 未提交 `pubspec.yaml`(已加入 .gitignore,由脚本生成)
|
||
- [ ] 依赖变更已同步更新 `pubspec.macos.yaml` 和 `pubspec.ohos.yaml` 两个模板
|
||
- [ ] 未引入 `TargetPlatform` exhaustive switch 问题
|
||
- [ ] 新增路由已在 `ohos_nav_bridge.dart` 中同步
|
||
- [ ] `lib/` 代码无平台特定硬编码(应使用 `platform_utils.dart`)
|
||
- [ ] 新增依赖已通知鸿蒙开发者评估适配
|
||
|
||
---
|
||
|
||
## 四、TargetPlatform.ohos 处理
|
||
|
||
### 4.1 问题背景
|
||
|
||
鸿蒙定制 Flutter SDK 在 `TargetPlatform` 枚举中新增了 `TargetPlatform.ohos`。
|
||
官方 SDK 不包含此值。本地包中引用了 `TargetPlatform.ohos`,在官方 SDK 下会编译报错。
|
||
|
||
**MacBook Pro 端的解决方案**:使用远程版本的三方库,远程版本不含 `TargetPlatform.ohos`,无需关心此问题。
|
||
|
||
### 4.2 lib/ 代码中的平台判断
|
||
|
||
`lib/` 目录下的项目代码使用 `platform_utils.dart` 进行平台判断,**不依赖 `TargetPlatform.ohos` 枚举**,
|
||
在官方 SDK 下编译完全正常:
|
||
|
||
```dart
|
||
// lib/ 代码使用 Platform.operatingSystem 字符串比较,不依赖枚举
|
||
// platform_io_native.dart:
|
||
bool _isOhos() {
|
||
try {
|
||
return Platform.operatingSystem == 'ohos'; // 字符串比较,官方SDK也支持
|
||
} catch (_) {
|
||
return false;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.3 iOS/macOS 开发者规范
|
||
|
||
**规则1:不要使用 `switch(TargetPlatform)` 穷举匹配**
|
||
|
||
```dart
|
||
// ❌ 错误 — 官方SDK没有 ohos,鸿蒙SDK缺少 ohos 会报错
|
||
switch (defaultTargetPlatform) {
|
||
case TargetPlatform.iOS:
|
||
// ...
|
||
case TargetPlatform.android:
|
||
// ...
|
||
case TargetPlatform.macOS:
|
||
// ...
|
||
case TargetPlatform.windows:
|
||
// ...
|
||
case TargetPlatform.linux:
|
||
// ...
|
||
case TargetPlatform.fuchsia:
|
||
// ...
|
||
}
|
||
|
||
// ✅ 正确 — 使用 if-else 或添加 default
|
||
if (defaultTargetPlatform == TargetPlatform.iOS) {
|
||
// iOS 逻辑
|
||
} else if (defaultTargetPlatform == TargetPlatform.macOS) {
|
||
// macOS 逻辑
|
||
} else {
|
||
// 其他平台
|
||
}
|
||
|
||
// ✅ 正确 — switch 加 default
|
||
switch (defaultTargetPlatform) {
|
||
case TargetPlatform.iOS:
|
||
case TargetPlatform.android:
|
||
return mobileLayout;
|
||
default:
|
||
return desktopLayout;
|
||
}
|
||
```
|
||
|
||
**规则2:使用 `platform_utils.dart` 判断平台**
|
||
|
||
```dart
|
||
import 'package:xianyan/core/utils/platform_utils.dart' as pu;
|
||
|
||
// ✅ 推荐方式 — 内部使用字符串比较,两端都安全
|
||
if (pu.isIOS) { /* iOS */ }
|
||
if (pu.isMacOS) { /* macOS */ }
|
||
if (pu.isOhos) { /* 鸿蒙 */ }
|
||
if (pu.isMobile) { /* 移动端 */ }
|
||
if (pu.isDesktop) { /* 桌面端 */ }
|
||
```
|
||
|
||
### 4.4 已知的 TargetPlatform.ohos 适配点(仅本地包)
|
||
|
||
以下文件在本地包中引用了 `TargetPlatform.ohos`,MacBook Pro 端使用远程版本不受影响:
|
||
|
||
| 文件 | 位置 | 引用类型 |
|
||
|---|---|---|
|
||
| `packages/flex_color_picker/.../picker_functions.dart` | L49, L65 | `case TargetPlatform.ohos:` |
|
||
| `packages/flutter_quill/.../raw_editor_state.dart` | L309 | `case TargetPlatform.ohos:` |
|
||
| `packages/flutter_quill/.../link.dart` | L58 | `case TargetPlatform.ohos:` |
|
||
| `packages/flutter_local_notifications/.../plugin.dart` | 8处 | `== TargetPlatform.ohos` |
|
||
| `packages/mobile_scanner/.../method_channel.dart` | 3处 | `== / != TargetPlatform.ohos` |
|
||
| `packages/audioplayers/.../audioplayer.dart` | L178 | `!= TargetPlatform.ohos` |
|
||
|
||
> 以上文件均在 `packages/` 目录中,MacBook Pro 端使用远程版本,不会编译这些文件。
|
||
|
||
### 4.5 lib/ 代码中的鸿蒙SDK类型桥接
|
||
|
||
鸿蒙端本地包中某些类有官方 SDK 不存在的额外参数或类型(如 `OhosInitializationSettings`、`ohosName`)。
|
||
项目通过桥接文件和 `dynamic` 调用隔离这些差异,确保两端编译正常。
|
||
|
||
#### 4.5.1 通知服务桥接
|
||
|
||
**桥接文件**:`lib/core/services/notification/notification_init_stub.dart`
|
||
|
||
| 方法 | 官方SDK行为 | 鸿蒙端行为 |
|
||
|---|---|---|
|
||
| `buildNotificationInitSettings()` | 构建 `InitializationSettings`(无 ohos 参数) | 鸿蒙端本地包的 `InitializationSettings` 自带 ohos 参数,官方端不传即可 |
|
||
| `requestOhosNotificationPermission()` | 返回 `false`(不执行) | 动态调用 `OhosFlutterLocalNotificationsPlugin` |
|
||
|
||
**使用此桥接的文件**:
|
||
- `lib/core/services/notification/notification_service.dart`
|
||
- `lib/core/services/notification/local_notification_service.dart`
|
||
- `lib/features/file_transfer/services/notification_service.dart`
|
||
|
||
#### 4.5.2 HomeWidget 桥接
|
||
|
||
鸿蒙端本地包的 `HomeWidget.updateWidget()` 和 `HomeWidget.requestPinWidget()` 有 `ohosName` 参数,
|
||
官方 SDK 不存在此参数。项目通过 `pu.isOhos` 条件 + `dynamic` 调用隔离:
|
||
|
||
```dart
|
||
// 官方SDK:标准调用
|
||
await HomeWidget.updateWidget(
|
||
androidName: type.androidProviderName,
|
||
iOSName: type.iosWidgetKind,
|
||
);
|
||
|
||
// 鸿蒙端:dynamic 调用,传入 ohosName
|
||
if (pu.isOhos) {
|
||
final dynamic updateWidget = HomeWidget.updateWidget;
|
||
await updateWidget(
|
||
androidName: type.androidProviderName,
|
||
iOSName: type.iosWidgetKind,
|
||
ohosName: type.ohosFormName,
|
||
);
|
||
}
|
||
```
|
||
|
||
**涉及文件**:
|
||
- `lib/core/services/data/home_widget_service.dart`
|
||
- `lib/features/widget/providers/widget_provider.dart`
|
||
|
||
#### 4.5.3 新增鸿蒙SDK特有类型的规范
|
||
|
||
如果鸿蒙端本地包新增了官方 SDK 不存在的类型或参数,按以下规范处理:
|
||
|
||
1. **在 `lib/` 代码中不直接 import 鸿蒙专用类型**(如 `OhosInitializationSettings`)
|
||
2. **使用 `pu.isOhos` 条件分支**:鸿蒙端逻辑仅在 `pu.isOhos` 为 true 时执行
|
||
3. **使用 `dynamic` 调用**:绕过官方 SDK 的静态类型检查
|
||
4. **优先创建桥接文件**:将鸿蒙特有逻辑封装在独立文件中(如 `notification_init_stub.dart`)
|
||
|
||
---
|
||
|
||
## 五、其他注意事项
|
||
|
||
### 5.1 应用入口架构差异
|
||
|
||
项目在 `app.dart` 中根据平台选择不同的应用架构:
|
||
|
||
| 平台 | 应用架构 | 导航方式 | 入口Widget |
|
||
|---|---|---|---|
|
||
| iOS/macOS/Android | `MaterialApp.router` + GoRouter | GoRouter 声明式路由 | `AppShell` |
|
||
| 鸿蒙 | `MaterialApp(home:)` + Navigator | `CupertinoTabView` + Navigator.push | `OhosAppShell` |
|
||
|
||
**原因**:鸿蒙端 `MaterialApp.router` 白屏,无法使用 GoRouter,因此使用传统 Navigator 导航。
|
||
|
||
### 5.2 路由注册双写
|
||
|
||
新增页面时,必须在两处注册路由:
|
||
|
||
1. **`lib/core/router/app_router.dart`** — GoRouter 路由表(iOS/macOS/Android 使用)
|
||
2. **`lib/core/router/ohos_nav_bridge.dart`** — 鸿蒙路由映射表
|
||
|
||
```dart
|
||
// app_router.dart
|
||
GoRoute(
|
||
path: AppRoutes.newPage,
|
||
name: 'new-page',
|
||
parentNavigatorKey: rootNavigatorKey,
|
||
pageBuilder: (context, state) =>
|
||
iosSlideTransition(state: state, child: const NewPage()),
|
||
),
|
||
|
||
// ohos_nav_bridge.dart
|
||
AppRoutes.newPage: (_) => const NewPage(),
|
||
```
|
||
|
||
### 5.3 平台特性检测
|
||
|
||
使用 `OhosDeviceCapabilities` 检测鸿蒙设备特性(毛玻璃、液态玻璃、重度动画、折叠屏等),
|
||
iOS/macOS 端这些检测不会执行(`isOhos` 为 false),无需关心。
|
||
|
||
### 5.4 packages 目录说明
|
||
|
||
- `packages/` 目录存放鸿蒙适配的本地三方库,已在 `.gitignore` 中排除(`/packages/`)
|
||
- MacBook Pro 端使用远程版本,无需 `packages/` 目录
|
||
- 鸿蒙开发者需手动维护本地 `packages/` 目录
|
||
- ⚠️ `pubspec.yaml` 已加入 `.gitignore`,不再提交到 Git
|
||
- 鸿蒙端模板:`pubspec.ohos.yaml`,MacBook Pro 端模板:`pubspec.macos.yaml`
|
||
- 使用 `tools/setup_pubspec.ps1` 生成 `pubspec.yaml`
|
||
|
||
### 5.5 MacBook Pro 修改 ios/macos 后,鸿蒙端是否需要同步?
|
||
|
||
| 修改内容 | 鸿蒙端是否需要同步 | 原因 |
|
||
|---|---|---|
|
||
| `ios/` 目录 | ❌ 不需要 | 平台原生代码,鸿蒙不使用 |
|
||
| `macos/` 目录 | ❌ 不需要 | 平台原生代码,鸿蒙不使用 |
|
||
| `lib/` 目录 | ✅ 需要同步 | Dart 代码所有平台共用 |
|
||
| `pubspec.ohos.yaml` | ✅ 鸿蒙端模板 | 鸿蒙端依赖配置 |
|
||
| `pubspec.macos.yaml` | ✅ MacBook Pro端模板 | MacBook Pro端依赖配置 |
|
||
| `pubspec.yaml` | ❌ 不提交 | 已加入 .gitignore,本地生成 |
|
||
| `assets/` 目录 | ✅ 需要同步 | 资源文件共享 |
|
||
|
||
> **实际案例**:`bitsdojo_window → window_manager` 迁移时,MacBook Pro 端先完成代码和依赖替换,
|
||
> 鸿蒙端随后同步更新 `pubspec.yaml`(移除 `bitsdojo_window`,添加 `window_manager`),
|
||
> 并删除 `packages/bitsdojo_window_windows/` 废弃目录。
|
||
|
||
> **⚠️ 特殊案例:桌面端增强库(tray_manager / macos_window_utils / flutter_acrylic)**
|
||
>
|
||
> 这三个库仅 macOS/Windows/Linux 调用原生 API,鸿蒙端运行时 no-op(`pu.isDesktop` 守卫)。
|
||
> 但 Dart 编译时**静态解析 import 链**:`app.dart` → `desktop_service_registry.dart` → 实现文件 → `package:tray_manager/...`
|
||
>
|
||
> 鸿蒙端 `pubspec.ohos.yaml` **必须声明这三个库**,否则编译报 `Target of URI doesn't exist`。
|
||
> 运行时不会调用原生 API,无副作用。
|
||
|
||
### 5.6 常见问题
|
||
|
||
| 问题 | 原因 | 解决方案 |
|
||
|---|---|---|
|
||
| `flutter pub get` 报 packages/xxx 目录不存在 | pubspec.yaml 是鸿蒙端模板 | MacBook Pro端运行 `.\tools\setup_pubspec.ps1 -Platform macos` |
|
||
| 编译报 `TargetPlatform.ohos` 不存在 | 使用了含 ohos 引用的本地包 | 确认使用 `pubspec.macos.yaml` 生成的 pubspec.yaml |
|
||
| iOS 编译报 ohos 相关错误 | 误用鸿蒙SDK编译iOS | 切换到官方 Flutter SDK |
|
||
| GoRouter 路由正常但鸿蒙端白屏 | 鸿蒙端不支持 GoRouter | 检查 OhosNavBridge 路由映射 |
|
||
| 新增依赖后另一端报错 | 只更新了一个模板 | 必须同时更新两个模板 + 文档,参见 §2.4 |
|
||
| 新增依赖后鸿蒙端报错 | 新增的三方库未适配鸿蒙 | 通知鸿蒙开发者评估,必要时本地化到 packages/ |
|
||
| 编译报 `OhosInitializationSettings` 不存在 | 官方SDK无此类型 | 使用 `notification_init_stub.dart` 桥接,参见 §4.5 |
|
||
| 编译报 `ohosName` 参数不存在 | 官方SDK的 HomeWidget 无此参数 | 使用 `dynamic` 调用,参见 §4.5.2 |
|
||
| `pro_image_editor` 报 `CanvasStyleModel` 不存在 | 远程版本不含魔改内容 | 使用本地包 `path: packages/pro_image_editor`,参见 §2.8.1 |
|
||
| `FilePicker.platform` 报错 | file_picker 11.x API 变更 | 使用 `FilePicker.pickFiles()`,参见 §2.8.2 |
|
||
| 鸿蒙端报 `Target of URI doesn't exist: package:tray_manager/...` | `pubspec.ohos.yaml` 缺桌面端库声明 | 在 `pubspec.ohos.yaml` 添加 `tray_manager`/`macos_window_utils`/`flutter_acrylic`,参见 §5.5 特殊案例 |
|
||
|
||
---
|
||
|
||
## 六、快速检查清单
|
||
|
||
MacBook Pro 开发前,确认以下事项:
|
||
|
||
- [ ] 使用官方 Flutter SDK(非 flutter-ohos)
|
||
- [ ] 已 `git clone` 拉取最新代码
|
||
- [ ] `flutter pub get` 无报错
|
||
- [ ] `dart analyze lib/` 无 error
|
||
- [ ] 新增代码未使用 `switch(TargetPlatform)` 穷举匹配
|
||
- [ ] 新增鸿蒙SDK特有类型已通过桥接文件隔离(参见 §4.5)
|
||
- [ ] 新增路由已在 `app_router.dart` 和 `ohos_nav_bridge.dart` 双写
|
||
- [ ] 依赖变更已同步更新两个模板文件
|
||
- [ ] Git 提交未删除 `pu.isOhos` 相关代码
|
||
|
||
---
|
||
|
||
*文档创建时间: 2026-05-21 | 更新时间: 2026-06-15 v11 | 维护者: 闲言APP开发团队*
|
||
*更新内容: 同步三方库升级;删除custom_lint/riverpod_lint;新增analyzer/test_api/test/xml/pointycastle overrides;record降级到^6.2.1;更新差异对照表*
|