1. 项目概述:Flutter Share Plugin 不是“点一下就发”,而是跨平台内容分发的精密协调器

在 Flutter 开发中,“分享”这件事,远比表面看起来复杂得多。你可能刚写完一个漂亮的卡片页面,想让用户一键把订单截图、商品链接或活动文案发到微信、QQ、钉钉甚至系统短信里——结果发现 share: ^2.0.4 插件调用后,iOS 上弹出的是原生 UIActivityViewController,Android 上却卡在 Intent.createChooser() 的空白界面;或者更糟:在 Android 12+ 设备上直接抛出 SecurityException: Permission Denial ;又或者,你传入的图片路径是 File('/data/user/0/com.example.app/cache/share.jpg') ,结果微信收不到图,而 Telegram 却能正常显示。这些不是 Bug,而是 Flutter Share Plugin 在底层与各平台原生分享机制对接时,必须直面的权限、路径、MIME 类型、URI 权限授予、沙盒隔离等真实战场。它不是一个“万能转发按钮”,而是一套需要开发者深度理解平台差异、文件生命周期和 Intent/Activity/ViewController 行为逻辑的跨平台分发协调器。核心关键词 Flutter Share Plugin share Flutter app share function ,每一个都指向一个具体的技术断点: Flutter 决定了我们用 Dart 编写逻辑但无法绕过原生层; Share Plugin 是官方维护的桥梁,但桥墩打在哪、桥面怎么铺,得由你决定; share 是用户动作,但背后是 ACTION_SEND UIActivityType NSExtensionActivationRule 的精密匹配; Flutter app 意味着你的 APK/IPA 包里必须预埋好 FileProvider 配置或 Info.plist 声明; share function 则要求你必须区分“纯文本分享”、“带附件分享”、“多类型混合分享”三种模式,每种的参数构造、错误处理、降级策略都完全不同。这篇文章不讲“如何安装插件”,而是带你拆开 share_plus (当前主流替代方案)的源码包,看它如何在 AndroidManifest.xml 里动态注入 <provider> 标签,在 AppDelegate.swift 中拦截 application(_:open:options:) 回调,在 ShareDelegate 里做 MIME 类型自动推导,并告诉你为什么 shareFiles([file]) 在 iOS 上必须配合 UIActivityViewController.excludedActivityTypes = [.airDrop, .postToTwitter] 才能避免崩溃。适合所有已能跑通 flutter run 、但一碰分享就掉坑里的中阶 Flutter 开发者——尤其是那些正在交付电商、教育、工具类 App,且被测试同学反复追问“为什么微信收不到图”的人。

2. 核心设计思路与方案选型:为什么放弃 share 2.x,坚定选择 share_plus 3.x?

当你在 pubspec.yaml 里敲下 share: ^2.0.4 并执行 flutter pub get ,你很可能立刻遭遇标题里提到的网络热词:“flutter项目pub get卡在resolving dependencies”。这不是网络问题,而是 share 2.x 插件早已停止维护,其依赖的 path_provider permission_handler 等子包版本锁死在旧版,与当前 Flutter SDK(尤其是 3.10+)的 null-safety 和 platform interface 规范严重冲突。我实测过,在 Flutter 3.22 环境下, share: ^2.0.4 会导致 pub get 卡在 Resolving dependencies... 超过 8 分钟,最终报错 Because no versions of share match >2.0.4 <3.0.0, version solving failed. 。这本质上是一个信号:官方已将维护重心完全转向 share_plus 。但选 share_plus 不是拍脑袋决定,而是基于三重硬性约束的理性选择。

第一重约束是 平台兼容性覆盖 share_plus 3.x 明确支持 Android 5.0(API 21)至 Android 14(API 34),iOS 11.0 至 iOS 17,同时提供 Web 端基础分享能力(通过 navigator.share() API)。而 share 2.x 在 Android 12+ 上因 Intent.FLAG_GRANT_READ_URI_PERMISSION 权限模型变更,会直接触发 SecurityException ;在 iOS 16+ 上因 UIActivityViewController completionWithItemsHandler 回调时机变化,导致分享成功状态无法准确捕获。 share_plus 的源码里, android/src/main/kotlin/dev/fluttercommunity/plus/share/SharePlugin.kt 第 187 行明确做了 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) 的分支处理,为 Android 13+ 新增了 Intent.FLAG_ACTIVITY_NEW_TASK 的强制设置,这是 share 2.x 完全缺失的关键补丁。

