Flutter 三方库 share_plus 的鸿蒙化适配与实战指南


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Hey 大家好!上海某高校计算机专业大一学生 📤!今天来聊聊分享功能!

做聊天 App 怎么能少了分享呢?用户看到一条有趣的消息、一张好看的图片,想分享给好友——这时候就需要系统级的分享功能!share_plus 就是来解决这个问题的!

一、share_plus 是什么?

share_plus 是一个 Flutter 插件,提供跨平台的分享功能。它可以调用系统的分享面板,让用户选择分享到微信、QQ、邮件等任何支持接收分享的应用。

特点:

  • ✅ 跨平台:iOS、Android、鸿蒙、Web 都能用
  • ✅ 系统原生分享面板
  • ✅ 支持文本、链接、图片、文件
  • ✅ 零 UI,直接调用系统功能

二、依赖配置

dependencies:
  share_plus: ^10.1.4

AtomGit 适配说明:share_plus 底层调用各平台的原生分享 API,在鸿蒙上依赖系统的分享服务,兼容性良好!

三、封装分享服务

import 'package:flutter/foundation.dart';
import 'package:share_plus/share_plus.dart';

/// 分享服务
/// 支持分享文本、链接、图片、文件到其他App
class ShareService {
  static ShareService? _instance;
  static ShareService get instance => _instance ??= ShareService._();

  ShareService._();

1. 分享文本

  /// 分享纯文本【基础方法】
  Future<bool> shareText(String text, {String? subject}) async {
    try {
      await Share.share(text, subject: subject);
      debugPrint('分享文本成功');
      return true;
    } catch (e) {
      debugPrint('分享文本失败: $e');
      return false;
    }
  }

2. 分享链接

  /// 分享链接【实用方法】
  Future<bool> shareUrl(String url, {String? text}) async {
    try {
      final shareText = text != null ? '$text\n$url' : url;
      await Share.share(shareText);
      debugPrint('分享链接成功: $url');
      return true;
    } catch (e) {
      debugPrint('分享链接失败: $e');
      return false;
    }
  }

3. 分享聊天消息

  /// 分享聊天消息【聊天场景专用】
  Future<bool> shareChatMessage({
    required String senderName,
    required String content,
    required String time,
  }) async {
    try {
      final text = '$senderName ($time):\n$content';
      await Share.share(text);
      debugPrint('分享聊天消息成功');
      return true;
    } catch (e) {
      debugPrint('分享聊天消息失败: $e');
      return false;
    }
  }

4. 分享文件

  /// 分享文件【重要方法】
  Future<bool> shareFiles(List<String> filePaths, {String? text}) async {
    try {
      // 将文件路径转换为 XFile 对象
      final xFiles = filePaths.map((path) => XFile(path)).toList();
      await Share.shareXFiles(xFiles, text: text);
      debugPrint('分享文件成功: ${filePaths.length}个文件');
      return true;
    } catch (e) {
      debugPrint('分享文件失败: $e');
      return false;
    }
  }

5. 分享图片

  /// 分享单张图片【常用方法】
  Future<bool> shareImage(String imagePath, {String? text}) async {
    try {
      await Share.shareXFiles(
        [XFile(imagePath)],
        text: text,
      );
      debugPrint('分享图片成功');
      return true;
    } catch (e) {
      debugPrint('分享图片失败: $e');
      return false;
    }
  }

  /// 分享多张图片【实用方法】
  Future<bool> shareImages(List<String> imagePaths, {String? text}) async {
    try {
      final xFiles = imagePaths.map((path) => XFile(path)).toList();
      await Share.shareXFiles(xFiles, text: text);
      debugPrint('分享多张图片成功: ${imagePaths.length}张');
      return true;
    } catch (e) {
      debugPrint('分享多张图片失败: $e');
      return false;
    }
  }

6. 分享带截图

  /// 分享文字+图片(用于分享卡片预览)
  Future<bool> shareWithPreview({
    required String title,
    required String text,
    required String imagePath,
  }) async {
    try {
      await Share.shareXFiles(
        [XFile(imagePath)],
        text: '$title\n$text',
        subject: title,
      );
      return true;
    } catch (e) {
      debugPrint('分享失败: $e');
      return false;
    }
  }

四、在聊天页面中集成

class ChatMessageBubble extends StatelessWidget {
  final ChatMessage message;
  final VoidCallback onReply;
  final VoidCallback onForward;
  final VoidCallback onCopy;
  final VoidCallback onDelete;

  const ChatMessageBubble({
    super.key,
    required this.message,
    required this.onReply,
    required this.onForward,
    required this.onCopy,
    required this.onDelete,
  });

  
  Widget build(BuildContext context) {
    return GestureDetector(
      onLongPress: () => _showMessageMenu(context),
      child: // 消息气泡内容
    );
  }

