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

Flutter中PopupMenu内ListView无法更新过滤数据问题

解决Flutter CustomDropdown搜索过滤后ListView不更新的问题

问题根源

你使用showMenu弹出的下拉菜单,其内容是在调用showMenu时一次性构建的快照。后续调用setState更新filteredItems只会刷新CustomDropdown组件本身,不会重新渲染已经弹出的菜单内容,因此ListView无法同步显示过滤后的条目。

解决方案一:用StatefulBuilder实现菜单内部状态更新

将搜索框和过滤列表放到StatefulBuilder内部,让菜单拥有独立的状态更新能力,搜索时直接刷新菜单内的UI:

import 'package:flutter/material.dart';

class CustomDropdown extends StatefulWidget {
  final String labelText;
  final List<String> items;
  final ValueChanged<String?> onChanged;
  final TextEditingController dropdownTextFieldController;

  const CustomDropdown({
    Key? key,
    required this.labelText,
    required this.items,
    required this.onChanged,
    required this.dropdownTextFieldController,
  }) : super(key: key);

  @override
  CustomDropdownState createState() => CustomDropdownState();
}

class CustomDropdownState extends State<CustomDropdown> {
  TextEditingController searchController = TextEditingController();

  void _showDropdown(BuildContext context) {
    final RenderBox textFieldRenderBox = context.findRenderObject() as RenderBox;
    List<String> tempFilteredItems = List.from(widget.items);

    showMenu(
      context: context,
      position: RelativeRect.fromLTRB(
        textFieldRenderBox.localToGlobal(Offset.zero).dx,
        textFieldRenderBox.localToGlobal(Offset.zero).dy + textFieldRenderBox.size.height,
        textFieldRenderBox.localToGlobal(Offset.zero).dx + textFieldRenderBox.size.width,
        textFieldRenderBox.localToGlobal(Offset.zero).dy + textFieldRenderBox.size.height + 10,
      ),
      items: <PopupMenuEntry<String>>[
        PopupMenuItem<String>(
          child: StatefulBuilder(
            builder: (menuContext, setStateMenu) {
              return SizedBox(
                width: 300,
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    TextField(
                      controller: searchController,
                      decoration: const InputDecoration(
                        hintText: 'Search...',
                        suffixIcon: Icon(Icons.search),
                      ),
                      onChanged: (query) {
                        setStateMenu(() {
                          tempFilteredItems = widget.items
                              .where((item) => item.toLowerCase().contains(query.toLowerCase()))
                              .toList();
                        });
                      },
                    ),
                    if (tempFilteredItems.isNotEmpty)
                      SizedBox(
                        height: 200,
                        width: 300,
                        child: ListView.builder(
                          itemCount: tempFilteredItems.length,
                          itemBuilder: (BuildContext context, int index) {
                            return ListTile(
                              title: Text(tempFilteredItems[index]),
                              onTap: () {
                                widget.dropdownTextFieldController.text = tempFilteredItems[index];
                                widget.onChanged(tempFilteredItems[index]);
                                Navigator.pop(context);
                              },
                            );
                          },
                        ),
                      ),
                  ],
                ),
              );
            },
          ),
        ),
      ],
    ).then((value) {
      if (value != null) {
        widget.dropdownTextFieldController.text = value;
        widget.onChanged(value);
      }
      searchController.clear();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text(
          widget.labelText,
          style: const TextStyle(fontSize: 16),
        ),
        const SizedBox(height: 10),
        TextField(
          readOnly: true,
          onTap: () => _showDropdown(context),
          controller: widget.dropdownTextFieldController,
          decoration: InputDecoration(
            hintText: "Select an item",
            suffixIcon: IconButton(
              icon: const Icon(Icons.arrow_drop_down),
              onPressed: () => _showDropdown(context),
            ),
          ),
        ),
      ],
    );
  }
}

class CustomDropDown {
  Widget simpleDropDown(
    bool loading,
    String label,
    List<String> items,
    TextEditingController controller,
    Function? function, {
    bool showSearch = false,
  }) {
    return SizedBox(
      width: 300,
      child: CustomDropdown(
        labelText: label,
        items: items,
        onChanged: (value) => function?.call(value),
        dropdownTextFieldController: controller,
      ),
    );
  }
}

class Components {
  bool isCompanyLoading = false;
  List<String> listOfCompany = [];
  TextEditingController companyController = TextEditingController();
  void comIdDd(String? value) {}

  Widget customdd() {
    return CustomDropDown().simpleDropDown(
      isCompanyLoading,
      'Company',
      listOfCompany,
      companyController,
      comIdDd,
    );
  }
}

关键修改点

  1. 移除原有的filteredItems变量和filterItems方法,将过滤逻辑移到菜单内部
  2. 用StatefulBuilder给菜单内部提供状态更新能力,搜索时调用setStateMenu刷新ListView
  3. 菜单关闭时清空搜索框内容,优化用户体验
  4. 删除重复定义的CustomDropDown类,避免编译错误

方案二:改用Dialog实现更灵活的可搜索下拉框

如果showMenu限制较多,可以改用Dialog实现,Dialog本身是Stateful组件,无需额外处理状态:

void _showDropdown(BuildContext context) {
  List<String> tempFilteredItems = List.from(widget.items);
  showDialog(
    context: context,
    builder: (dialogContext) {
      return AlertDialog(
        contentPadding: EdgeInsets.zero,
        content: SizedBox(
          width: 300,
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Padding(
                padding: const EdgeInsets.all(8.0),
                child: TextField(
                  controller: searchController,
                  decoration: const InputDecoration(
                    hintText: 'Search...',
                    suffixIcon: Icon(Icons.search),
                  ),
                  onChanged: (query) {
                    setState(() {
                      tempFilteredItems = widget.items
                          .where((item) => item.toLowerCase().contains(query.toLowerCase()))
                          .toList();
                    });
                  },
                ),
              ),
              if (tempFilteredItems.isNotEmpty)
                SizedBox(
                  height: 200,
                  child: ListView.builder(
                    itemCount: tempFilteredItems.length,
                    itemBuilder: (BuildContext context, int index) {
                      return ListTile(
                        title: Text(tempFilteredItems[index]),
                        onTap: () {
                          widget.dropdownTextFieldController.text = tempFilteredItems[index];
                          widget.onChanged(tempFilteredItems[index]);
                          Navigator.pop(dialogContext);
                        },
                      );
                    },
                  ),
                ),
            ],
          ),
        ),
      );
    },
  ).then((_) => searchController.clear());
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 16:08:20