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

如何实现Flutter列表项中子菜单的单一展开与切换控制?

解决方案:实现列表项子菜单互斥显示

方法一:使用Stateful Widget + 父组件统一管理状态

核心思路:将所有子菜单的展开状态交由父组件集中管理,点击列表项按钮时通知父组件更新当前展开项的索引,所有子项根据该索引自动同步菜单的显示/隐藏状态。

1. 改造父组件为StatefulWidget

class ItemListView extends StatefulWidget {
  const ItemListView({super.key, required this.items});
  final List<Item> items;

  @override
  State<ItemListView> createState() => _ItemListViewState();
}

class _ItemListViewState extends State<ItemListView> {
  // 保存当前展开菜单的item索引,null表示无菜单展开
  int? _expandedItemIndex;

  // 切换菜单展开状态的回调
  void _toggleMenu(int index) {
    setState(() {
      if (_expandedItemIndex == index) {
        // 点击已展开的项,关闭菜单
        _expandedItemIndex = null;
      } else {
        // 点击新项,关闭旧菜单并打开新菜单
        _expandedItemIndex = index;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemBuilder: (context, index) {
        return ItemListTile(
          item: widget.items[index],
          isMenuExpanded: _expandedItemIndex == index,
          onMenuToggle: () => _toggleMenu(index),
        );
      },
      itemCount: widget.items.length,
    );
  }
}

2. 改造ItemListTile为StatefulWidget(处理菜单Overlay逻辑)

class ItemListTile extends StatefulWidget {
  const ItemListTile({
    super.key,
    required this.item,
    required this.isMenuExpanded,
    required this.onMenuToggle,
  });
  final Item item;
  final bool isMenuExpanded;
  final VoidCallback onMenuToggle;

  @override
  State<ItemListTile> createState() => _ItemListTileState();
}

class _ItemListTileState extends State<ItemListTile> {
  OverlayEntry? _overlayEntry;

  @override
  void didUpdateWidget(covariant ItemListTile oldWidget) {
    super.didUpdateWidget(oldWidget);
    // 父组件状态变化时,同步更新菜单显示
    if (widget.isMenuExpanded != oldWidget.isMenuExpanded) {
      widget.isMenuExpanded ? _openMenu() : _closeMenu();
    }
  }

  void _openMenu() {
    _overlayEntry = _overlayEntryBuilder();
    Overlay.of(context).insert(_overlayEntry!);
  }

  void _closeMenu() {
    _overlayEntry?.remove();
    _overlayEntry = null;
  }

  OverlayEntry _overlayEntryBuilder() {
    // 自定义菜单UI,这里用示例容器代替
    return OverlayEntry(
      builder: (context) => Positioned(
        top: 80,
        right: 16,
        child: Container(
          width: 150,
          color: Colors.white,
          child: Column(
            children: const [
              ListTile(title: Text('操作1')),
              ListTile(title: Text('操作2')),
            ],
          ),
        ),
      ),
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(widget.item.title),
      trailing: IconButton(
        onPressed: widget.onMenuToggle,
        icon: const Icon(Icons.more_vert_outlined, color: Colors.white),
        color: Colors.white,
      ),
    );
  }
}

方法二:使用Bloc模式全局管理状态

核心思路:通过Bloc统一维护菜单展开状态,所有列表项监听Bloc状态变化自动同步菜单显示,点击按钮时发送事件更新Bloc状态。

1. 定义Bloc的事件和状态

// 菜单操作事件
abstract class MenuEvent {}
class OpenMenuEvent extends MenuEvent {
  final String itemId; // 用item唯一ID区分不同项
  OpenMenuEvent(this.itemId);
}
class CloseMenuEvent extends MenuEvent {}

// 菜单状态
class MenuState {
  final String? expandedItemId; // 当前展开的item ID,null表示无展开
  MenuState(this.expandedItemId);
}

// Bloc逻辑实现
class MenuBloc extends Bloc<MenuEvent, MenuState> {
  MenuBloc() : super(MenuState(null)) {
    on<OpenMenuEvent>((event, emit) {
      if (state.expandedItemId == event.itemId) {
        // 点击已展开项,关闭菜单
        emit(MenuState(null));
      } else {
        // 打开新菜单,关闭旧菜单
        emit(MenuState(event.itemId));
      }
    });
    on<CloseMenuEvent>((event, emit) => emit(MenuState(null)));
  }
}

2. 父组件提供Bloc并构建列表

class ItemListView extends StatelessWidget {
  const ItemListView({super.key, required this.items});
  final List<Item> items;

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => MenuBloc(),
      child: ListView.builder(
        itemBuilder: (context, index) {
          return ItemListTile(item: items[index]);
        },
        itemCount: items.length,
      ),
    );
  }
}

3. 改造ItemListTile监听Bloc状态

class ItemListTile extends StatefulWidget {
  const ItemListTile({super.key, required this.item});
  final Item item;

  @override
  State<ItemListTile> createState() => _ItemListTileState();
}

class _ItemListTileState extends State<ItemListTile> {
  OverlayEntry? _overlayEntry;

  @override
  Widget build(BuildContext context) {
    return BlocListener<MenuBloc, MenuState>(
      listener: (context, state) {
        // 根据Bloc状态更新菜单显示
        final isExpanded = state.expandedItemId == widget.item.id;
        isExpanded ? _openMenu() : _closeMenu();
      },
      child: ListTile(
        title: Text(widget.item.title),
        trailing: IconButton(
          onPressed: () {
            context.read<MenuBloc>().add(OpenMenuEvent(widget.item.id));
          },
          icon: const Icon(Icons.more_vert_outlined, color: Colors.white),
          color: Colors.white,
        ),
      ),
    );
  }

  void _openMenu() {
    _overlayEntry = _overlayEntryBuilder();
    Overlay.of(context).insert(_overlayEntry!);
  }

  void _closeMenu() {
    _overlayEntry?.remove();
    _overlayEntry = null;
  }

  OverlayEntry _overlayEntryBuilder() {
    return OverlayEntry(
      builder: (context) => Positioned(
        top: 80,
        right: 16,
        child: Container(
          width: 150,
          color: Colors.white,
          child: Column(
            children: const [
              ListTile(title: Text('操作1')),
              ListTile(title: Text('操作2')),
            ],
          ),
        ),
      ),
    );
  }

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

内容的提问来源于stack exchange,提问作者zex_rectooor

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 04:44:56