Scaffold布局模式综合应用

Scaffold布局模式综合应用

一、Scaffold布局模式概述

Scaffold作为Flutter Material Design的基础布局组件,提供了多种布局模式的组合使用方案。通过合理地组合AppBar、Drawer、BottomNavigationBar、TabBar等组件,可以构建出丰富多样的应用界面结构。掌握这些布局模式对于开发高质量的应用至关重要。

常见布局模式分类

Scaffold布局模式
基础布局
导航布局
内容布局
特殊布局
单页面布局
列表布局
表单布局
底部导航布局
抽屉导航布局
标签导航布局
混合导航布局
网格布局
瀑布流布局
卡片布局
全屏内容布局
沉浸式布局
多面板布局

布局模式选择指南

布局模式 适用场景 复杂度 用户体验
单页面布局 简单工具类应用 低 简单直接
底部导航布局 3-5个主要功能区 中 易于切换
抽屉导航布局 多层级导航 中 空间高效
标签导航布局 内容分类浏览 中 快速筛选
混合导航布局 复杂大型应用 高 功能完整

二、底部导航+列表布局

典型的信息流应用布局

dart 复制代码
class BottomNavListLayoutPage extends StatefulWidget {
  const BottomNavListLayoutPage({super.key});

  @override
  State<BottomNavListLayoutPage> createState() => _BottomNavListLayoutPageState();
}

class _BottomNavListLayoutPageState extends State<BottomNavListLayoutPage> {
  int _currentIndex = 0;

  final List<Widget> _pages = [
    const _HomePage(),
    const _DiscoverPage(),
    const _MessagesPage(),
    const _ProfilePage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(_getAppBarTitle(_currentIndex)),
        backgroundColor: Colors.blue,
        foregroundColor: Colors.white,
        actions: [
          IconButton(
            icon: const Icon(Icons.search),
            onPressed: () {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('搜索功能')),
              );
            },
          ),
          PopupMenuButton<String>(
            onSelected: (value) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text('选择:$value')),
              );
            },
            itemBuilder: (context) => [
              const PopupMenuItem(value: '设置', child: Text('设置')),
              const PopupMenuItem(value: '关于', child: Text('关于')),
              const PopupMenuItem(value: '帮助', child: Text('帮助')),
            ],
          ),
        ],
      ),
      body: _pages[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        type: BottomNavigationBarType.fixed,
        selectedItemColor: Colors.blue,
        unselectedItemColor: Colors.grey,
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            activeIcon: Icon(Icons.home),
            label: '首页',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.explore),
            activeIcon: Icon(Icons.explore),
            label: '发现',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.message),
            activeIcon: Icon(Icons.message),
            label: '消息',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            activeIcon: Icon(Icons.person),
            label: '我的',
          ),
        ],
      ),
      floatingActionButton: _currentIndex == 0
          ? FloatingActionButton.extended(
              onPressed: () {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('发布新内容')),
                );
              },
              backgroundColor: Colors.blue,
              foregroundColor: Colors.white,
              icon: const Icon(Icons.add),
              label: const Text('发布'),
            )
          : null,
    );
  }

  String _getAppBarTitle(int index) {
    switch (index) {
      case 0:
        return '首页';
      case 1:
        return '发现';
      case 2:
        return '消息';
      case 3:
        return '我的';
      default:
        return '应用';
    }
  }
}

class _HomePage extends StatelessWidget {
  const _HomePage();

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      padding: const EdgeInsets.all(16),
      itemCount: 20,
      itemBuilder: (context, index) {
        return Card(
          margin: const EdgeInsets.only(bottom: 16),
          elevation: 2,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              ListTile(
                leading: CircleAvatar(
                  backgroundColor: Colors.primaries[index % Colors.primaries.length],
                  child: Text(
                    '${index + 1}',
                    style: const TextStyle(color: Colors.white),
                  ),
                ),
                title: Text('用户 ${index + 1}'),
                subtitle: Text('发布于${DateTime.now().toString().substring(0, 10)}'),
                trailing: IconButton(
                  icon: const Icon(Icons.more_vert),
                  onPressed: () {},
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
                child: Text(
                  '这是第${index + 1}条动态的内容。这里可以显示用户的文字、图片、视频等多种类型的内容。',
                ),
              ),
              ButtonBar(
                children: [
                  TextButton.icon(
                    onPressed: () {},
                    icon: const Icon(Icons.favorite_border),
                    label: const Text('点赞'),
                  ),
                  TextButton.icon(
                    onPressed: () {},
                    icon: const Icon(Icons.comment_outlined),
                    label: const Text('评论'),
                  ),
                  TextButton.icon(
                    onPressed: () {},
                    icon: const Icon(Icons.share_outlined),
                    label: const Text('分享'),
                  ),
                ],
              ),
            ],
          ),
        );
      },
    );
  }
}

