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

151 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 【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 插件——想学封装思想可以直接读它的源码。