Flutter中如何获取多ExpansionTile选中索引并通过Provider/Bloc修改菜单选中色
Flutter 多级侧边栏菜单:获取选中索引并通过状态管理修改选中颜色
问题描述
我需要开发一个包含多个ExpansionTile和ListTile的侧边栏菜单,请问在Flutter中如何获取多个ExpansionTile的选中索引,并通过Provider或Bloc状态管理工具来修改选中菜单的颜色?

原始代码示例
children: [ ExpansionTile( title: Text('main a'), children: [ ListTile( title: Text('a1'), ), ListTile( title: Text('a2'), ), ExpansionTile( title: Text('a3'), children: [ ListTile( title: Text('a31'), ), ListTile( title: Text('a32'), ), ListTile( title: Text('a32'), ), ], ), ], ), ExpansionTile( title: Text('main b'), children: [ ListTile( title: Text('b1'), ), ListTile( title: Text('b2'), ), ListTile( title: Text('b3'), ), ], ), ],
解决方案
核心思路是给每个可选中的菜单项分配唯一标识,通过状态管理工具记录当前选中的标识,最后根据选中状态动态设置样式。以下分别提供Provider和Bloc两种实现方式:
方式一:使用Provider实现
1. 创建状态管理类
新建一个ChangeNotifier子类,用于保存选中状态并通知UI更新:
import 'package:flutter/material.dart'; class MenuProvider extends ChangeNotifier { // 用字符串作为菜单项的唯一标识,建议采用层级路径格式(如"main_a/a1") String? _selectedMenuId; String? get selectedMenuId => _selectedMenuId; // 更新选中状态的方法 void selectMenu(String menuId) { _selectedMenuId = menuId; notifyListeners(); } }
2. 在应用顶层注入Provider
将Provider包裹在MaterialApp外层,确保全局可访问:
void main() { runApp( ChangeNotifierProvider( create: (context) => MenuProvider(), child: const MyApp(), ), ); }
3. 改造侧边栏菜单组件
封装复用的菜单项组件,通过Consumer监听状态变化,动态调整样式:
// 构建普通ListTile菜单项 Widget buildMenuListItem(String menuId, String title) { return Consumer<MenuProvider>( builder: (context, provider, child) { final isSelected = provider.selectedMenuId == menuId; return ListTile( title: Text( title, style: TextStyle( color: isSelected ? Colors.blue : Colors.black, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), onTap: () { provider.selectMenu(menuId); // 可选:点击后关闭侧边栏 Scaffold.of(context).closeDrawer(); }, ); }, ); } // 构建ExpansionTile菜单组 Widget buildExpansionMenu(String groupId, String title, List<Widget> children) { return Consumer<MenuProvider>( builder: (context, provider, child) { // 判断当前组是否有子项被选中(父级高亮) final isGroupSelected = provider.selectedMenuId?.startsWith(groupId) ?? false; return ExpansionTile( title: Text( title, style: TextStyle( color: isGroupSelected ? Colors.blue : Colors.black, fontWeight: isGroupSelected ? FontWeight.bold : FontWeight.normal, ), ), children: children, // 可选:点击ExpansionTile标题时也触发选中 onExpansionChanged: (_) { provider.selectMenu(groupId); }, ); }, ); } // 侧边栏最终实现 Drawer buildSidebarDrawer(BuildContext context) { return Drawer( child: ListView( padding: EdgeInsets.zero, children: [ buildExpansionMenu( "main_a", "main a", [ buildMenuListItem("main_a/a1", "a1"), buildMenuListItem("main_a/a2", "a2"), buildExpansionMenu( "main_a/a3", "a3", [ buildMenuListItem("main_a/a3/a31", "a31"), buildMenuListItem("main_a/a3/a32", "a32"), buildMenuListItem("main_a/a3/a33", "a33"), ], ), ], ), buildExpansionMenu( "main_b", "main b", [ buildMenuListItem("main_b/b1", "b1"), buildMenuListItem("main_b/b2", "b2"), buildMenuListItem("main_b/b3", "b3"), ], ), ], ), ); }
方式二:使用Bloc实现
1. 定义事件、状态和Bloc类
import 'package:flutter_bloc/flutter_bloc.dart'; // 菜单事件 abstract class MenuEvent {} class SelectMenuEvent extends MenuEvent { final String menuId; SelectMenuEvent(this.menuId); } // 菜单状态 class MenuState { final String? selectedMenuId; MenuState({this.selectedMenuId}); } // Bloc逻辑处理 class MenuBloc extends Bloc<MenuEvent, MenuState> { MenuBloc() : super(MenuState()) { on<SelectMenuEvent>((event, emit) { emit(MenuState(selectedMenuId: event.menuId)); }); } }
2. 注入Bloc到应用顶层
void main() { runApp( BlocProvider( create: (context) => MenuBloc(), child: const MyApp(), ), ); }
3. 改造菜单组件
用BlocBuilder替代Provider的Consumer:
Widget buildBlocMenuListItem(String menuId, String title) { return BlocBuilder<MenuBloc, MenuState>( builder: (context, state) { final isSelected = state.selectedMenuId == menuId; return ListTile( title: Text( title, style: TextStyle( color: isSelected ? Colors.blue : Colors.black, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), onTap: () { context.read<MenuBloc>().add(SelectMenuEvent(menuId)); Scaffold.of(context).closeDrawer(); }, ); }, ); } // ExpansionTile组件同理,用BlocBuilder包裹即可
关键注意点
- 菜单项的唯一标识建议用层级路径(如
main_a/a3/a31),便于判断父级菜单是否需要高亮 - 可根据需求自定义选中样式(背景色、边框等),不止局限于文字颜色
- 如果不需要父级菜单高亮,直接判断
selectedMenuId与当前菜单项ID完全匹配即可
内容的提问来源于stack exchange,提问作者sianami
相关产品推荐
相关产品推荐

