背景
files_page.dart 作为应用的核心文件列表页面,代码量达到 3035 行,包含了大量的 UI 组件和业务逻辑。随着功能不断增加,代码变得难以维护。同时,预览页面(image、video、pdf、text)也需要文件操作功能,存在代码重复问题。
一、重构目标
- 减少 files_page.dart 代码量 - 将可复用组件拆分到独立文件
- 统一文件操作逻辑 - 创建 FileOperationService 服务层
- 消除代码警告 - 修复所有 info 级别的
use_build_context_synchronously警告 - 修复功能 bug - 菜单点击后自动关闭
二、创建 file_tiles.dart
提取的组件
从 files_page.dart 中提取 _FileListTile 和 _FileGridTile 组件:
// lib/ui/widgets/file_tiles.dart
/// 文件列表项组件
class FileListTile extends StatefulWidget {
final FileMetadata file;
final Map<String, Uint8List?> thumbnailCache;
final ApiClient api;
final bool scrollIdle;
final bool isThumbFolder;
final bool isOrphanFolder;
// ... 回调函数
}
/// 文件网格项组件
class FileGridTile extends StatefulWidget {
final FileMetadata file;
final Map<String, Uint8List?> thumbnailCache;
final ApiClient api;
final bool scrollIdle;
// ... 回调函数
}
/// 获取或生成缩略图(异步)
Future<Uint8List?> fetchOrGenerateThumbnail(
ApiClient api,
String fileId, {
String? fileName,
}) async { ... }功能特点
- 支持图片/视频缩略图延迟加载
- VisibilityDetector 检测可见性,减少不必要加载
- 统一的图标和颜色处理
- E2E 加密导出对话框
三、创建 file_dialogs.dart
提取的对话框
// lib/ui/widgets/file_dialogs.dart
/// 创建文件夹对话框
class CreateFolderDialog extends StatefulWidget { ... }
/// 创建文本文件对话框
class CreateTextDialog extends StatefulWidget { ... }
/// 重命名对话框
class RenameDialog extends StatefulWidget { ... }
/// 文件详情对话框
class FileDetailsDialog extends StatelessWidget { ... }使用方式
// 之前(私有类)
builder: (context) => _CreateFolderDialog(),
// 之后(公共类)
builder: (context) => const CreateFolderDialog(),四、修复 file_action_sheet.dart
问题
点击文件操作菜单中的按钮后,菜单不会自动关闭。
原因
_QuickActionButton 和 _ActionListTile 组件直接调用回调,没有先关闭菜单。
解决方案
// _QuickActionButton
onTap: () {
Navigator.pop(context); // 先关闭菜单
onTap(); // 再执行操作
},
// _ActionListTile
onTap: () {
Navigator.pop(context);
onTap();
},五、修复 use_build_context_synchronously 警告
问题
10 个 info 级别警告:在异步操作后使用 BuildContext,可能导致在 Widget 已卸载后操作 UI。
解决方案
1. StatefulWidget 中使用 mounted 检查
// chat_page.dart, send_page.dart
Future<void> someAsyncMethod() async {
await someOperation();
if (!mounted) return; // 检查是否仍然挂载
showAppToast(context, 'Done');
}2. 非 StatefulWidget 类中使用 isMounted 回调
// FileOperationService
class FileOperationService {
final BuildContext context;
final bool Function()? isMounted;
FileOperationService(this.context, {this.isMounted});
bool get _mounted => isMounted?.call() ?? true;
void downloadFile(FileMetadata file) {
_appState.enqueueDownloadFolder(file).then((count) {
if (!_mounted) return; // 异步后检查
// ignore: use_build_context_synchronously
showAppToast(context, '已开始下载...');
});
}
}
// 调用处
final service = FileOperationService(context, isMounted: () => mounted);3. StatelessWidget 不需要检查
StatelessWidget 的生命周期由框架管理,不需要 mounted 检查。
六、代码量变化
files_page.dart
| 阶段 | 行数 | 减少 |
|---|---|---|
| 重构前 | 3035 | - |
| 删除对话框类 | 2750 | -285 (-9.4%) |
| 删除 Tile 类 | 1970 | -780 (-25.7%) |
| 总计 | 1970 | -1065 (-35%) |
新增文件
| 文件 | 行数 | 说明 |
|---|---|---|
| file_tiles.dart | 795 | Tile 组件 |
| file_dialogs.dart | ~150 | 对话框组件 |
| file_operation_service.dart | 323 | 文件操作服务 |
七、模块化结构
lib/├── core/│ └── services/│ └── file_operation_service.dart # 文件操作服务├── ui/│ ├── files_page.dart # 主页面 (1970 行)│ ├── image_preview_page.dart # 使用 FileOperationService│ ├── video_player_page.dart # 使用 FileOperationService│ ├── pdf_preview_page.dart # 使用 FileOperationService│ ├── text_editor_page.dart # 使用 FileOperationService│ └── widgets/│ ├── file_tiles.dart # Tile 组件│ ├── file_dialogs.dart # 对话框组件│ ├── file_action_sheet.dart # 操作菜单│ └── file_picker_dialog.dart # 文件选择器八、其他页面检查
对其他页面进行了代码检查:
| 页面 | 行数 | 评估 |
|---|---|---|
| chat_page.dart | 1565 | ✅ 已拆分到 chat_widgets.dart |
| settings_page.dart | 864 | ✅ 结构合理 |
| s3_config_page.dart | 805 | ✅ 可接受 |
| transfers_page.dart | 665 | ✅ 可接受 |
结论:其他页面代码结构合理,无需进一步拆分。
总结
本次重构完成了以下工作:
- 代码瘦身 - files_page.dart 减少 35%(1065 行)
- 组件复用 - 创建 file_tiles.dart、file_dialogs.dart
- 服务层 - FileOperationService 统一文件操作逻辑
- 警告清零 - 修复所有 use_build_context_synchronously 警告
- Bug 修复 - 菜单点击后自动关闭
代码质量检查结果:
flutter analyze: No issues found!经验教训
- 删除 StatefulWidget 时需同步删除 State 类引用 - 否则会导致编译错误
- PowerShell 处理大文件时注意编码 - 使用
-Encoding UTF8避免中文乱码 - isMounted 回调模式 - 非 StatefulWidget 类中检查 mounted 状态的优雅方式