class _DiscoverPage extends StatelessWidget {
  const _DiscoverPage();

  @override
  Widget build(BuildContext context) {
    return GridView.builder(
      padding: const EdgeInsets.all(8),
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        crossAxisSpacing: 8,
        mainAxisSpacing: 8,
        childAspectRatio: 0.75,
      ),
      itemCount: 20,
      itemBuilder: (context, index) {
        return Card(
          clipBehavior: Clip.antiAlias,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Expanded(
                child: Container(
                  color: Colors.primaries[index % Colors.primaries.length],
                  child: const Center(
                    child: Icon(Icons.image, size: 48, color: Colors.white),
                  ),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(8),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      '推荐 ${index + 1}',
                      style: const TextStyle(fontWeight: FontWeight.bold),
                    ),
                    const SizedBox(height: 4),
                    Text(
                      '这是一个推荐内容',
                      style: TextStyle(
                        fontSize: 12,
                        color: Colors.grey[600],
                      ),
                    ),
                  ],
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}

class _MessagesPage extends StatelessWidget {
  const _MessagesPage();

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      padding: const EdgeInsets.all(8),
      itemCount: 15,
      itemBuilder: (context, index) {
        return ListTile(
          leading: CircleAvatar(
            backgroundColor: Colors.primaries[index % Colors.primaries.length],
            child: Text(
              '${index + 1}',
              style: const TextStyle(color: Colors.white),
            ),
          ),
          title: Text('消息 ${index + 1}'),
          subtitle: Text('这是第${index + 1}条消息的内容'),
          trailing: const Icon(Icons.chevron_right),
          onTap: () {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text('打开消息 ${index + 1}')),
            );
          },
        );
      },
    );
  }
}

class _ProfilePage extends StatelessWidget {
  const _ProfilePage();

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        SliverAppBar(
          expandedHeight: 200,
          flexibleSpace: FlexibleSpaceBar(
            background: Container(
              decoration: BoxDecoration(
                gradient: LinearGradient(
                  colors: [Colors.blue.shade400, Colors.blue.shade700],
                ),
              ),
              child: const Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    CircleAvatar(
                      radius: 40,
                      backgroundColor: Colors.white,
                      child: Icon(Icons.person, size: 50, color: Colors.blue),
                    ),
                    SizedBox(height: 16),
                    Text(
                      '用户名称',
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 20,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
          pinned: true,
        ),
        SliverList(
          delegate: SliverChildListDelegate([
            ListTile(
              leading: const Icon(Icons.person_outline),
              title: const Text('个人信息'),
              trailing: const Icon(Icons.chevron_right),
              onTap: () {},
            ),
            ListTile(
              leading: const Icon(Icons.favorite_outline),
              title: const Text('我的收藏'),
              trailing: const Icon(Icons.chevron_right),
              onTap: () {},
            ),
            ListTile(
              leading: const Icon(Icons.history),
              title: const Text('浏览历史'),
              trailing: const Icon(Icons.chevron_right),
              onTap: () {},
            ),
            ListTile(
              leading: const Icon(Icons.settings),
              title: const Text('设置'),
              trailing: const Icon(Icons.chevron_right),
              onTap: () {},
            ),
            const Divider(),
            ListTile(
              leading: Icon(Icons.info_outline, color: Colors.blue),
              title: const Text('关于我们'),
              trailing: const Icon(Icons.chevron_right),
              onTap: () {},
            ),
            ListTile(
              leading: Icon(Icons.help_outline, color: Colors.blue),
              title: const Text('帮助与反馈'),
              trailing: const Icon(Icons.chevron_right),
              onTap: () {},
            ),
          ]),
        ),
      ],
    );
  }
}