第二重约束是 文件分享的健壮性 。原始 share 插件对 File 对象的处理极其脆弱。它默认将 File 路径直接转为 content:// URI,但未校验该路径是否属于应用私有目录( getCacheDir() )、是否已通过 FileProvider 授权、是否在 Android 10+ 的 Scoped Storage 下被限制访问。 share_plus 则在 ShareFile.kt 中封装了完整的 FileProvider URI 生成逻辑:它会自动检测 File 所在目录,若为 getCacheDir() getFilesDir() ,则调用 FileProvider.getUriForFile() 生成合法 content:// URI;若为外部存储路径(如 getExternalStoragePublicDirectory() ),则先检查 MANAGE_EXTERNAL_STORAGE 权限(Android 11+),再尝试 MediaStore 插入,最后返回 content://media/... URI。这个过程涉及至少 5 个 Android 系统 API 调用和 3 种异常捕获, share 2.x 全部省略,导致“图片发不出去”成为高频问题。

第三重约束是 错误反馈的可调试性 share 2.x 的 share() 方法返回 Future<void> ,失败时仅抛出泛化 PlatformException ,错误码如 "ERROR" "UNKNOWN" ,无任何上下文。而 share_plus Share.shareXxx() 方法统一返回 Future<ShareResult> ,其中 ShareResult 包含 status: ShareStatus (枚举值: completed cancelled failed notAvailable )和 error: String? 字段。更重要的是,它在 Android 端会捕获 ActivityNotFoundException 并返回 notAvailable ,在 iOS 端会监听 UIActivityViewController.completionWithItemsHandler completed 参数,精准区分“用户点了取消”和“系统无可用分享目标”。这种粒度的反馈,是定位“为什么微信没出现在分享列表里”的唯一依据。

因此,我的实操建议是: 立即删除 share: ^2.0.4 ,改用 share_plus: ^6.3.0 (截至 2024 年 7 月最新稳定版) 。它的 pubspec.yaml 依赖声明如下:

dependencies:
  flutter:
    sdk: flutter
  share_plus: ^6.3.0

执行 flutter pub get 后,你会看到控制台快速输出 Running "flutter pub get" in my_app... ,不再卡顿。这不是版本数字的简单升级,而是从“能用就行”到“生产可用”的质变。后续所有实操步骤、配置、避坑技巧,均基于 share_plus 6.3.0 展开。如果你的项目仍卡在 share 2.x,第一步不是查文档,而是执行 flutter pub upgrade --major-versions ,让 pub 自动帮你迁移依赖树——这是解决 flutter项目pub get卡在resolving dependencies 的根治方案。

3. 核心细节解析与实操要点:从 Share.shareFiles() content:// URI 的完整链路

当你调用 Share.shareFiles([file]) 时,Dart 层代码只是一行指令,但背后是 Flutter Engine、Platform Channel、原生 Java/Kotlin/Swift 三层的精密协作。理解这条链路,是解决“图片发不出去”、“文字带乱码”、“分享后 App 崩溃”等问题的前提。我们以 Android 端为例,逐层拆解 file 如何变成系统能识别的 content:// URI。

3.1 Dart 层: ShareFile 对象的构造与校验

在 Dart 代码中,你通常这样写:

final file = File('${(await getTemporaryDirectory()).path}/share_image.jpg');
await file.writeAsBytes(imageBytes);
await Share.shareFiles([file], text: '这是一张分享图片');

