如何用Flutter默认组件实现Material 3风格NavigationRail响应式子菜单?
完全可以用Flutter默认的Material 3组件实现这个效果,核心是结合LayoutBuilder做响应式判断,配合NavigationRail、NavigationDrawer和ListTile构建子菜单容器。下面是具体实现方案:
1. 定义响应式宽度阈值
先设定三个宽度区间的阈值(可根据需求调整):
- 小屏:宽度 < 600px,使用NavigationDrawer
- 中等屏:600px ≤ 宽度 < 1000px,选中NavigationRail项时显示子菜单
- 大屏:宽度 ≥ 1000px,始终显示子菜单
2. 核心布局结构
用LayoutBuilder包裹整体布局,根据当前可用宽度选择对应的导航模式:
class ResponsiveNavRail extends StatefulWidget { const ResponsiveNavRail({super.key}); @override State<ResponsiveNavRail> createState() => _ResponsiveNavRailState(); } class _ResponsiveNavRailState extends State<ResponsiveNavRail> { // 当前选中的导航项索引 int _selectedNavIndex = 0; // 控制小屏时Drawer的显示 final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); // 导航项数据(包含子菜单) final List<NavItem> _navItems = [ NavItem( icon: Icons.home, label: '首页', subItems: ['概览', '统计', '快捷操作'], ), NavItem( icon: Icons.settings, label: '设置', subItems: ['账号', '通知', '隐私'], ), NavItem( icon: Icons.person, label: '个人中心', subItems: [], // 无子菜单的项 ), ]; @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final double width = constraints.maxWidth; final bool isSmallScreen = width < 600; final bool isMediumScreen = width >= 600 && width < 1000; final bool isLargeScreen = width >= 1000; return Scaffold( key: _scaffoldKey, // 小屏显示Drawer按钮 appBar: isSmallScreen ? AppBar( leading: IconButton( icon: const Icon(Icons.menu), onPressed: () => _scaffoldKey.currentState?.openDrawer(), ), ) : null, drawer: isSmallScreen ? NavigationDrawer( selectedIndex: _selectedNavIndex, onDestinationSelected: (index) { setState(() => _selectedNavIndex = index); Navigator.pop(context); }, children: [ ..._navItems.map((item) => NavigationDrawerDestination( icon: Icon(item.icon), label: Text(item.label), )), // 子菜单在Drawer里直接展开显示 if (_navItems[_selectedNavIndex].subItems.isNotEmpty) const Divider(), ..._navItems[_selectedNavIndex].subItems.map((subItem) => Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: ListTile( title: Text(subItem), onTap: () {}, ), )), ], ) : null, body: Row( children: [ // 中大屏显示NavigationRail if (!isSmallScreen) NavigationRail( selectedIndex: _selectedNavIndex, onDestinationSelected: (index) { setState(() => _selectedNavIndex = index); }, labelType: NavigationRailLabelType.all, destinations: _navItems.map((item) => NavigationRailDestination( icon: Icon(item.icon), label: Text(item.label), )).toList(), ), const VerticalDivider(thickness: 1, width: 1), // 主内容区 + 子菜单容器 Expanded( child: Row( children: [ // 子菜单容器:大屏始终显示,中等屏仅选中项有子菜单时显示 if ((isLargeScreen || (isMediumScreen && _navItems[_selectedNavIndex].subItems.isNotEmpty)) && _navItems[_selectedNavIndex].subItems.isNotEmpty) Container( width: 280, decoration: BoxDecoration( border: Border( right: BorderSide( color: Theme.of(context).dividerColor, width: 1, ), ), ), child: ListView.builder( itemCount: _navItems[_selectedNavIndex].subItems.length, itemBuilder: (context, index) { final subItem = _navItems[_selectedNavIndex].subItems[index]; return ListTile( title: Text(subItem), onTap: () {}, style: ListTileStyle.list, ); }, ), ), // 主内容区域 Expanded( child: Center( child: Text( '当前选中:${_navItems[_selectedNavIndex].label}', style: Theme.of(context).textTheme.titleLarge, ), ), ), ], ), ), ], ), ); }, ); } } // 导航项数据模型 class NavItem { final IconData icon; final String label; final List<String> subItems; NavItem({required this.icon, required this.label, required this.subItems}); }
3. 关键细节说明
- 响应式判断:通过
LayoutBuilder获取当前布局宽度,自动切换导航模式,完全符合Material 3的响应式要求。 - 子菜单显示逻辑:
- 大屏:不管是否选中,只要当前导航项有子菜单就始终显示右侧容器。
- 中等屏:只有选中的导航项包含子菜单时,才渲染右侧子菜单容器。
- 小屏:NavigationRail隐藏,改用NavigationDrawer,子菜单跟随选中项展开在Drawer内。
- Material 3风格适配:使用默认的
NavigationRail、ListTile、NavigationDrawer组件,会自动继承当前主题的Material 3样式,无需额外自定义。
4. 自定义调整建议
- 可根据产品需求修改宽度阈值,比如把大屏阈值调整到1200px。
- 子菜单的
ListTile可添加选中状态、图标等,增强视觉反馈。 - 可给子菜单容器添加阴影、圆角等Material 3风格的装饰,提升质感。
内容的提问来源于stack exchange,提问作者Daniel Reghin
相关产品推荐
相关产品推荐