布局特点分析

这种布局模式是社交媒体和内容类应用最常见的结构:

  1. 底部导航切换:通过BottomNavigationBar实现4个主要功能区的快速切换
  2. 列表内容展示:首页使用ListView展示动态信息流
  3. 网格内容展示:发现页使用GridView展示推荐内容
  4. 悬浮操作按钮:在首页显示发布按钮,其他页面隐藏
  5. 顶部操作入口:AppBar右侧提供搜索和更多操作入口
  6. 个人中心设计:使用CustomScrollView和SliverAppBar实现可折叠的个人信息页

三、抽屉+标签导航布局

混合导航模式实现

dart 复制代码
class DrawerTabLayoutPage extends StatefulWidget {
  const DrawerTabLayoutPage({super.key});

  @override
  State<DrawerTabLayoutPage> createState() => _DrawerTabLayoutPageState();
}

class _DrawerTabLayoutPageState extends State<DrawerTabLayoutPage>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('混合导航'),
        backgroundColor: Colors.green,
        foregroundColor: Colors.white,
        bottom: TabBar(
          controller: _tabController,
          indicatorColor: Colors.white,
          labelColor: Colors.white,
          unselectedLabelColor: Colors.white70,
          tabs: const [
            Tab(text: '文档'),
            Tab(text: '图片'),
            Tab(text: '视频'),
          ],
        ),
      ),
      drawer: Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: [
            DrawerHeader(
              decoration: BoxDecoration(
                color: Colors.green,
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const CircleAvatar(
                    radius: 32,
                    backgroundColor: Colors.white,
                    child: Icon(Icons.folder, size: 40, color: Colors.green),
                  ),
                  const SizedBox(height: 12),
                  const Text(
                    '我的文件',
                    style: TextStyle(
                      color: Colors.white,
                      fontSize: 20,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    '已使用 50GB / 100GB',
                    style: TextStyle(
                      color: Colors.white70,
                      fontSize: 14,
                    ),
                  ),
                ],
              ),
            ),
            _buildDrawerItem(Icons.home, '首页', () {
              Navigator.pop(context);
              _tabController.animateTo(0);
            }),
            _buildDrawerItem(Icons.recent_actors, '最近使用', () {
              Navigator.pop(context);
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('最近使用')),
              );
            }),
            _buildDrawerItem(Icons.star_border, '收藏夹', () {
              Navigator.pop(context);
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('收藏夹')),
              );
            }),
            _buildDrawerItem(Icons.delete_outline, '回收站', () {
              Navigator.pop(context);
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('回收站')),
              );
            }),
            const Divider(),
            _buildDrawerItem(Icons.settings, '设置', () {
              Navigator.pop(context);
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('设置')),
              );
            }),
            _buildDrawerItem(Icons.info_outline, '关于', () {
              Navigator.pop(context);
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('关于')),
              );
            }),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: const [
          _FileListPage(fileType: '文档'),
          _FileListPage(fileType: '图片'),
          _FileListPage(fileType: '视频'),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _showAddFileDialog(context);
        },
        backgroundColor: Colors.green,
        foregroundColor: Colors.white,
        child: const Icon(Icons.add),
      ),
    );
  }

  Widget _buildDrawerItem(IconData icon, String title, VoidCallback onTap) {
    return ListTile(
      leading: Icon(icon),
      title: Text(title),
      onTap: onTap,
    );
  }

  void _showAddFileDialog(BuildContext context) {
    showModalBottomSheet(
      context: context,
      backgroundColor: Colors.transparent,
      builder: (context) {
        return Container(
          decoration: const BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.vertical(
              top: Radius.circular(20),
            ),
          ),
          padding: const EdgeInsets.all(16),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const Text(
                '添加文件',
                style: TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                ),
              ),
              const SizedBox(height: 20),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: [
                  _buildAddButton(Icons.insert_drive_file, '文档', Colors.blue),
                  _buildAddButton(Icons.image, '图片', Colors.green),
                  _buildAddButton(Icons.videocam, '视频', Colors.red),
                ],
              ),
              const SizedBox(height: 20),
            ],
          ),
        );
      },
    );
  }

  Widget _buildAddButton(IconData icon, String label, Color color) {
    return Column(
      children: [
        CircleAvatar(
          radius: 32,
          backgroundColor: color.withOpacity(0.2),
          child: Icon(icon, color: color, size: 32),
        ),
        const SizedBox(height: 8),
        Text(label),
      ],
    );
  }
}

