1. 替换hive_flutter为hive_ce_flutter依赖 2. 从各平台插件列表移除sqlite3_flutter_libs 3. 重构API请求体格式,优化历史记录去重逻辑 4. 新增CTC笔记相关功能:桌面小部件、模板模型、本地存储 5. 新增表单收集服务和后台管理接口 6. 优化缓存配置、多语言文案和UI细节 7. 重构首页状态监听组件
44 KiB
iOS / macOS 开发者指南
本项目同时支持 iOS、macOS、Android、鸿蒙(HarmonyOS) 四端。 鸿蒙端使用定制 Flutter SDK(
flutter-ohos),在TargetPlatform枚举中新增了TargetPlatform.ohos, 并对大量三方库做了本地化适配。iOS/macOS 开发者需了解以下关键事项,避免踩坑。
文档更新日志
| 日期 | 版本 | 变更内容 |
|---|---|---|
| 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 与双模板机制对齐 |
| 2026-06-06 | v9 | 清理未使用依赖:移除 animations、animate_do、value_layout_builder、flutter_advanced_canvas_editor、flutter_blue_plus、http_cache_file_store、dartx、vector_math;删除差异对照表中 flutter_nfc_kit 过时条目 |
| 2026-06-06 | v8 | 新增 app_tracking_transparency 差异对照条目;新增 nearby_connections 鸿蒙端本地stub包说明;新增 §2.10 nearby_connections鸿蒙适配说明 |
| 2026-06-02 | v7 | 重大变更:pubspec.yaml 拆分为双模板(pubspec.ohos.yaml + pubspec.macos.yaml),pubspec.yaml 不再提交到 Git;新增三方库变更通知机制;新增 setup_pubspec.ps1 脚本 |
| 2026-06-02 | v6 | 鸿蒙端 pubspec.yaml 同步 bitsdojo_window → window_manager 迁移;更新 file_picker 本地包版本注释(v8.3.7→v11.0.0-ohos.1);更新 speech_to_text(^7.0.0→^7.4.0)、live_activities(^2.0.0→^2.4.9) 远程版本号;补充 dependency_overrides 中 bitsdojo_window_windows 移除说明 |
| 2026-06-01 | v5 | 新增 §2.6 pub cache 补丁说明;标记 bitsdojo_window 迁移完成;file_picker 升级到 12.x |
| 2026-05-30 | v4 | 初版完整指南 |
一、环境准备与项目拉取
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 克隆仓库
# 克隆项目仓库
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
# 1. 确认使用官方 Flutter SDK
flutter --version # 应显示官方版本,非 flutter-ohos
# 3. 获取依赖
flutter pub get
# 4. iOS 编译验证
flutter build ios --no-codesign
# 5. macOS 编译验证
flutter build macos
鸿蒙端开发者:
# 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.4.0 |
| 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 脚本说明
# 鸿蒙端
.\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 端完整操作流程
# 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 鸿蒙端完整操作流程
# 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,两端均使用远程版本:
# 两端均使用远程版本
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,原因:
- API 变更(11.x → 12.x 通用):
// ❌ 旧版 API(file_picker ^8.x)
final result = await FilePicker.platform.pickFiles();
// ✅ 新版 API(file_picker ^11.x / ^12.x)
final result = await FilePicker.pickFiles();
- 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 引用:
# 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 引用:
# 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 调用隔离:
# 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 编译。
# 两端配置相同
app_tracking_transparency: ^2.0.6
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 鸿蒙端升级步骤
# 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: trueNullable<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 补丁应用流程
# 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(已包含在仓库中),内容如下:
#!/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 提交铁律:
pubspec.yaml已加入.gitignore,不应提交 — 由setup_pubspec.ps1脚本从模板生成- 修改依赖时编辑模板文件 —
pubspec.macos.yaml(MacBook Pro 端)或pubspec.ohos.yaml(鸿蒙端) - 模板文件必须提交 — 确保两端依赖配置同步
3.3 pubspec.yaml 处理策略(⭐ 重点)
双模板机制下,
pubspec.yaml由脚本生成,不提交到 Git。 MacBook Pro 端开发者无需手动替换本地包引用,也无需 git stash 隔离。
3.3.1 日常操作
# git pull 后,重新生成 pubspec.yaml 即可
.\tools\setup_pubspec.ps1 -Platform macos
flutter pub get
# 编译验证
flutter build ios --no-codesign
3.3.2 需要新增依赖时
新增依赖时,必须在两端模板分别操作:
# 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 被覆盖
# 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 其他合并注意事项
- 不要删除
pu.isOhos相关代码:这些条件分支在 iOS/macOS 上不会执行,但删除会导致鸿蒙端编译失败 - 不要修改
OhosAppShell、OhosNavBridge等鸿蒙专用类:这些类仅在鸿蒙端使用 - 新增页面路由时:需同时在
app_router.dart(GoRouter路由表)和ohos_nav_bridge.dart(鸿蒙路由映射表)中注册
3.5 PR 审查要点
- 未提交
pubspec.yaml(已加入 .gitignore,由脚本生成) - 依赖变更已同步更新
pubspec.macos.yaml和pubspec.ohos.yaml两个模板 - 未引入
TargetPlatformexhaustive 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 下编译完全正常:
// 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) 穷举匹配
// ❌ 错误 — 官方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 判断平台
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.dartlib/core/services/notification/local_notification_service.dartlib/features/file_transfer/services/notification_service.dart
4.5.2 HomeWidget 桥接
鸿蒙端本地包的 HomeWidget.updateWidget() 和 HomeWidget.requestPinWidget() 有 ohosName 参数,
官方 SDK 不存在此参数。项目通过 pu.isOhos 条件 + dynamic 调用隔离:
// 官方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.dartlib/features/widget/providers/widget_provider.dart
4.5.3 新增鸿蒙SDK特有类型的规范
如果鸿蒙端本地包新增了官方 SDK 不存在的类型或参数,按以下规范处理:
- 在
lib/代码中不直接 import 鸿蒙专用类型(如OhosInitializationSettings) - 使用
pu.isOhos条件分支:鸿蒙端逻辑仅在pu.isOhos为 true 时执行 - 使用
dynamic调用:绕过官方 SDK 的静态类型检查 - 优先创建桥接文件:将鸿蒙特有逻辑封装在独立文件中(如
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 路由注册双写
新增页面时,必须在两处注册路由:
lib/core/router/app_router.dart— GoRouter 路由表(iOS/macOS/Android 使用)lib/core/router/ohos_nav_bridge.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/废弃目录。
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 |
六、快速检查清单
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;更新差异对照表