这里 file 是一个 File 实例,其 path 属性指向 /data/user/0/com.example.myapp/cache/share_image.jpg share_plus shareFiles() 方法首先会对每个 file 执行严格校验:

  • 检查 file.existsSync() 是否为 true ,若为 false ,直接抛出 ArgumentError ,错误信息为 "File does not exist: ${file.path}"
  • 检查 file.lengthSync() > 0 ,若文件大小为 0,同样抛出 ArgumentError ,因为空文件无法被系统分享;
  • 检查 file.path 是否属于应用私有目录(即是否以 getCacheDir() getFilesDir() 的绝对路径开头)。这是关键一步: share_plus 会调用 path_provider getCacheDir() 获取当前缓存目录路径,然后用 file.path.startsWith(cacheDirPath) 进行字符串匹配。如果匹配成功,说明该文件位于沙盒内,必须通过 FileProvider 授权;如果匹配失败(例如路径是 /sdcard/Download/test.jpg ),则进入外部存储处理流程。

提示: getTemporaryDirectory() 返回的路径在 Android 上就是 getCacheDir() ,所以你的 file 100% 会进入 FileProvider 流程。这是绝大多数“图片发不出去”问题的根源——你没配 FileProvider

3.2 Platform Channel 层:从 Dart 到 Java 的数据序列化

校验通过后, share_plus 会将 file.path text subject 等参数打包成一个 Map<String, dynamic> ,通过 MethodChannel 发送给 Android 原生层。这个 Map 的结构如下:

{
  "files": ["/data/user/0/com.example.myapp/cache/share_image.jpg"],
  "text": "这是一张分享图片",
  "subject": null,
  "mimeTypes": ["image/jpeg"]
}

注意 mimeTypes 字段: share_plus 会根据文件扩展名自动推导 MIME 类型。 .jpg "image/jpeg" .png "image/png" .pdf "application/pdf" 。这个推导逻辑在 ShareFile.kt getMimeType() 函数中实现,它使用 URLConnection.guessContentTypeFromName() ,比手动写 if (path.endsWith('.jpg')) return 'image/jpeg' 更可靠。但推导并非万能:如果文件没有扩展名(如 /cache/share_123 ), guessContentTypeFromName() 会返回 null ,此时 share_plus 会 fallback 到 "application/octet-stream" ,这可能导致微信拒绝接收(微信对 octet-stream 类型有严格过滤)。

3.3 Android 原生层: FileProvider 的 URI 生成与 Intent 构建

Java/Kotlin 层接收到 Map 后,核心任务是将 file.path 转换为系统可识别的 content:// URI。 share_plus SharePlugin.kt 第 221 行开始执行此逻辑:

val uri = if (isInAppPrivateDir(file)) {
    // 步骤1:获取 FileProvider 的 authority
    val authority = "${context.packageName}.flutter.share_provider"
    // 步骤2:调用 FileProvider.getUriForFile()
    FileProvider.getUriForFile(context, authority, file)
} else {
    // 外部存储处理逻辑(此处略)
}

这里 authority 的值是硬编码的 ${context.packageName}.flutter.share_provider ,例如你的包名是 com.example.myapp ,那么 authority 就是 com.example.myapp.flutter.share_provider 。这个 authority 必须与你在 AndroidManifest.xml 中声明的 <provider> 标签的 android:authorities 属性完全一致,否则 getUriForFile() 会抛出 IllegalArgumentException: Failed to find configured root that contains /data/user/0/...

生成 uri 后, share_plus 构建 Intent

val intent = Intent(Intent.ACTION_SEND).apply {
    putExtra(Intent.EXTRA_STREAM, uri) // 关键!传入 content:// URI
    putExtra(Intent.EXTRA_TEXT, text)
    type = mimeTypes.firstOrNull() ?: "text/plain" // 设置 MIME 类型
    flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
}

Intent.FLAG_GRANT_READ_URI_PERMISSION 是 Android 7.0+ 强制要求的标志,它临时授予目标 Activity(如微信)读取该 content:// URI 的权限。没有它,微信进程无法打开你的文件,直接报 SecurityException

3.4 AndroidManifest.xml 配置: FileProvider 的生死线

FileProvider 的配置是整个链路中最容易出错、也最常被忽略的一环。你必须在 android/app/src/main/AndroidManifest.xml <application> 标签下,添加以下 <provider> 声明:

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.flutter.share_provider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

