You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 17:45:37