Files
xianyan/iOS_macOS_Developer_Guide.md
Developer ae6804e8bd refactor: 兼容后端返回数字类型波动,清理废弃代码
主要变更:
1.  全局修复类型转换问题,将多处`as int?`改为`(num?)?.toInt()`兼容浮点/字符串类型的数字字段
2.  移除废弃的nearby_p2p配对方式和对应的依赖包
3.  优化鸿蒙端快捷方式、引导页、路由导航的稳定性
4.  合并日志输出避免鸿蒙端IDE卡顿
5.  修复安卓端蓝牙权限冗余声明
2026-06-07 08:04:38 +08:00

42 KiB
Raw Blame History

iOS / macOS 开发者指南

本项目同时支持 iOS、macOS、Android、鸿蒙(HarmonyOS) 四端。 鸿蒙端使用定制 Flutter SDKflutter-ohos),在 TargetPlatform 枚举中新增了 TargetPlatform.ohos 并对大量三方库做了本地化适配。iOS/macOS 开发者需了解以下关键事项,避免踩坑。


文档更新日志

日期 版本 变更内容
<<<<<<< Updated upstream
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-06 | v8 | 移除 nearby_connections 库及P2P功能本地stub包影响Android构建更新差异对照表新增 §2.8.7 app_tracking_transparency说明 |

Stashed changes | 2026-06-02 | v7 | 重大变更pubspec.yaml 拆分为双模板pubspec.ohos.yaml + pubspec.macos.yamlpubspec.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

# 2. 运行脚本生成 pubspec.yamlMacBook Pro 端)
.\tools\setup_pubspec.ps1 -Platform macos
# 或自动检测平台
.\tools\setup_pubspec.ps1

# 3. 获取依赖
flutter pub get

# 4. iOS 编译验证
flutter build ios --no-codesign

# 5. macOS 编译验证
flutter build macos

鸿蒙端开发者:

# 1. 确认使用 flutter-ohos SDK
flutter --version   # 应显示 ohos 版本

# 2. 运行脚本生成 pubspec.yaml鸿蒙端
.\tools\setup_pubspec.ps1 -Platform 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.yamlMacBook 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.yamlMacBook Pro端
shared_preferences path: packages/shared_preferences ^2.5.5
flutter_secure_storage path: packages/flutter_secure_storage ^10.2.0
hive_flutter path: packages/hive_flutter ^1.1.0
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 ^21.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.1
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.4.0
audioplayers path: packages/audioplayers ^6.5.0
record path: packages/record ^6.0.0
video_compress path: packages/video_compress ^3.1.2
video_player path: packages/video_player ^2.10.0
local_auth path: packages/local_auth ^3.0.1
sensors_plus path: packages/sensors_plus ^6.1.0
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.1.4
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.0
speech_to_text path: packages/speech_to_text ^7.4.0
live_activities path: packages/live_activities ^2.4.9
dependency_overrides 46 行(含本地包覆盖 + ohos 子包) 5 行(仅版本号覆盖 + win32 + quill_native_bridge_windows

2.4 ⚠️ 新增三方库变更流程(必读)

铁律:新增三方库时,必须同时更新两个模板 + 本文档。

新增三方库流程:
1. 在 pubspec.ohos.yaml 添加依赖(鸿蒙端优先)
2. 在 pubspec.macos.yaml 添加对应远程版本
3. 在本文档 §2.3 差异对照表添加一行
4. 在本文档顶部更新日志记录变更
5. 在 CHANGELOG.md 记录变更
6. git push 后通知另一端开发者
7. 另一端开发者: git pull → 运行 setup_pubspec.ps1 → flutter pub get

鸿蒙端新增本地化包时额外步骤:

  • 将包放入 packages/ 目录
  • pubspec.ohos.yamldependency_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.yamlpubspec.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_pickerAPI 变更 + win32 6.x 兼容)

file_picker 已从 ^11.0.0 升级为 ^12.0.0-beta.5,原因:

  1. API 变更11.x → 12.x 通用):
// ❌ 旧版 APIfile_picker ^8.x
final result = await FilePicker.platform.pickFiles();

// ✅ 新版 APIfile_picker ^11.x / ^12.x
final result = await FilePicker.pickFiles();
  1. 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.2.0,其 Windows 平台实现兼容 win32 ^6.0.1 解决了之前版本与 win32 6.x 的编译冲突。

注意:鸿蒙端本地包版本为 9.2.4-ohos.1(基于 9.x 适配), 与 MacBook Pro 端远程版本 10.2.0 存在主版本号差异。 两端 API 兼容,lib/ 代码无需特殊处理。

