# 【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 单例,任何页面一行调用。 --- ## 二、怎么运行 / 演示内容 ```dart runApp(const MaterialApp(home: ShareDemoPage())); ``` - ① 输入框改文案 → 分享文本; - ② 选一张图 → 系统分享面板; - ③ 点"截图并分享" → 海报 Widget 被截成图片 → 分享面板; - ④ 点"仅截图预览" → 弹窗显示截图 + 图片大小(KB); - ④ 场景的两个按钮:`选图→保存` / `截图→保存` 写入系统相册。 --- ## 三、核心知识点(新手向) ### 1. 场景 3 的核心:Widget → 图片 → 分享 ```dart 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 ```dart 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. 防重入 + 复位:异步按钮的三段式 ```dart setState(() => _sharing = true); // ① 锁 try { ...await 分享... } catch (e) { _showResult('分享失败:$e'); } finally { if (mounted) setState(() => _sharing = false); // ③ 无论成败都解锁 } // 按钮:_shareButton(onPressed: _sharing ? null : onPressed) ② 置灰 ``` `_sharing` 置灰所有分享按钮,防连点重复弹系统面板;`finally` 保证异常也不卡死按钮。 ### 4. Web 端限制:路径分享不可用 ```dart final filePath = picked.first.path; if (filePath == null) { _showResult('Web 端暂不支持文件路径分享'); // Web 拿不到真实文件路径 return; } ``` --- ## 四、关键代码剖析 ```dart /// 模拟"攻克海报"布局(实际项目中从业务数据构建) 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](note_picker_demo.md); - 异步三段式(锁 + try/catch + finally 复位)同样用在 [note_picker_demo.md](note_picker_demo.md) 的上传和 [dio_demo.md](dio_demo.md) 的请求里; - 真实项目:"攻克海报"数据来自答题结果接口,把 `_buildPosterWidget` 的参数换成接口返回的 userName/taskName/score 即可; - `ShareService` 内部用 share_plus / screenshot / gal 插件——想学封装思想可以直接读它的源码。