January 11, 2026
6 min read
By devshan

Table of Contents

This is a list of all the sections in this post. Click on any of them to jump to that section.

范围: Flutter 客户端 UI 层
关键词: Toast 统一、UI 比例优化、小部件简化、文件夹选择器复用


一、背景与问题

本次开发解决了多个 UI 一致性和组件复用问题:

  1. 弹窗样式不统一: 部分页面使用 ScaffoldMessenger.showSnackBar 而非项目统一的 showAppToast
  2. UI 比例过大: 只优化了文件列表,但会话列表、传输列表、聊天输入框等仍显得”臃肿”
  3. 笔记小部件过于复杂: Android Widget 布局包含预览区和保存按钮,不符合”快速操作”定位
  4. 文件夹选择器重复实现: 设置页面自己实现了 _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 处:

  1. home_page.dart L129-130 - 笔记保存成功提示
  2. message_detail_sheet.dart L397-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头像尺寸5042
头像圆角1412
内边距16,1414,10
名称字号1615
副标题字号1312
_DesktopSessionTile头像尺寸3632
头像圆角108
内边距12,810,6
名称字号1413
_TransferTile图标区域3632
内边距1210
外边距12,412,3
圆角108
进度条高度65
ChatInput移动端边距1612
桌面端边距12,1010,8
发送按钮4036
字号1514

3.3 设计原则

  1. 紧凑化: 减少不必要的留白,提高信息密度
  2. 统一性: 所有列表项遵循相似的比例关系
  3. 层次感: 主要内容突出,次要信息弱化
  4. 现代化: 更小的圆角、更紧凑的布局符合现代 UI 趋势

四、问题三:笔记小部件简化

4.1 问题分析

原有的笔记小部件布局 (widget_note.xml) 包含:

  • 标题栏(图标 + 标题 + 保存按钮)
  • 预览区域(可点击输入的文本区)

这种设计的问题:

  1. 功能错位: 桌面小部件应该是”快速入口”,不是”迷你应用”
  2. 交互复杂: 使用时需要理解”点击输入”→“点击保存”的流程
  3. 空间浪费: 大量空间用于显示空白预览区
  4. 不一致: 与”快速上传”小部件的简洁按钮形式不统一

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 收益

指标BeforeAfter
代码行数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

理由:

  1. 小部件的核心价值是”快速入口”,不是”迷你应用”
  2. 与上传小部件保持一致的交互模式
  3. 桌面空间有限,按钮形式更省空间

7.3 为什么要复用 FilePickerDialog?

选项对比:

选项优点缺点
A: 保留两套实现灵活定制代码重复、维护成本高
B: 统一为 FilePickerDialogDRY原则、功能完整需要增强现有组件

决策: 选择 B

理由:

  1. FilePickerDialog 已经很完善,支持新建文件夹
  2. 只需添加”上传时选择”选项即可满足设置页需求
  3. 减少 300+ 行重复代码

八、验证

cd client
flutter analyze --no-fatal-infos
# No issues found!

九、后续建议

  1. 全局审查: 搜索项目中是否还有其他使用原生 SnackBar 的地方
  2. 组件库文档: 建立内部 UI 组件使用规范,避免重复造轮子
  3. 代码复用检查: 定期检查是否有可复用但被重复实现的组件

十、相关文件

  • 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 - 上传小部件布局(参考)