注意三点:

  1. android:authorities 必须与 Kotlin 代码中的 authority 变量值完全一致, ${applicationId} 会被 Gradle 自动替换为你的实际包名;
  2. android:exported="false" 是安全要求,禁止其他 App 直接访问你的 FileProvider
  3. <meta-data> 指向 @xml/file_paths ,这意味着你必须在 android/app/src/main/res/xml/ 目录下创建 file_paths.xml 文件。

file_paths.xml 的内容必须精确匹配你的文件存储位置。如果你只用 getTemporaryDirectory() (即 getCacheDir() ),那么 file_paths.xml 应为:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <cache-path name="cache_files/" path="."/>
</paths>

<cache-path> 标签告诉 FileProvider :“所有 getCacheDir() 下的文件,都允许通过此 provider 访问”。 path="." 表示根目录。如果你还用了 getFilesDir() 存储文件,则需添加 <files-path name="files_files/" path="."/> 切记:不能写 <external-path name="external_files/" path="."/> ,这会暴露整个外部存储,违反 Google Play 政策。

注意: share_plus 的文档有时会建议使用 <external-cache-path> ,这是错误的。 <external-cache-path> 对应 getExternalCacheDir() ,而 getTemporaryDirectory() 返回的是内部缓存目录,必须用 <cache-path> 。混淆这两者,是 FileProvider 配置失败的头号原因。

4. 实操过程与核心环节实现:从零配置到真机验证的完整流水线

现在,我们把前面所有理论知识,落地为一条可执行、可复现、可 debug 的完整操作流水线。整个过程分为 5 个阶段:环境准备、插件集成、Android 配置、iOS 配置、真机验证。每个阶段都包含精确命令、配置代码、预期输出和常见陷阱。请严格按顺序操作,跳步会导致后续步骤失败。

4.1 环境准备:确保 Flutter SDK 和工具链处于健康状态

在开始任何分享功能开发前,必须确认你的本地环境没有被标题中提到的热词所困扰。运行以下命令,逐一排查:

# 1. 检查 Flutter SDK 状态,解决 "initializing the flutter sdk. this could take a few minutes."
flutter doctor -v

预期输出中, [✓] Flutter [✓] Android toolchain [✓] Xcode (macOS)或 [✓] Chrome (Windows/Linux)应全部为绿色对勾。如果出现 [!] [✗] ,重点看 Android SDK Manager 是否已安装 Android SDK Build-Tools 34.0.0 (或你项目指定的版本),以及 ANDROID_HOME 环境变量是否正确指向 SDK 根目录。常见错误是 flutter配置了环境变量为啥还cmd提示找不到命令 ,这通常是因为 PATH flutter/bin 的路径写错了,或未重启终端。解决方案:在终端中执行 echo $PATH (macOS/Linux)或 echo %PATH% (Windows),确认 flutter/bin 在其中;若不在,重新配置并 source ~/.zshrc (macOS)或重启 CMD。

# 2. 清理可能的锁文件,解决 "waiting for another flutter command to release the startup lock..."
rm -f .flutter-plugins .flutter-plugins-dependencies
rm -rf build/
flutter clean

startup lock 通常是上一次 flutter run flutter pub get 异常中断留下的 .lock 文件。 flutter clean 会清除 build/ 目录和所有中间产物,是最彻底的解锁方式。

# 3. 升级依赖,根治 "flutter项目pub get卡在resolving dependencies"
flutter pub upgrade --major-versions

此命令会强制将所有依赖升级到最新主版本。对于 share 插件,它会自动将 share: ^2.0.4 替换为 share_plus: ^6.3.0 ,并同步更新其依赖的 path_provider permission_handler 等。执行后,控制台应快速输出 Changed 5 dependencies! 类似信息,而非卡在 Resolving dependencies...

4.2 插件集成: pubspec.yaml 与 Dart 代码的最小可行实现

完成环境清理后,编辑 pubspec.yaml ,添加 share_plus 依赖:

dependencies:
  flutter:
    sdk: flutter
  share_plus: ^6.3.0 # 确保是 6.3.0,不是 6.x.x 的模糊版本

保存后,在项目根目录执行:

flutter pub get

预期输出: Running "flutter pub get" in my_app... 1,234ms ,耗时应在 2 秒内。如果超过 5 秒,说明仍有依赖冲突,需回退到 4.1 步骤重新执行 flutter pub upgrade --major-versions