2.8.4 receive_sharing_intentgitcode 引用)

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_vibrategitcode 引用)

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_widgetpub.dev 远程版本)

home_widget 的 gitcode 版本依赖 path_provider 的 git 版本,会与远程 path_provider 冲突。 MacBook Pro 端使用 pub.dev 版本 ^0.9.1,鸿蒙端的 ohosName 参数通过 pu.isOhos + dynamic 调用隔离:

# MacBook Pro 端使用 pub.dev 远程版本
  home_widget: ^0.9.1

2.8.7 nearby_connections已移除

nearby_connectionsGoogle 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 ⚠️ pub cache 补丁MacBook Pro 端必读)

关键问题dependency_overrideswin32: ^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 APITEXT()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

# 2. 运行补丁脚本(见 §2.6.3
bash scripts/patch_pub_cache.sh

# 3. 验证构建
flutter build macos
flutter build ios --no-codesign

⚠️ flutter cleanflutter 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, ...) NULLnullnullptrnullPCWSTR? 类型)
HGLOBALint HGLOBALextension type const HGLOBAL(Pointer _) 需类型转换
HANDLEint HANDLEextension type const HANDLE(Pointer _) 需类型转换

类型转换HANDLEHGLOBALimplements Pointer,可互相转换:

  • HANDLE → HGLOBALHGLOBAL(handle)
  • HGLOBAL → HANDLEHANDLE(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.yamlMacBook Pro 端已替换为远程版本,绝不能提交
  • packages/ 目录(已在 .gitignore 中排除)
  • ohos/ 目录下的鸿蒙原生代码
  • 鸿蒙SDK特有的配置文件

⚠️ pubspec.yaml 提交铁律:

  1. pubspec.yaml 已加入 .gitignore,不应提交 — 由 setup_pubspec.ps1 脚本从模板生成
  2. 修改依赖时编辑模板文件pubspec.macos.yamlMacBook Pro 端)或 pubspec.ohos.yaml(鸿蒙端)
  3. 模板文件必须提交 — 确保两端依赖配置同步

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 其他合并注意事项

  1. 不要删除 pu.isOhos 相关代码:这些条件分支在 iOS/macOS 上不会执行,但删除会导致鸿蒙端编译失败
  2. 不要修改 OhosAppShellOhosNavBridge 等鸿蒙专用类:这些类仅在鸿蒙端使用
  3. 新增页面路由时:需同时在 app_router.dartGoRouter路由表ohos_nav_bridge.dart(鸿蒙路由映射表)中注册

3.5 PR 审查要点

  • 未提交 pubspec.yaml(已加入 .gitignore由脚本生成
  • 依赖变更已同步更新 pubspec.macos.yamlpubspec.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 下编译完全正常:

// 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.ohosMacBook 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 不存在的额外参数或类型(如 OhosInitializationSettingsohosName)。 项目通过桥接文件和 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 调用隔离:

// 官方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 — 鸿蒙路由映射表
// 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.yamlMacBook 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 路由映射
git pull 后 pubspec.yaml 被覆盖 pubspec.yaml 已在 .gitignore 重新运行 .\tools\setup_pubspec.ps1
新增依赖后另一端报错 只更新了一个模板 必须同时更新两个模板 + 文档,参见 §2.4
新增依赖后鸿蒙端报错 新增的三方库未适配鸿蒙 通知鸿蒙开发者评估,必要时本地化到 packages/
编译报 OhosInitializationSettings 不存在 官方SDK无此类型 使用 notification_init_stub.dart 桥接,参见 §4.5
编译报 ohosName 参数不存在 官方SDK的 HomeWidget 无此参数 使用 dynamic 调用,参见 §4.5.2
pro_image_editorCanvasStyleModel 不存在 远程版本不含魔改内容 使用本地包 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 拉取最新代码
  • 已运行 .\tools\setup_pubspec.ps1 -Platform macos 生成 pubspec.yaml
  • flutter pub get 无报错
  • dart analyze lib/ 无 error
  • 新增代码未使用 switch(TargetPlatform) 穷举匹配
  • 新增鸿蒙SDK特有类型已通过桥接文件隔离参见 §4.5
  • 新增路由已在 app_router.dartohos_nav_bridge.dart 双写
  • 依赖变更已同步更新两个模板文件
  • Git 提交未删除 pu.isOhos 相关代码

文档创建时间: 2026-05-21 | 更新时间: 2026-06-07 v10 | 维护者: 闲言APP开发团队 更新内容: 修正dependency_overrides行数、补丁引用简化pro_image_editor和pubspec.yaml处理策略与双模板机制对齐