UI 重构:页面组件化与导航优化

December 16, 2025
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.

一、背景与动机

1.1 原有问题

home_page.dart 文件过于庞大(2000+ 行),包含了:

  • 底部导航栏逻辑
  • 文件列表完整实现
  • 传输列表完整实现
  • 设置页面入口
  • 所有业务逻辑混在一起

问题

  1. 可维护性差:修改任何功能都需要在巨大的文件中定位
  2. 可读性低:不同功能模块缺乏清晰边界
  3. 协作困难:多人修改同一文件容易冲突

1.2 重构目标

  1. 文件页面独立files_page.dart 包含所有文件操作逻辑
  2. 传输页面独立transfers_page.dart 包含所有传输任务逻辑
  3. 主页轻量化home_page.dart 只负责导航容器和页面切换
  4. 清晰边界:每个页面只关注自己的职责

二、重构步骤

2.1 第一步:提取传输页面(2025-12-16 23:16)

创建 transfers_page.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../core/state/app_state.dart';
 
class TransfersPage extends StatefulWidget {
  const TransfersPage({super.key});
 
  @override
  State<TransfersPage> createState() => _TransfersPageState();
}
 
class _TransfersPageState extends State<TransfersPage> {
  int _currentTab = 0;  // 0: 上传, 1: 下载
  
  @override
  Widget build(BuildContext context) {
    final state = context.watch<AppState>();
    
    return Scaffold(
      appBar: AppBar(
        title: const Text('传输管理'),
        bottom: TabBar(
          controller: _tabController,
          tabs: const [
            Tab(text: '上传'),
            Tab(text: '下载'),
          ],
        ),
        actions: [
          // 清空按钮
          IconButton(
            icon: const Icon(Icons.delete_sweep),
            onPressed: _showClearDialog,
          ),
        ],
      ),
      body: TabBarView(
        controller: _tabController,
        children: [
          _buildUploadList(state),
          _buildDownloadList(state),
        ],
      ),
    );
  }
  
  Widget _buildUploadList(AppState state) {
    final uploads = state.transfers
        .where((t) => t.type == TransferType.upload)
        .toList();
    
    if (uploads.isEmpty) {
      return Center(child: Text('暂无上传任务'));
    }
    
    return ListView.builder(
      itemCount: uploads.length,
      itemBuilder: (context, index) {
        final item = uploads[index];
        return _buildTransferItem(item);
      },
    );
  }
  
  Widget _buildTransferItem(TransferItem item) {
    return ListTile(
      leading: _buildStatusIcon(item.status),
      title: Text(item.name),
      subtitle: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          if (item.size != null)
            Text(_formatSize(item.size!)),
          if (item.status == TransferStatus.running ||
              item.status == TransferStatus.finishing)
            LinearProgressIndicator(
              value: item.status == TransferStatus.running
                  ? item.progress
                  : null,
            ),
          if (item.error != null)
            Text(
              item.error!,
              style: TextStyle(color: Colors.red, fontSize: 12),
            ),
        ],
      ),
      trailing: _buildActions(item),
    );
  }
  
  // ... 更多方法
}

提取内容(~580 行):

  • 完整的传输列表 UI
  • 上传/下载任务分组
  • 任务状态显示
  • 取消/重试/清空功能
  • 进度条显示

修改 home_page.dart

// 删除传输相关代码,替换为:
class HomePage extends StatefulWidget {
  const HomePage({super.key});
 
  @override
  State<HomePage> createState() => _HomePageState();
}
 
class _HomePageState extends State<HomePage> {
  int _currentIndex = 0;
  
  final _pages = [
    const FilesPage(),      // 文件页面(待提取)
    const TransfersPage(),  // 传输页面(已提取)
    const SettingsPage(),   // 设置页面
  ];
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: _pages,
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() => _currentIndex = index);
        },
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.folder),
            label: '文件',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.swap_vert),
            label: '传输',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.settings),
            label: '设置',
          ),
        ],
      ),
    );
  }
}

代码减少home_page.dart 从 2000+ 行减少到 ~1400 行


2.2 第二步:添加标签切换动画(2025-12-16 23:53)