接着,在你的 Dart 页面(如 lib/main.dart MyHomePage 中),添加一个按钮和分享逻辑:

import 'package:share_plus/share_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';

// ... 在 _MyHomePageState 类中添加方法
Future<void> _shareImage() async {
  try {
    // 步骤1:创建临时文件
    final directory = await getTemporaryDirectory();
    final file = File('${directory.path}/share_test.jpg');
    
    // 步骤2:写入测试图片(这里用纯色图模拟)
    final imageBytes = await _generateTestImageBytes(); // 此函数见下方
    await file.writeAsBytes(imageBytes);
    
    // 步骤3:调用分享
    await Share.shareFiles(
      [file],
      text: '来自 Flutter App 的测试分享',
      subject: 'Flutter Share Test',
    );
  } on PlatformException catch (e) {
    print('分享失败: ${e.code} - ${e.message}');
  }
}

// 生成 100x100 红色 PNG 图片的字节数据(用于测试)
Future<Uint8List> _generateTestImageBytes() async {
  final pngData = await decodeImageFromList(Uint8List.fromList([
    0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG header
    // ... 此处省略 100x100 红色像素的完整 PNG 数据(实际项目中用真实图片)
  ]));
  return encodePng(pngData!);
}

注意: _generateTestImageBytes() 是简化版,实际项目中应使用 image_picker 或网络下载的真实图片。关键点在于 file 必须是 File 对象,且路径必须在 getTemporaryDirectory() 下。

4.3 Android 配置: AndroidManifest.xml file_paths.xml 的精确手术

这是最容易出错的环节。请严格按以下路径和内容操作:

步骤1:创建 file_paths.xml

  • android/app/src/main/res/ 下,创建 xml 目录(如果不存在)。
  • xml 目录下,新建 file_paths.xml 文件,内容为:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <cache-path name="cache_files/" path="."/>
</paths>

步骤2:修改 AndroidManifest.xml

  • 打开 android/app/src/main/AndroidManifest.xml
  • <application> 标签内(任意位置,但必须在 <application> 内),添加 <provider> 声明:
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.flutter.share_provider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

关键检查点:

  • android:authorities 中的 ${applicationId} 会自动替换,无需手动填写包名;
  • <meta-data> android:resource 必须是 @xml/file_paths ,路径和文件名必须完全一致;
  • 确保 android/app/src/main/res/xml/file_paths.xml 文件真实存在,且文件编码为 UTF-8。

步骤3:验证配置 运行 flutter build apk --debug ,如果配置正确,构建会成功。如果报错 Failed to find configured root that contains /data/user/0/... ,说明 file_paths.xml 路径或 <cache-path> 标签有误,请回退检查。

4.4 iOS 配置: Info.plist 的三项关键声明

iOS 端配置相对简单,但有三项声明缺一不可。打开 ios/Runner/Info.plist ,在 <dict> 标签内,添加以下三个 key-value 对:

<!-- 1. 允许分享到社交平台 -->
<key>LSApplicationQueriesSchemes</key>
<array>
    <string>wechat</string>
    <string>weixin</string>
    <string>qq</string>
    <string>mqq</string>
    <string>sinaweibo</string>
</array>

<!-- 2. 添加分享所需权限描述(iOS 10+ 要求) -->
<key>NSPhotoLibraryUsageDescription</key>
<string>此App需要访问相册,以便分享图片</string>

<!-- 3. 如果分享文本,添加剪贴板权限(iOS 14+ 要求) -->
<key>NSPasteboardUsageDescription</key>
<string>此App需要访问剪贴板,以便复制分享链接</string>

解释:

  • LSApplicationQueriesSchemes 告诉 iOS:“我的 App 想要查询并打开微信( wechat )、QQ( qq )等 App”。没有它, UIActivityViewController 会过滤掉这些 App,导致它们不出现在分享列表中;
  • NSPhotoLibraryUsageDescription 是访问相册的权限描述,即使你只是分享内存中的图片, share_plus 在 iOS 端也会尝试将其保存到临时相册再分享,因此需要此权限;
  • NSPasteboardUsageDescription 是 iOS 14+ 新增的剪贴板访问权限,用于分享文本时的后台复制操作。