  /// 长按显示操作菜单
  void _showMessageMenu(BuildContext context) {
    showModalBottomSheet(
      context: context,
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
      ),
      builder: (context) => Container(
        padding: const EdgeInsets.symmetric(vertical: 20),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            // 【分享按钮】
            ListTile(
              leading: const Icon(Icons.share, color: Color(0xFF6366F1)),
              title: const Text('分享'),
              onTap: () {
                Navigator.pop(context);
                _shareMessage(context);
              },
            ),
            ListTile(
              leading: const Icon(Icons.reply),
              title: const Text('回复'),
              onTap: () {
                Navigator.pop(context);
                onReply();
              },
            ),
            ListTile(
              leading: const Icon(Icons.copy),
              title: const Text('复制'),
              onTap: () {
                Navigator.pop(context);
                _copyMessage();
              },
            ),
            ListTile(
              leading: const Icon(Icons.delete, color: Colors.red),
              title: const Text('删除', style: TextStyle(color: Colors.red)),
              onTap: () {
                Navigator.pop(context);
                _confirmDelete(context);
              },
            ),
          ],
        ),
      ),
    );
  }

  /// 分享消息
  void _shareMessage(BuildContext context) async {
    final shareService = ShareService.instance;
    
    if (message.type == MessageType.text) {
      // 分享文本消息
      await shareService.shareChatMessage(
        senderName: message.senderName,
        content: message.content,
        time: _formatTime(message.timestamp),
      );
    } else if (message.type == MessageType.image && message.imagePath != null) {
      // 分享图片
      await shareService.shareImage(
        message.imagePath!,
        text: '来自 ${message.senderName} 的图片',
      );
    }
  }

  /// 复制消息
  void _copyMessage() {
    if (message.type == MessageType.text) {
      Clipboard.setData(ClipboardData(text: message.content));
    }
    onCopy();
  }

  /// 确认删除
  void _confirmDelete(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('确认删除'),
        content: const Text('确定要删除这条消息吗?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          TextButton(
            onPressed: () {
              Navigator.pop(context);
              onDelete();
            },
            style: TextButton.styleFrom(foregroundColor: Colors.red),
            child: const Text('删除'),
          ),
        ],
      ),
    );
  }

  String _formatTime(DateTime time) {
    return '${time.hour}:${time.minute.toString().padLeft(2, '0')}';
  }
}

五、在商品详情页中分享

/// 商品卡片分享
class ProductShareButton extends StatelessWidget {
  final Product product;

  const ProductShareButton({super.key, required this.product});

  
  Widget build(BuildContext context) {
    return IconButton(
      icon: const Icon(Icons.share),
      onPressed: () => _shareProduct(context),
    );
  }

  void _shareProduct(BuildContext context) async {
    final shareService = ShareService.instance;
    
    // 分享商品链接
    final url = 'https://example.com/product/${product.id}';
    final text = '''
${product.name}
价格: ¥${product.price}
${product.description}
''';
    
    await shareService.shareUrl(url, text: text);
  }
}

六、测试功能

  /// 测试分享功能
  Future<Map<String, dynamic>> testShare() async {
    try {
      await Share.share(
        '这是一条来自Flutter聊天App的测试分享!\n\n功能包括:\n- 文本分享\n- 链接分享\n- 图片分享\n- 文件分享',
        subject: 'Flutter聊天App分享测试',
      );
      return {'success': true, 'message': '分享功能正常'};
    } catch (e) {
      return {'success': false, 'message': '分享功能异常: $e'};
    }
  }

七、踩坑纪实

踩坑1:分享图片闪退 💥

在鸿蒙设备上分享图片时,有时候会闪退!原因是图片路径可能不存在或者格式不支持。解决方案:

// 分享前检查文件是否存在
final file = File(imagePath);
if (!await file.exists()) {
  return {'success': false, 'message': '图片文件不存在'};
}

// 如果是网络图片,先下载到本地
final localPath = await _downloadImage(imageUrl);
if (localPath != null) {
  await Share.shareXFiles([XFile(localPath)]);
}

踩坑2:分享文本过长被截断 📝

分享超长文本时,部分应用会截断。后来改成限制文本长度:

String truncateText(String text, int maxLength) {
  if (text.length <= maxLength) return text;
  return '${text.substring(0, maxLength - 3)}...';
}

踩坑3:XFile 路径格式问题 🔗

在鸿蒙上,XFile 对路径格式有要求。必须使用完整的文件路径,不能用 content:// 格式:

// 错误 ❌
// XFile('content://xxx')

// 正确 ✅
XFile('/data/user/0/xxx/cache/image.jpg')

踩坑4:没有安装可分享的应用 📱

如果用户手机上没有安装任何可接收分享的应用,share_plus 会抛出异常。需要做好异常处理:

try {
  await Share.share('Hello');
} on PlatformException catch (e) {
  if (e.code == 'ACTIVITY_NOT_FOUND') {
    // 没有可用的分享应用
    showSnackBar('未找到可用的分享应用');
  }
}

八、Android 配置

<!-- AndroidManifest.xml -->
<application>
    <!-- 如果需要分享文件,需要添加 FileProvider -->
    <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="${applicationId}.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths"/>
    </provider>
</application>
<!-- res/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" path="/"/>
    <files-path name="files" path="/"/>
    <external-path name="external" path="/"/>
</paths>

九、效果展示

在这里插入图片描述

功能验证结果:

  • ✅ 分享图片正常
  • ✅ 分享文件正常

十、总结心得

share_plus 真的是"五星级好用"的库!一行代码就能调用系统分享,而且鸿蒙兼容性也很好。

核心要点:

  1. 文件路径要完整且存在
  2. 网络图片需要先下载到本地
  3. 长文本要适当截断
  4. 做好异常处理

使用场景:

  • 聊天消息分享
  • 商品链接分享
  • 文章分享
  • 朋友圈分享

后续计划:

  • 研究 deep link 实现分享回调
  • 尝试自定义分享面板

今天的分享就到这里!有问题评论区见!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