问题

传输页面的上传/下载标签切换时,内容切换生硬,没有动画过渡。

解决方案:PageController

class _TransfersPageState extends State<TransfersPage>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;
  late PageController _pageController;
  
  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 2, vsync: this);
    _pageController = PageController();
    
    // 同步 TabBar 和 PageView
    _tabController.addListener(() {
      if (_tabController.indexIsChanging) {
        _pageController.animateToPage(
          _tabController.index,
          duration: const Duration(milliseconds: 300),
          curve: Curves.easeInOut,
        );
      }
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('传输管理'),
        bottom: TabBar(
          controller: _tabController,
          tabs: const [
            Tab(text: '上传'),
            Tab(text: '下载'),
          ],
        ),
      ),
      body: PageView(
        controller: _pageController,
        onPageChanged: (index) {
          _tabController.animateTo(index);
        },
        children: [
          _buildUploadList(),
          _buildDownloadList(),
        ],
      ),
    );
  }
}

效果

  • 点击 Tab 时,内容平滑滑动切换
  • 支持左右滑动手势切换 Tab
  • Tab 指示器自动跟随动画

2.3 第三步:提取文件页面(2025-12-18 00:10)

创建 files_page.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../core/state/app_state.dart';
 
class FilesPage extends StatefulWidget {
  const FilesPage({super.key});
 
  @override
  State<FilesPage> createState() => _FilesPageState();
}
 
class _FilesPageState extends State<FilesPage> {
  final Set<String> _selectedIds = {};
  bool _isGridView = false;
  
  @override
  Widget build(BuildContext context) {
    final state = context.watch<AppState>();
    
    return Scaffold(
      appBar: _buildAppBar(state),
      body: _buildBody(state),
      floatingActionButton: _buildFAB(state),
    );
  }
  
  AppBar _buildAppBar(AppState state) {
    if (_selectedIds.isNotEmpty) {
      // 多选模式
      return AppBar(
        leading: IconButton(
          icon: Icon(Icons.close),
          onPressed: () {
            setState(() => _selectedIds.clear());
          },
        ),
        title: Text('已选择 ${_selectedIds.length} 项'),
        actions: [
          IconButton(
            icon: Icon(Icons.select_all),
            onPressed: _selectAll,
          ),
          IconButton(
            icon: Icon(Icons.delete),
            onPressed: _deleteSelected,
          ),
          // ... 更多操作
        ],
      );
    }
    
    // 正常模式
    return AppBar(
      title: Text(_getCurrentPathDisplay(state)),
      leading: state.currentPath != '/'
          ? IconButton(
              icon: Icon(Icons.arrow_back),
              onPressed: () => state.navigateUp(),
            )
          : null,
      actions: [
        IconButton(
          icon: Icon(_isGridView ? Icons.view_list : Icons.grid_view),
          onPressed: () {
            setState(() => _isGridView = !_isGridView);
          },
        ),
      ],
    );
  }
  
  Widget _buildBody(AppState state) {
    final files = state.currentFiles;
    
    if (files.isEmpty && !state.isLoading) {
      return _buildEmptyState(state);
    }
    
    return RefreshIndicator(
      onRefresh: () => state.refreshFiles(),
      child: _isGridView
          ? _buildGridView(files)
          : _buildListView(files),
    );
  }
  
  // ... 所有文件操作逻辑(~2400 行)
}

提取内容(~2400 行):

  • 完整的文件列表(列表/网格视图)
  • 文件导航(进入文件夹、返回上级)
  • 多选模式(全选、反选、批量操作)
  • 文件操作(上传、下载、删除、重命名、移动、复制)
  • 预览功能(图片、视频、PDF、文本)
  • 系统文件夹处理(缩略图、游离文件)
  • 初始化/解锁界面

最终的 home_page.dart

class HomePage extends StatefulWidget {
  const HomePage({super.key});
 
  @override
  State<HomePage> createState() => _HomePageState();
}
 
class _HomePageState extends State<HomePage> {
  int _currentIndex = 0;
  