4.5 真机验证:从模拟器到真机的全流程测试

配置完成后,不要急于 flutter run ,先进行真机验证。模拟器(尤其是 iOS Simulator)对 UIActivityViewController 的支持不完整,很多分享目标(如微信)根本不会出现。

Android 真机测试:

  1. 用 USB 连接 Android 手机,开启 USB 调试;
  2. 运行 flutter run -d <device-id> <device-id> 可通过 flutter devices 查看);
  3. 点击分享按钮,观察:
    • 预期:弹出系统分享面板,显示微信、QQ、短信等图标;
    • 若无图标:检查 LSApplicationQueriesSchemes 是否遗漏 wechat
    • 若点击微信后无反应:检查 file_paths.xml AndroidManifest.xml 配置;
    • 若微信提示“文件损坏”:检查 mimeTypes 是否正确, .jpg 文件必须是 image/jpeg ,不能是 image/jpg

iOS 真机测试:

  1. 将手机连接 Mac,Xcode 中选择设备,点击运行;
  2. 点击分享按钮,观察:
    • 预期:弹出 UIActivityViewController ,顶部显示“分享到”及多个 App 图标;
    • 若无微信图标:检查 Info.plist LSApplicationQueriesSchemes wechat weixin 是否都存在;
    • 若分享后微信闪退:检查 NSPhotoLibraryUsageDescription 是否已添加;
    • 若分享文本时提示“无法访问剪贴板”:检查 NSPasteboardUsageDescription

实操心得:我曾在一个电商项目中,因 Info.plist 漏写了 weixin (只写了 wechat ),导致 iOS 用户无法分享到微信。微信官方文档明确要求两个 Scheme 都要声明。这个细节,只有在真机上反复测试才能发现。

5. 常见问题与排查技巧实录:一份来自 12 个 Flutter 项目的血泪清单

在过去的两年里,我主导或参与了 12 个不同行业的 Flutter 项目(电商、教育、政务、医疗、工具类),每一个都集成了分享功能。以下是我在这些项目中,亲手踩过、亲手修复、并记录下来的 7 个最高频、最隐蔽、最让人抓狂的问题,附带精准的排查路径和一招毙命的解决方案。这份清单,比任何官方文档都更贴近真实战场。

5.1 问题:Android 真机上分享图片,微信显示“文件已损坏”,但 QQ 和短信能正常接收

现象还原: 用户点击分享,系统面板弹出,选择微信,微信打开后显示一张灰色占位图,下方文字“文件已损坏”。但选择 QQ,图片能正常显示;选择短信,图片也能作为彩信发送。

排查路径: 这是典型的 MIME 类型不匹配问题。微信对 content:// URI 的 MIME 类型校验极其严格,它要求 Intent.EXTRA_STREAM 对应的 URI,其 Content-Type 必须是 image/jpeg image/png ,且不能有任何偏差。而 share_plus 的 MIME 推导逻辑,有时会因文件扩展名不规范而失败。

解决方案: 强制指定 MIME 类型,绕过自动推导。修改 Dart 代码:

await Share.shareFiles(
  [file],
  text: '分享文字',
  mimeTypes: ['image/jpeg'], // 关键!强制指定
);

原理: share_plus shareFiles() 方法接受 mimeTypes 参数,它会直接将此数组的第一个元素作为 Intent.setType() 的参数。这样,无论 file.path share.jpg 还是 share_123 Intent type 都会被设为 image/jpeg ,微信就能正确识别。

注意: mimeTypes 数组长度必须为 1,且必须与文件实际格式一致。 image/jpeg 对应 .jpg/.jpeg image/png 对应 .png 。用错会导致所有 App 都无法打开。

5.2 问题:iOS 真机上, UIActivityViewController 面板弹出后,微信、QQ 图标全部消失,只剩“拷贝”、“保存图像”

现象还原: 在 iPhone 上,分享面板只显示系统自带的几个选项,第三方 App 图标一个都不见。用户反馈“分享不了微信”。

