Files
App.K12.Student.Miniapp_Flu…/docs/demos/share_demo.md
T
2026-09-04 15:22:20 +08:00

5.9 KiB
Raw Blame History

【Demo】分享功能演示(ShareService 五个场景)

源文件:lib/pages/demo/share_demo.dart | 分类:组件封装 一句话:文本分享 / 选图分享 / Widget 截图生成海报分享 / 截图预览 / 保存相册 —— 一个服务类覆盖全部分享场景。


一、这个 Demo 解决什么问题

"分享"是 App 的标配能力,但场景很多:分享文字、分享图片、把 UI 截图成海报分享、保存到相册。 本页演示 ShareService(lib/core/services/share_service.dart)的 5 个场景:

# 场景 方法 用途
1 分享文本/链接 shareText(text:) 邀请链接、文案
2 选图后分享 shareImage(filePath:, text:) 分享单张图片
3 Widget 截图 → 分享 shareFromWidget(context:, text:, builder:) 攻克海报分享
4 Widget 截图预览 captureWidget(context:, builder:) 截图不发,先看效果
5a 选图保存相册 saveToGallery(filePath:) 存图
5b 截图保存相册 saveToGalleryFromBytes(bytes:) 海报存图

核心思想:"分享"是平台能力,不是页面能力 —— 封装成 Service 单例,任何页面一行调用。


二、怎么运行 / 演示内容

runApp(const MaterialApp(home: ShareDemoPage()));
  • ① 输入框改文案 → 分享文本;
  • ② 选一张图 → 系统分享面板;
  • ③ 点"截图并分享" → 海报 Widget 被截成图片 → 分享面板;
  • ④ 点"仅截图预览" → 弹窗显示截图 + 图片大小(KB);
  • ④ 场景的两个按钮:选图→保存 / 截图→保存 写入系统相册。

三、核心知识点(新手向)

1. 场景 3 的核心:Widget → 图片 → 分享

final result = await ShareService.instance.shareFromWidget(
  context: context,
  text: '我攻克了一道难题,快来挑战!',
  builder: (_) => _buildPosterWidget(          // 任意 Widget 都能截图
    userName: '张三同学',
    taskName: '二次函数综合练习',
    score: 98,
  ),
);

海报就是一个普通 Container(300 宽、渐变背景、纯 UI 无交互)——不是图片素材,是代码画出来的。 分享时被 captureWidget 截成 PNG 字节,再交给系统分享。

2. 场景 4:截图预览 = bytes → Image.memory

final bytes = await ShareService.captureWidget(context: context, builder: ...);
if (bytes == null) { _showResult('截图失败'); return; }
showDialog(
  context: context,
  builder: (ctx) => Dialog(
    child: Column(children: [
      ClipRRect(
        borderRadius: BorderRadius.circular(12),
        child: Image.memory(bytes, width: 280),   // 字节流直接渲染
      ),
      Text('图片大小:${(bytes.length / 1024).toStringAsFixed(1)} KB'),
    ]),
  ),
);

captureWidget 返回 Uint8List?——null 表示截图失败,必须判空。

3. 防重入 + 复位:异步按钮的三段式

setState(() => _sharing = true);   // ① 锁
try {
  ...await 分享...
} catch (e) {
  _showResult('分享失败:$e');
} finally {
  if (mounted) setState(() => _sharing = false);  // ③ 无论成败都解锁
}
// 按钮:_shareButton(onPressed: _sharing ? null : onPressed) ② 置灰

_sharing 置灰所有分享按钮,防连点重复弹系统面板;finally 保证异常也不卡死按钮。

4. Web 端限制:路径分享不可用

final filePath = picked.first.path;
if (filePath == null) {
  _showResult('Web 端暂不支持文件路径分享');  // Web 拿不到真实文件路径
  return;
}

四、关键代码剖析

/// 模拟"攻克海报"布局(实际项目中从业务数据构建)
Widget _buildPosterWidget({
  required String userName,
  required String taskName,
  required int score,
}) {
  return Container(
    width: 300,
    padding: const EdgeInsets.all(24),
    decoration: const BoxDecoration(
      gradient: LinearGradient(          // 海报背景:紫蓝渐变
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
        colors: [Color(0xFF667EEA), Color(0xFF764BA2)],
      ),
      borderRadius: BorderRadius.all(Radius.circular(16)),
    ),
    child: Column(mainAxisSize: MainAxisSize.min, children: [
      const Icon(Icons.school, color: Colors.white, size: 40),  // Logo
      const Text('爱学蝶变', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
      ...
      Text('$score', style: const TextStyle(color: Colors.white, fontSize: 48, fontWeight: FontWeight.w800, height: 1)),
    ]),
  );
}

海报 Widget 的纪律:固定宽度 + 纯展示 + 不依赖页面状态(参数全传入)——这样截图永远稳定。


五、踩坑记录

  1. captureWidget 必须在页面挂载后调用(context 有效),否则截图 null;
  2. bytes 判空:if (bytes == null) return;,别直接 Image.memory(bytes!) 崩掉;
  3. Web 端 picked.first.path 为 null——图片分享/存相册要走别的通道,代码里已降级提示;
  4. 所有按钮共享 _sharing 一把锁:分享中所有分享按钮都禁用,防止并发弹面板;
  5. finally 里必须判 mounted 再 setState(await 期间页面可能已销毁);
  6. 海报是"代码画的图",字体/图标依赖系统字体,跨设备效果略有差异——正式海报可换用真实图片素材。

六、扩展思考

  • 选图用的 PickerService.showPickSheet 与缩略图渲染见 note_picker_demo.md;
  • 异步三段式(锁 + try/catch + finally 复位)同样用在 note_picker_demo.md 的上传和 dio_demo.md 的请求里;
  • 真实项目:"攻克海报"数据来自答题结果接口,把 _buildPosterWidget 的参数换成接口返回的 userName/taskName/score 即可;
  • ShareService 内部用 share_plus / screenshot / gal 插件——想学封装思想可以直接读它的源码。