Search code examples
flutterlistviewfilter

Flutter having Listview in Popmenu is not filtering data


I am currently working on a Flutter project and I am trying to create a searchable dropdown using the CustomDropdown widget. The issue I am facing is that the filteredItems variable is updating correctly when I change the search filter text, but the ListView within the dropdown does not update to display the filtered items inside PopupMenuItem.

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();
  List<String> filteredItems = [];

  @override
  void initState() {
    super.initState();
    filteredItems = widget.items;
  }

  void filterItems(String query) {
    setState(() {
      filteredItems = widget.items
          .where((item) => item.toLowerCase().contains(query.toLowerCase()))
          .toList();
    });
  }

  void setSelectedValue(String? value) {
    widget.dropdownTextFieldController.text = value ?? '';
  }

  void _showDropdown(BuildContext context) {
    final RenderBox textFieldRenderBox =
        context.findRenderObject() as RenderBox;

    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>>[
        _buildSearchField(),
        if (filteredItems.isNotEmpty) _buildFilteredItems(),
      ],
    ).then((value) {
      if (value != null) {
        widget.dropdownTextFieldController.text = value;
        widget.onChanged(value);
      }
    });
  }

  PopupMenuItem<String> _buildSearchField() {
    return PopupMenuItem<String>(
      child: SizedBox(
        width: 300,
        child: TextField(
          controller: searchController,
          decoration: const InputDecoration(
            hintText: 'Search...',
            suffixIcon: Icon(Icons.search),
          ),
          onChanged: (query) {
            filterItems(query);
          },
        ),
      ),
    );
  }

  PopupMenuItem<String> _buildFilteredItems() {
    return PopupMenuItem<String>(
      child: SizedBox(
        height: 200,
        width: 300,
        child: ListView.builder(
          itemCount: filteredItems.length,
          itemBuilder: (BuildContext context, int index) {
            return ListTile(
              title: Text(filteredItems[index]),
              onTap: () {
                widget.dropdownTextFieldController.text = filteredItems[index];
                widget.onChanged(filteredItems[index]);
                Navigator.pop(context);
              },
            );
          },
        ),
      ),
    );
  }

  @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 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 {
  Widget customdd() {
    return CustomDropDown().simpleDropDown(
      isCompanyLoading,
      'Company',
      listOfCompany,
      companyController,
      comIdDd,
    );
  }
}

Solution

  • Use StatefulBuilder widget to achieve this thing.

    First make a widget function that return PopupMenuItem<String>.

    PopupMenuItem<String> _newPopupMenuItem() {
        return PopupMenuItem<String>(
          child: SizedBox(
            height: 300,
            width: 300,
            child: StatefulBuilder(
              builder: (context, setState) {
                return Column(
                  children: <Widget>[
                    TextField(
                      controller: searchController,
                      decoration: const InputDecoration(
                        hintText: 'Search...',
                        suffixIcon: Icon(Icons.search),
                      ),
                      onChanged: (query) {
                        setState(() {
                          filteredItems = widget.items.where((item) => item.toLowerCase().contains(query.toLowerCase())).toList();
                        });
                      },
                    ),
                    if (filteredItems.isNotEmpty)
                      PopupMenuItem<String>(
                        child: SizedBox(
                          height: 200,
                          width: 300,
                          child: ListView.builder(
                            itemCount: filteredItems.length,
                            itemBuilder: (BuildContext context, int index) {
                              return ListTile(
                                title: Text(filteredItems[index]),
                                onTap: () {
                                  widget.dropdownTextFieldController.text = filteredItems[index];
                                  widget.onChanged(filteredItems[index]);
                                  Navigator.pop(context);
                                },
                              );
                            },
                          ),
                        ),
                      ),
                  ],
                );
              },
            ),
          ),
        );
      }
    

    then use them into _showDropdown(), like below:

    showMenu(
          ...
          items: <PopupMenuEntry<String>>[
            // _buildSearchField(),
            // if (filteredItems.isNotEmpty) _buildFilteredItems(),
            _newPopupMenuItem()
          ],
         ...