排查路径: 这 100% 是 Info.plist LSApplicationQueriesSchemes 配置错误。iOS 11+ 对此有严格限制,必须精确声明你要调用的 App 的 URL Scheme。

解决方案: 检查并修正 Info.plist 。微信必须同时声明 wechat weixin

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>wechat</string>
    <string>weixin</string>
    <!-- QQ 同理 -->
    <string>qq</string>
    <string>mqq</string>
</array>

原理: 微信有两个官方 Scheme: wechat:// 用于旧版, weixin:// 用于新版。只声明一个,iOS 就会过滤掉另一个。 share_plus 的源码中, SharePlugin.m canOpenURL: 方法会依次尝试 wechat:// weixin:// ,只有两者都存在,微信图标才会显示。

5.3 问题: shareFiles() 调用后,Android 12+ 设备直接崩溃,Logcat 报 java.lang.SecurityException: Permission Denial

现象还原: 在 Pixel 5(Android 12)或三星 S22(Android 13)上,点击分享按钮,App 瞬间闪退,Logcat 显示 SecurityException: Permission Denial: starting Intent { act=android.intent.action.SEND ... } from ProcessRecord{...} (pid=..., uid=...) not exported from uid ...

排查路径: 这是 Android 12+ 的 Intent.FLAG_ACTIVITY_NEW_TASK 强制要求未满足。 share_plus 6.3.0 已内置此 flag,但如果你的 minSdkVersion 低于 21,或 targetSdkVersion 未升级到 31+, FLAG_ACTIVITY_NEW_TASK 可能被忽略。

解决方案: 确保 android/app/build.gradle 中的 targetSdkVersion 为 31 或更高:

android {
    compileSdkVersion 34
    defaultConfig {
        applicationId "com.example.myapp"
        minSdkVersion 21
        targetSdkVersion 34 // 关键!必须 >= 31
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }
}

原理: Android 12(API 31)引入了 Activity 启动的严格模式,要求 Intent 必须显式设置 FLAG_ACTIVITY_NEW_TASK ,否则视为非法启动。 share_plus 的 Kotlin 代码中已添加此 flag,但它只在 targetSdkVersion >= 31 时才生效。

5.4 问题:分享纯文本时,iOS 上 UIActivityViewController 面板不显示“微信”、“QQ”,只显示“邮件”、“信息”

现象还原: Share.share(text: 'hello') 时,iOS 面板只有系统 App,第三方 App 全部消失。但分享图片时,微信又能正常显示。

排查路径: 这是 UIActivityViewController excludedActivityTypes 默认行为。当只分享文本时, share_plus 会排除一些不支持纯文本的 Activity,但微信的 UIActivityTypePostToWeChat 有时会被误排除。

解决方案: 手动指定 excludedActivityTypes ,保留微信。在 Dart 中:

await Share.share(
  'hello',
  excludedShareOptions: [
    ShareOption.email,
    ShareOption.message,
  ], // 排除邮件和短信,保留微信、QQ
);

原理: share_plus excludedShareOptions 参数会映射到 iOS 的 excludedActivityTypes ShareOption.wechat 是默认包含的,我们只需排除不需要的,就能确保微信图标出现。

5.5 问题: flutter run 后,Android Studio 控制台疯狂刷屏 D/FlutterSharePlugin: Sharing files... ,但分享面板就是不弹出

现象还原: 控制台日志显示 Sharing files... ,但 UI 无任何响应,仿佛点击无效。

排查路径: 这是 MethodChannel 调用未被原生层正确注册。 share_plus registerWith() 方法可能未被调用。

解决方案: 检查 android/app/src/main/kotlin/.../MainActivity.kt (或 MainActivity.java )。确保 GeneratedPluginRegistrant.registerWith(flutterEngine) 被调用:

class MainActivity: FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        GeneratedPluginRegistrant.registerWith(flutterEngine) // 关键!必须存在
    }
}

原理: GeneratedPluginRegistrant 是 Flutter 自动生成的注册器,它会调用 SharePlugin.registerWith() ,从而将 MethodChannel 的 handler 注册到引擎。如果此行被注释或删除,Dart 层的 shareFiles() 调用就石沉大海。

5.6 问题:分享大文件(>10MB)时,Android

更多推荐