范围: Flutter 客户端 UI 层
关键词: Toast 统一、UI 比例优化、小部件简化、文件夹选择器复用
一、背景与问题
本次开发解决了多个 UI 一致性和组件复用问题:
- 弹窗样式不统一: 部分页面使用
ScaffoldMessenger.showSnackBar而非项目统一的showAppToast - UI 比例过大: 只优化了文件列表,但会话列表、传输列表、聊天输入框等仍显得”臃肿”
- 笔记小部件过于复杂: Android Widget 布局包含预览区和保存按钮,不符合”快速操作”定位
- 文件夹选择器重复实现: 设置页面自己实现了
_PathPickerSheet,没有复用现有的FilePickerDialog
二、问题一:弹窗样式统一
2.1 问题分析
项目中有统一的 Toast 组件 app_toast.dart,提供 showAppToast() 函数,样式为:
- 底部弹出的水平条形 Toast
- 深色背景 + 琥珀色文字
- 支持多个 Toast 堆叠显示
但某些地方错误使用了 Flutter 原生的 ScaffoldMessenger.showSnackBar,样式为:
- 居中的胶囊形状
- 深色背景 + 白色文字
- 与整体风格不协调
2.2 问题定位
通过 grep 搜索定位使用 ScaffoldMessenger.showSnackBar 的位置:
grep -r "ScaffoldMessenger.*showSnackBar" --include="*.dart"找到 2 处:
home_page.dartL129-130 - 笔记保存成功提示message_detail_sheet.dartL397-398 - 消息保存失败提示
2.3 解决方案
将所有 ScaffoldMessenger.showSnackBar 替换为 showAppToast:
home_page.dart:
// Before
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('笔记已保存')),
);
// After
showAppToast(context, '笔记已保存');message_detail_sheet.dart:
// Before
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('保存失败: $e')),
);
// After
showAppToast(context, '保存失败: $e');2.4 关键教训
当时记录的原话: “问题不是 app_toast.dart 啊,这个文件我们从来没改过,因为它本来就提供的对的样式,而是你之后写的东西不对啊”
教训:
- 问题定位要准确,不要急于修改”看起来相关”的文件
- 项目已有统一组件时,问题往往在于没有使用而非组件本身
- 全局搜索是定位此类问题的有效手段
三、问题二:全局 UI 比例优化
3.1 问题分析
我这边用的时候发现:“你刚才优化UI比例只优化了文件列表界面,我需要的是全局,全局懂吗”
之前只优化了:
file_tiles.dart- 文件列表项app_bar_widgets.dart- 搜索栏home_page.dart- 底部导航栏
遗漏了:
- 会话列表项 (
session_widgets.dart) - 桌面端会话项 (
send_page.dart) - 传输列表项 (
transfers_page.dart) - 聊天输入框 (
chat_input.dart)
3.2 优化清单
| 组件 | 修改项 | 原值 | 新值 |
|---|---|---|---|
| SessionListTile | 头像尺寸 | 50 | 42 |
| 头像圆角 | 14 | 12 | |
| 内边距 | 16,14 | 14,10 | |
| 名称字号 | 16 | 15 | |
| 副标题字号 | 13 | 12 | |
| _DesktopSessionTile | 头像尺寸 | 36 | 32 |
| 头像圆角 | 10 | 8 | |
| 内边距 | 12,8 | 10,6 | |
| 名称字号 | 14 | 13 | |
| _TransferTile | 图标区域 | 36 | 32 |
| 内边距 | 12 | 10 | |
| 外边距 | 12,4 | 12,3 | |
| 圆角 | 10 | 8 | |
| 进度条高度 | 6 | 5 | |
| ChatInput | 移动端边距 | 16 | 12 |
| 桌面端边距 | 12,10 | 10,8 | |
| 发送按钮 | 40 | 36 | |
| 字号 | 15 | 14 |
3.3 设计原则
- 紧凑化: 减少不必要的留白,提高信息密度
- 统一性: 所有列表项遵循相似的比例关系
- 层次感: 主要内容突出,次要信息弱化
- 现代化: 更小的圆角、更紧凑的布局符合现代 UI 趋势
四、问题三:笔记小部件简化
4.1 问题分析
原有的笔记小部件布局 (widget_note.xml) 包含:
- 标题栏(图标 + 标题 + 保存按钮)
- 预览区域(可点击输入的文本区)
这种设计的问题:
- 功能错位: 桌面小部件应该是”快速入口”,不是”迷你应用”
- 交互复杂: 使用时需要理解”点击输入”→“点击保存”的流程
- 空间浪费: 大量空间用于显示空白预览区
- 不一致: 与”快速上传”小部件的简洁按钮形式不统一
4.2 解决方案
将笔记小部件改为与上传小部件相同的按钮形式:
Before (widget_note.xml):
<LinearLayout orientation="vertical">
<!-- 标题栏 -->
<LinearLayout orientation="horizontal">
<ImageView /> <!-- 图标 -->
<TextView /> <!-- 标题 -->
<ImageButton /> <!-- 保存按钮 -->
</LinearLayout>
<!-- 预览区域 -->
<TextView hint="点击输入笔记..." />
</LinearLayout>After:
<LinearLayout
orientation="horizontal"
gravity="center">
<ImageView
layout_width="32dp"
layout_height="32dp"
src="@android:drawable/ic_menu_edit" />
<TextView
layout_marginStart="8dp"
text="笔记"
textSize="16sp"
textStyle="bold" />
</LinearLayout>4.3 设计理念
小部件的职责是触发动作,不是承载功能
点击小部件 → 打开 App → 进入笔记编辑界面
这种设计:
- 保持小部件轻量
- 统一两个小部件的交互模式
- 学习成本低
五、问题四:文件夹选择器复用
5.1 问题分析
项目中已有完善的文件夹选择器 FilePickerDialog:
- 支持路径导航和面包屑
- 支持新建文件夹
- 统一的 UI 风格
但设置页面自己实现了一个 _PathPickerSheet:
- 约 300+ 行重复代码
- 缺少新建文件夹功能
- 需要单独维护
5.2 我这边用的时候发现
“拉起的文件夹选择器又是你自己实现的,你没看到我们文件移动的时候已经有一个文件夹选择器了吗?还能新建文件夹,你为什么还要用一个不一样的???“
5.3 解决方案
步骤 1: 删除 _PathPickerSheet
删除设置页面中的 _PathPickerSheet 类(约 300 行代码)。
步骤 2: 修改 _showPathPickerDialog
// Before - 使用自定义的 _PathPickerSheet
Future<String?> _showPathPickerDialog({...}) async {
return showModalBottomSheet<String>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => _PathPickerSheet(
title: title,
currentPath: currentPath,
showAskOnUpload: showAskOnUpload,
askOnUploadValue: _askOnUpload,
),
);
}
// After - 复用 FilePickerDialog
Future<String?> _showPathPickerDialog({...}) async {
return FilePickerDialog.pickFolder(
context,
title: title,
showAskOnUpload: showAskOnUpload,
askOnUploadValue: _askOnUpload,
);
}步骤 3: 增强 FilePickerDialog
FilePickerDialog 原本有 showAskOnUpload 参数但未使用,需要添加实际功能:
新增 _AskOnUploadItem 组件:
class _AskOnUploadItem extends StatelessWidget {
final VoidCallback onTap;
const _AskOnUploadItem({required this.onTap});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
TablerIcons.hand_click,
color: theme.colorScheme.primary,
size: 24,
),
),
title: Text('上传时选择', ...),
subtitle: Text('每次上传时让我选择目录', ...),
onTap: onTap,
);
}
}修改文件列表构建逻辑:
Expanded(
child: ListView(
children: [
// 上传时选择选项(仅在根目录且启用时显示)
if (widget.showAskOnUpload && _currentPath == '/')
_AskOnUploadItem(
onTap: () => Navigator.pop(context, widget.askOnUploadValue),
),
// 文件/文件夹列表
...files.map((file) => _FilePickerItem(...)),
// 空状态
if (files.isEmpty && !(widget.showAskOnUpload && _currentPath == '/'))
_buildEmptyState(),
],
),
),5.4 收益
| 指标 | Before | After |
|---|---|---|
| 代码行数 | settings_page.dart 1648 行 | 1343 行 (-305) |
| 组件数量 | 2 个文件夹选择器 | 1 个统一组件 |
| 功能完整性 | 设置页选择器无新建文件夹 | 统一支持 |
| 维护成本 | 两处独立维护 | 单点维护 |
六、修改文件清单
| 文件 | 修改类型 | 说明 |
|---|---|---|
home_page.dart | 修改 | 替换 ScaffoldMessenger 为 showAppToast |
message_detail_sheet.dart | 修改 | 替换 ScaffoldMessenger 为 showAppToast,添加导入 |
session_widgets.dart | 修改 | UI 比例优化 |
send_page.dart | 修改 | 桌面端会话项 UI 比例优化 |
transfers_page.dart | 修改 | 传输列表项 UI 比例优化 |
chat_input.dart | 修改 | 聊天输入框 UI 比例优化 |
widget_note.xml | 重写 | 简化为按钮形式 |
settings_page.dart | 修改 | 删除 _PathPickerSheet,复用 FilePickerDialog |
file_picker_dialog.dart | 修改 | 添加”上传时选择”选项支持 |
七、设计决策记录
7.1 为什么不修改 app_toast.dart?
背景: 最初误以为 Toast 样式问题在于 app_toast.dart 本身。
决策:
app_toast.dart的实现是正确的- 问题在于其他地方没有使用它
- 应该修复调用方,而非修改被调用方
教训:
“这个文件我们从来没改过,因为它本来就提供的对的样式”
7.2 为什么笔记小部件要简化?
选项对比:
| 选项 | 优点 | 缺点 |
|---|---|---|
| A: 保留预览区 | 可直接预览内容 | 复杂、占空间、与上传不一致 |
| B: 简化为按钮 | 简洁、一致、易用 | 无法直接预览 |
决策: 选择 B
理由:
- 小部件的核心价值是”快速入口”,不是”迷你应用”
- 与上传小部件保持一致的交互模式
- 桌面空间有限,按钮形式更省空间
7.3 为什么要复用 FilePickerDialog?
选项对比:
| 选项 | 优点 | 缺点 |
|---|---|---|
| A: 保留两套实现 | 灵活定制 | 代码重复、维护成本高 |
| B: 统一为 FilePickerDialog | DRY原则、功能完整 | 需要增强现有组件 |
决策: 选择 B
理由:
FilePickerDialog已经很完善,支持新建文件夹- 只需添加”上传时选择”选项即可满足设置页需求
- 减少 300+ 行重复代码
八、验证
cd client
flutter analyze --no-fatal-infos
# No issues found!九、后续建议
- 全局审查: 搜索项目中是否还有其他使用原生 SnackBar 的地方
- 组件库文档: 建立内部 UI 组件使用规范,避免重复造轮子
- 代码复用检查: 定期检查是否有可复用但被重复实现的组件
十、相关文件
lib/ui/app_toast.dart- 统一的 Toast 组件lib/ui/widgets/file_picker_dialog.dart- 统一的文件夹选择器android/app/src/main/res/layout/widget_note.xml- 笔记小部件布局android/app/src/main/res/layout/widget_upload.xml- 上传小部件布局(参考)