5.9 KiB
5.9 KiB
【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 的纪律:固定宽度 + 纯展示 + 不依赖页面状态(参数全传入)——这样截图永远稳定。
五、踩坑记录
- captureWidget 必须在页面挂载后调用(context 有效),否则截图 null;
- bytes 判空:
if (bytes == null) return;,别直接Image.memory(bytes!)崩掉; - Web 端
picked.first.path为 null——图片分享/存相册要走别的通道,代码里已降级提示; - 所有按钮共享
_sharing一把锁:分享中所有分享按钮都禁用,防止并发弹面板; finally里必须判mounted再 setState(await 期间页面可能已销毁);- 海报是"代码画的图",字体/图标依赖系统字体,跨设备效果略有差异——正式海报可换用真实图片素材。
六、扩展思考
- 选图用的
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 插件——想学封装思想可以直接读它的源码。