  static const _pages = [
    FilesPage(),
    TransfersPage(),
    SettingsPage(),
  ];
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: _pages,
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _currentIndex,
        onDestinationSelected: (index) {
          setState(() => _currentIndex = index);
        },
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.folder_outlined),
            selectedIcon: Icon(Icons.folder),
            label: '文件',
          ),
          NavigationDestination(
            icon: Icon(Icons.swap_vert_outlined),
            selectedIcon: Icon(Icons.swap_vert),
            label: '传输',
          ),
          NavigationDestination(
            icon: Icon(Icons.settings_outlined),
            selectedIcon: Icon(Icons.settings),
            label: '设置',
          ),
        ],
      ),
    );
  }
}

最终行数home_page.dart 从 2000+ 行减少到 ~100 行


三、重构对比

3.1 文件结构

重构前

lib/ui/
├── home_page.dart (2000+ 行,包含所有逻辑)
├── settings_page.dart
└── ...

重构后

lib/ui/
├── home_page.dart (~100 行,只负责导航)
├── files_page.dart (~2400 行,文件管理)
├── transfers_page.dart (~580 行,传输管理)
├── settings_page.dart
└── ...

3.2 职责划分

组件职责行数
HomePage底部导航、页面切换~100
FilesPage文件浏览、操作、预览~2400
TransfersPage传输任务管理~580
SettingsPage应用设置~370

3.3 优势

  1. 可维护性:每个页面独立,修改互不影响
  2. 可读性:清晰的文件边界,易于理解
  3. 复用性:独立组件可以在其他地方复用
  4. 协作性:多人可以同时修改不同页面
  5. 测试性:可以单独测试每个页面组件

四、导航优化

4.1 使用 NavigationBar(Material 3)

// 旧版 BottomNavigationBar
BottomNavigationBar(
  currentIndex: _currentIndex,
  onTap: (index) => setState(() => _currentIndex = index),
  items: [...],
)
 
// 新版 NavigationBar(Material 3)
NavigationBar(
  selectedIndex: _currentIndex,
  onDestinationSelected: (index) => setState(() => _currentIndex = index),
  destinations: [
    NavigationDestination(
      icon: Icon(Icons.folder_outlined),
      selectedIcon: Icon(Icons.folder),
      label: '文件',
    ),
    // ...
  ],
)

优势

  • Material 3 设计规范
  • 更好的视觉效果(圆角、高亮)
  • 区分选中/未选中图标

4.2 使用 IndexedStack

IndexedStack(
  index: _currentIndex,
  children: _pages,
)

优势

  • 保持所有页面的状态(不会重新构建)
  • 切换页面时不会丢失滚动位置
  • 性能更好(只渲染当前页面)

五、文件变更清单

第一阶段(传输页面提取)

  • client/lib/ui/home_page.dart - 删除传输相关代码
  • client/lib/ui/transfers_page.dart - 新建 传输页面

第二阶段(标签动画)

  • client/lib/ui/transfers_page.dart - 添加 PageController

第三阶段(文件页面提取)

  • client/lib/ui/home_page.dart - 简化为导航容器
  • client/lib/ui/files_page.dart - 新建 文件页面

六、经验总结

6.1 组件化原则

  1. 单一职责:一个组件只做一件事
  2. 清晰边界:组件之间通过 Props 和 Callbacks 通信
  3. 状态提升:共享状态放在 Provider(AppState)
  4. 适度拆分:不要过度拆分(100 行以下的文件没必要再拆)

6.2 重构技巧

  1. 渐进式重构:先提取传输页面,再提取文件页面
  2. 保持功能完整:每一步重构后都要验证功能正常
  3. 使用 Git:每一步提交一次,方便回滚
  4. 测试先行:有自动化测试的情况下先写测试

6.3 何时重构

当文件满足以下条件时考虑重构:

  • ✅ 文件超过 1000 行
  • ✅ 包含多个不相关的功能
  • ✅ 修改时需要大量滚动查找
  • ✅ 多人协作频繁冲突

总结:通过三阶段重构,将庞大的 home_page.dart 拆分为职责清晰的独立组件,显著提升了代码的可维护性和可读性。