背景
我这边用的时候发现生成缩略图时,弹窗闪一下就消失,无法感知任务执行状态。需要改进交互体验:
- 单文件:显示”正在生成中…”加载动画
- 文件夹:显示实时进度
xx/xx
需求分析
原有实现问题
- 确认后只显示 Toast 提示”正在生成缩略图…”
- 后台异步执行,使用时感知不到进度
- 批量任务时无法知道完成了多少
目标交互
| 场景 | 确认后显示 | 进度信息 | 完成处理 |
|---|---|---|---|
| 单文件 | 模态对话框 + 转圈 | ”正在生成中…” | 自动关闭 + Toast 成功 |
| 文件夹 | 模态对话框 + 转圈 | ”正在生成图片/视频缩略图…\n1/10” | 短暂显示完成 + 自动关闭 |
方案设计
核心思路
使用 showDialog + StatefulBuilder 实现状态可更新的模态对话框:
- 对话框弹出时触发生成任务
- 任务进度实时更新对话框内容
- 任务完成后自动关闭对话框
关键技术点
1. 避免重复触发任务
在 StatefulBuilder 的 builder 中启动任务,但 builder 会在每次 setState 时重新执行。使用标志位避免重复触发:
bool started = false;
builder: (ctx, setState) {
if (!started) {
started = true;
Future(() async {
// 执行任务...
});
}
return AlertDialog(...);
}2. 实时进度更新
文件夹场景需要显示 current/total:
int current = 0;
int successCount = 0;
for (final f in targetFiles) {
setState(() => current++); // 更新进度
try {
await _generateThumbnail(f);
successCount++;
} catch (e) {
// 继续处理下一个
}
}3. 完成状态短暂显示
完成后不立即关闭,先显示完成状态,再关闭:
bool completed = false;
// 任务完成后
if (ctx.mounted) {
setState(() => completed = true);
await Future.delayed(const Duration(milliseconds: 500));
if (ctx.mounted) Navigator.pop(ctx);
}
// UI 显示
Text(
completed
? '已完成 $successCount/$count'
: '正在生成图片缩略图...\n$current/$count',
textAlign: TextAlign.center,
),4. 错误状态处理
单文件场景需要显示错误信息:
String? errorMsg;
try {
await _generateThumbnail(file);
Navigator.pop(ctx, true);
} catch (e) {
setState(() => errorMsg = '生成失败');
await Future.delayed(const Duration(seconds: 1));
Navigator.pop(ctx, false);
}
// UI 显示
if (errorMsg == null) ...[
CircularProgressIndicator(...),
Text('正在生成中...'),
] else ...[
Icon(TablerIcons.alert_circle, color: error),
Text(errorMsg!),
],实现细节
单文件缩略图生成对话框
修改前:
showAppToast(context, '正在生成缩略图...');
try {
await _generateThumbnail(file);
showAppToast(context, '缩略图生成成功');
} catch (e) {
showAppToast(context, '缩略图生成失败');
}修改后:
bool generating = true;
String? errorMsg;
showDialog(
context: context,
barrierDismissible: false, // 禁止点击外部关闭
builder: (ctx) => StatefulBuilder(
builder: (ctx, setState) {
if (generating) {
generating = false;
Future(() async {
try {
await _generateThumbnail(file);
if (ctx.mounted) Navigator.pop(ctx, true);
} catch (e) {
if (ctx.mounted) {
setState(() => errorMsg = '生成失败');
await Future.delayed(const Duration(seconds: 1));
if (ctx.mounted) Navigator.pop(ctx, false);
}
}
});
}
return AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (errorMsg == null) ...[
const SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(strokeWidth: 3),
),
const SizedBox(height: 16),
const Text('正在生成中...'),
] else ...[
Icon(
TablerIcons.alert_circle,
size: 40,
color: Theme.of(ctx).colorScheme.error,
),
const SizedBox(height: 16),
Text(errorMsg!),
],
],
),
);
},
),
).then((success) {
if (mounted && success == true) {
showAppToast(context, '缩略图生成成功');
onActionComplete?.call();
}
});文件夹缩略图生成对话框
修改前:
showAppToast(context, '正在生成 $count 个图片缩略图...');
int successCount = 0;
for (final f in targetFiles) {
try {
await _generateThumbnail(f);
successCount++;
} catch (e) {}
}
showAppToast(context, '已生成 $successCount/$count 个缩略图');修改后:
bool started = false;
int current = 0;
int successCount = 0;
bool completed = false;
await showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) => StatefulBuilder(
builder: (ctx, setState) {
if (!started) {
started = true;
Future(() async {
for (final f in targetFiles) {
if (!ctx.mounted) break;
setState(() => current++);
try {
await _generateThumbnail(f);
successCount++;
} catch (e) {}
}
if (ctx.mounted) {
setState(() => completed = true);
await Future.delayed(const Duration(milliseconds: 500));
if (ctx.mounted) Navigator.pop(ctx);
}
});
}
return AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(strokeWidth: 3),
),
const SizedBox(height: 16),
Text(
completed
? '已完成 $successCount/$count'
: '正在生成图片缩略图...\n$current/$count',
textAlign: TextAlign.center,
),
],
),
);
},
),
);
showAppToast(context, '已生成 $successCount/$count 个缩略图');修改的文件
1. file_operation_service.dart
_showSingleFileThumbnailDialog: 单文件进度对话框_showFolderThumbnailDialog: 文件夹进度对话框(末尾执行部分)
2. file_tiles.dart
_showSingleFileThumbnailDialog: 同上_showFolderThumbnailDialog: 同上
设计取舍
为什么使用 StatefulBuilder 而不是独立组件?
- 进度对话框逻辑简单,不需要独立状态管理
StatefulBuilder可直接访问外层变量(targetFiles、count 等)- 代码内聚,修改方便
为什么不用 showDialog 的返回值传递进度?
showDialog返回Future<T?>,只能在关闭时获取最终结果- 需要实时更新显示,必须在对话框内部用
setState
为什么完成后延迟 500ms 再关闭?
- 让我这边看到完成状态,确认任务确实完成了
- 防止进度从 “9/10” 直接闪到下一页面,使用时可能没注意到完成
单文件错误显示 1 秒 vs 文件夹无错误显示
- 单文件:失败就是最终结果,需要明确提示
- 文件夹:批量任务允许部分失败,最终 Toast 显示成功/总数即可
相关规范记录
这次修改确立了批量任务进度反馈的规范:
- 批量操作需在弹窗中显示
xx/xx进度 - 配合加载动画确保能感知任务状态
- 单文件操作显示加载动画 + 简单状态提示
遗留的 Lint 警告
修改后存在 3 个 use_build_context_synchronously info 级别警告:
- 这是 Flutter 中使用异步上下文的常见提示
- 由于我们已经检查了
ctx.mounted,实际使用是安全的 - 不影响功能运行
测试验证
- 单文件图片:确认后显示转圈 → 成功自动关闭 → Toast 成功
- 单文件视频:确认后显示转圈 → 成功自动关闭 → Toast 成功
- 文件夹图片:确认后显示 “0/10” → “1/10” → … → “已完成 10/10” → 关闭
- 文件夹视频:同上
- 失败场景:单文件显示”生成失败”1秒后关闭