class _FileListPage extends StatelessWidget {
  final String fileType;

  const _FileListPage({required this.fileType});

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      padding: const EdgeInsets.all(16),
      itemCount: 20,
      itemBuilder: (context, index) {
        return Card(
          margin: const EdgeInsets.only(bottom: 12),
          child: ListTile(
            leading: CircleAvatar(
              backgroundColor: _getFileColor(index).withOpacity(0.2),
              child: Icon(_getFileIcon(), color: _getFileColor(index)),
            ),
            title: Text('$fileType ${index + 1}'),
            subtitle: Text('${(index + 1) * 1.5} MB'),
            trailing: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                IconButton(
                  icon: const Icon(Icons.star_border),
                  onPressed: () {},
                ),
                IconButton(
                  icon: const Icon(Icons.more_vert),
                  onPressed: () {},
                ),
              ],
            ),
            onTap: () {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text('打开$fileType ${index + 1}')),
              );
            },
          ),
        );
      },
    );
  }

  IconData _getFileIcon() {
    switch (fileType) {
      case '文档':
        return Icons.insert_drive_file;
      case '图片':
        return Icons.image;
      case '视频':
        return Icons.videocam;
      default:
        return Icons.insert_drive_file;
    }
  }

  Color _getFileColor(int index) {
    switch (fileType) {
      case '文档':
        return Colors.blue;
      case '图片':
        return Colors.green;
      case '视频':
        return Colors.red;
      default:
        return Colors.grey;
    }
  }
}

混合导航优势

这种结合Drawer和TabBar的布局模式适用于文件管理、应用商店等场景:

  1. Drawer用于导航:左侧抽屉提供主要功能入口和设置选项
  2. TabBar用于分类:顶部标签栏用于快速切换不同类型的内容
  3. 清晰的层级结构:用户可以通过Drawer和TabBar在不同层级间导航
  4. 统一的设计风格:使用相同的颜色和样式保持一致性

四、响应式布局模式

适配不同屏幕尺寸

dart 复制代码
class ResponsiveLayoutPage extends StatelessWidget {
  const ResponsiveLayoutPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('响应式布局'),
        backgroundColor: Colors.orange,
        foregroundColor: Colors.white,
      ),
      body: LayoutBuilder(
        builder: (context, constraints) {
          if (constraints.maxWidth > 600) {
            // 平板或大屏幕 - 显示侧边栏
            return _buildWideLayout();
          } else {
            // 手机或小屏幕 - 显示列表
            return _buildNarrowLayout();
          }
        },
      ),
    );
  }

  Widget _buildWideLayout() {
    return Row(
      children: [
        Container(
          width: 250,
          color: Colors.grey[200],
          child: _buildSidebar(),
        ),
        Expanded(
          child: _buildContent(),
        ),
      ],
    );
  }

  Widget _buildNarrowLayout() {
    return ListView.builder(
      padding: const EdgeInsets.all(16),
      itemCount: 20,
      itemBuilder: (context, index) {
        return Card(
          margin: const EdgeInsets.only(bottom: 12),
          child: ListTile(
            leading: CircleAvatar(
              backgroundColor: Colors.primaries[index % Colors.primaries.length],
              child: Text(
                '${index + 1}',
                style: const TextStyle(color: Colors.white),
              ),
            ),
            title: Text('项目 ${index + 1}'),
            subtitle: Text('这是第${index + 1}个项目的详细描述'),
            trailing: const Icon(Icons.chevron_right),
            onTap: () {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text('查看项目 ${index + 1}')),
              );
            },
          ),
        );
      },
    );
  }

  Widget _buildSidebar() {
    return ListView(
      padding: EdgeInsets.zero,
      children: [
        Container(
          height: 150,
          color: Colors.orange,
          padding: const EdgeInsets.all(16),
          child: const Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                '侧边栏',
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                ),
              ),
              SizedBox(height: 8),
              Text(
                '导航菜单',
                style: TextStyle(
                  color: Colors.white70,
                  fontSize: 14,
                ),
              ),
            ],
          ),
        ),
        _buildSidebarItem(Icons.home, '首页'),
        _buildSidebarItem(Icons.dashboard, '仪表盘'),
        _buildSidebarItem(Icons.analytics, '分析'),
        _buildSidebarItem(Icons.settings, '设置'),
      ],
    );
  }

  Widget _buildSidebarItem(IconData icon, String title) {
    return ListTile(
      leading: Icon(icon),
      title: Text(title),
      onTap: () {},
    );
  }

  Widget _buildContent() {
    return GridView.builder(
      padding: const EdgeInsets.all(16),
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 3,
        crossAxisSpacing: 16,
        mainAxisSpacing: 16,
        childAspectRatio: 1.2,
      ),
      itemCount: 20,
      itemBuilder: (context, index) {
        return Card(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(
                Icons.card_giftcard,
                size: 48,
                color: Colors.primaries[index % Colors.primaries.length],
              ),
              const SizedBox(height: 12),
              Text('项目 ${index + 1}'),
            ],
          ),
        );
      },
    );
  }
}

响应式设计要点

响应式布局是现代应用开发的重要考虑因素:

  1. 断点选择:使用600作为断点,区分手机和平板布局
  2. 布局切换:窄屏使用ListView,宽屏使用侧边栏+网格布局
  3. 空间利用:在宽屏上充分利用额外的空间显示更多信息
  4. 保持功能一致:不同屏幕尺寸下功能应该保持一致,只是布局方式不同

五、Scaffold布局最佳实践

实践总结表

实践要点 说明 重要性
统一导航模式 在整个应用中保持一致的导航方式 高
合理组织层级 避免过深的导航层级,控制在3层以内 高
响应式设计 考虑不同屏幕尺寸的适配 中
性能优化 使用懒加载、避免不必要的重建 中
用户体验 提供流畅的动画和清晰的反馈 高
可访问性 支持屏幕阅读器等辅助功能 中

关键设计原则

布局设计原则
一致性
简洁性
灵活性
性能
统一风格
统一交互
统一语言
减少层级
简化操作
清晰表达
适配多屏
主题切换
国际化
懒加载
缓存优化
避免重绘

通过遵循这些布局模式和最佳实践,可以构建出结构清晰、易于维护、用户体验优秀的Flutter应用。

相关推荐
HwJack2035 分钟前
【共创稿事节】HarmonyOS 7空间排布原则:视锥、舒适区与可达性
microsoft·华为·harmonyos
blanks20201 小时前
如何查看 drift 在 本机的 sqlite 文件路径
flutter
李游Leo1 小时前
HarmonyOS 7 Core Vision Kit + Image Kit:超分批处理的 PixelMap 预算、失败降级与结果原子替换【鸿蒙心迹】
harmonyos
李游Leo2 小时前
HarmonyOS 7 AbilityAccessCtrl + ArkUI:权限弹窗重入治理与提审证据链【鸿蒙心迹】
华为·harmonyos
李游Leo2 小时前
HarmonyOS 7 Spatial Recon Kit + Preferences:重建会话中断恢复与脏任务回收【鸿蒙心迹】
华为·harmonyos
李游Leo3 小时前
《HarmonyOS 7 ArkGraphics 3D 空间设计开发实战》03:glTF模型加载、实例复用与资源生命周期【鸿蒙心迹】
3d·harmonyos
李游Leo4 小时前
HarmonyOS 7 + ArkTS-HUKS:精准碰一碰载荷的签名校验与重放窗口【鸿蒙心迹】
华为·harmonyos
花花_14 小时前
HarmonyOS 7 状态手记 01|页面状态别乱放
华为·harmonyos
李游Leo5 小时前
HarmonyOS 7 + Hvigor-Localization Kit:多语言资源占位符签名与回退链路预检【鸿蒙心迹】
harmonyos
李游Leo5 小时前
HarmonyOS 7 AVSession + Float Window:闪控窗播放控制权的会话移交与重复指令去重【鸿蒙心迹】
华为·harmonyos