Code Monkey home page Code Monkey logo

search_choices's Introduction

search_choices

Highly customizable Widget to search through a single or multiple choices list in a dialog box or a menu. Supports pagination and future/network/API/webservice searches with sort and filter. Each release is thoroughly tested through automated integrated testing with Flutster.

Platforms

This widget has been successfully tested on iOS, Android, Linux and Chrome. It is expected to work fine on MacOS and Windows.

Examples

The following examples are extracted from the example project available in the repository. More examples are available in this project.

Gallery

See code below.

Example name Demonstration
Single dialog Single dialog
Multi dialog Multi dialog
Single done button
dialog
Single done button dialog
Multi custom display
dialog
Multi custom display dialog
Multi select 3 dialog Multi select 3 dialog
Single menu Single menu
Multi menu Multi menu
Multi menu select
all/none
Multi menu select all or none
Multi dialog select
all/none without clear
Multi dialog select all or none without clear
Single dialog custom
keyboard
Single dialog custom keyboard
Single dialog object Single dialog object
Single dialog overflow Single dialog overflow
Single dialog readOnly Single dialog readOnly
Single dialog disabled Single dialog disabled
Single dialog
editable items
Single dialog editable items
Single menu
editable items
Single menu editable items
Multi dialog
editable items
Multi dialog editable items
Single dialog dark
mode
Single dialog dark mode
Single dialog ellipsis Single dialog ellipsis
Single dialog right
to left
Single dialog right to left
Update value from
outside the plugin
Update value from outside the plugin
Multi select 3 menu
no-autofocus
Multi select 3 menu no-autofocus
Multi dialog with
count and wrap
Multi dialog with count and wrap
Single dialog open
and set search terms
Single dialog open and set search terms
Single dialog custom
dialog
Single dialog custom dialog
Single dialog custom
decorations
Single dialog custom decorations
Single dialog paged Single dialog paged
Multi dialog paged
rtl
Multi dialog paged rtl
Single dialog paged
custom pagination
Single dialog paged custom pagination
Single menu paged Single menu paged
Single dialog
paged future
Single dialog paged future
Multi menu paged
future
Multi menu paged future
Single dialog
custom empty list
Single dialog custom empty list
Single dialog future
custom empty list
Single dialog future custom empty list
Single dialog onTap Single dialog onTap
Multi dialog paged
future
Multi dialog paged future
Single dialog future
custom error button
Single dialog future custom error button
Single dialog paged
delayed
Single dialog paged delayed
Single dialog paged
future delayed
Single dialog paged future delayed
Single dialog custom
field presentation
Single dialog custom field presentation
Single custom showDialogFn Single custom showDialogFn
Validator in form Validator in form

Code

Plugin usage

Add to your pubspec.yaml in the dependencies section:

  search_choices:

Get packages with command:

flutter packages get

Import:

import 'package:search_choices/search_choices.dart';

Call either the single choice or the multiple choice constructor.

Single choice constructor

Search choices Widget with a single choice that opens a dialog or a menu to let the user do the selection conveniently with a search.

  factory SearchChoices.single({
    Key? key,
    required List<DropdownMenuItem<T>> items,
    Function? onChanged,
    T? value,
    TextStyle? style,
    dynamic searchHint,
    dynamic hint,
    dynamic disabledHint,
    dynamic icon = const Icon(Icons.arrow_drop_down),
    dynamic underline,
    dynamic doneButton,
    dynamic label,
    dynamic closeButton = "Close",
    bool displayClearIcon = true,
    Icon clearIcon = const Icon(Icons.clear),
    Color? iconEnabledColor,
    Color? iconDisabledColor,
    double iconSize = 24.0,
    bool isExpanded = false,
    bool isCaseSensitiveSearch = false,
    Function? searchFn,
    Function? onClear,
    Function? selectedValueWidgetFn,
    TextInputType keyboardType = TextInputType.text,
    Function? validator,
    bool assertUniqueValue = true,
    Function? displayItem,
    bool dialogBox = true,
    BoxConstraints? menuConstraints,
    bool readOnly = false,
    Color? menuBackgroundColor,
    bool? rightToLeft,
    bool autofocus = true,
    Function? selectedAggregateWidgetFn,
    dynamic padding = 10.0,
    Function? setOpenDialog,
    Widget Function  (Widget titleBar,Widget searchBar, Widget list, Widget closeButton, BuildContext dropDownContext,)? buildDropDownDialog,
    EdgeInsets? dropDownDialogPadding,
    InputDecoration searchInputDecoration = const InputDecoration(
      prefixIcon: Icon(
        Icons.search,
        size: 24,
      ),
      contentPadding: EdgeInsets.symmetric(vertical: 12),
    ),
    int? itemsPerPage,
    PointerThisPlease<int>? currentPage,
    Widget Function(Widget listWidget, int totalFilteredItemsNb,
            Function updateSearchPage)?
        customPaginationDisplay,
    Future<Tuple2<List<DropdownMenuItem>, int>> Function(
            String? keyword,
            String? orderBy,
            bool? orderAsc,
            List<Tuple2<String, String>>? filters,
            int? pageNb)?
        futureSearchFn,
    Map<String, Map<String, dynamic>>? futureSearchOrderOptions,
    Map<String, Map<String, Object>>? futureSearchFilterOptions,
    dynamic emptyListWidget,
    Function? onTap,
    Function? futureSearchRetryButton,
    int? searchDelay,
    Widget Function(Widget fieldWidget,{bool selectionIsValid})? fieldPresentationFn,
    Decoration? fieldDecoration,
    Widget? clearSearchIcon,
    Future<void> Function(
      BuildContext context,
      Widget Function({
        String searchTerms,
      })
          menuWidget,
      String searchTerms,
    )?
        showDialogFn,
    FormFieldSetter<T>? onSaved,
    AutovalidateMode autovalidateMode = AutovalidateMode.onUserInteraction,
    String? restorationId,
    Function(Function pop)? giveMeThePop,
    Widget Function({
      required bool filter,
      required BuildContext context,
      required Function onPressed,
      int? nbFilters,
      bool? orderAsc,
      String? orderBy,
    })?
        buildFutureFilterOrOrderButton,
    Widget Function({
        required List<Tuple3<int, DropdownMenuItem, bool>> itemsToDisplay,
        required ScrollController scrollController,
        required bool thumbVisibility,
        required Widget emptyListWidget,
        required void Function(int index, T value, bool itemSelected) itemTapped,
        required Widget Function(DropdownMenuItem item, bool isItemSelected)
        displayItem,
    })?
      searchResultDisplayFn,
})
  • items with child: Widget displayed ; value: any object with .toString() used to match search keyword.
  • onChanged Function with parameter: value not returning executed after the selection is done.
  • value value to be preselected.
  • style used for the hint if it is given is String.
  • searchHint String|Widget|Function with no parameter returning String|Widget displayed at the top of the search dialog box.
  • hint String|Widget|Function with no parameter returning String|Widget displayed before any value is selected or after the selection is cleared.
  • disabledHint String|Widget|Function with no parameter returning String|Widget displayed instead of hint when the widget is displayed.
  • icon String|Widget|Function with parameter: value returning String|Widget displayed next to the selected item or the hint if none.
  • underline String|Widget|Function with parameter: value returning String|Widget displayed below the selected item or the hint if none.
  • doneButton String|Widget|Function with parameter: value returning String|Widget displayed at the top of the search dialog box.
  • label String|Widget|Function with parameter: value returning String|Widget displayed above the selected item or the hint if none.
  • closeButton String|Widget|Function with parameter: value returning String|Widget displayed at the bottom of the search dialog box.
  • displayClearIcon whether or not to display an icon to clear the selected value.
  • clearIcon Icon to be used for clearing the selected value.
  • iconEnabledColor Color to be used for enabled icons.
  • iconDisabledColor Color to be used for disabled icons.
  • iconSize for the icons next to the selected value (icon and clearIcon).
  • isExpanded can be necessary to avoid pixel overflows (zebra symptom).
  • isCaseSensitiveSearch only used when searchFn is not specified.
  • searchFn Function with parameters: keyword, items returning List as the list of indexes for the items to be displayed.
  • onClear Function with no parameter not returning executed when the clear icon is tapped.
  • selectedValueWidgetFn Function with parameter: item returning Widget to be used to display the selected value.
  • keyboardType used for the search.
  • validator Function with parameter: value returning String displayed below selected value when not valid and null when valid.
  • assertUniqueValue whether to run a consistency check of the list of items.
  • displayItem Function with parameters: item, selected returning Widget to be displayed in the search list.
  • dialogBox whether the search should be displayed as a dialog box or as a menu below the selected value if any.
  • menuConstraints BoxConstraints used to define the zone where to display the search menu. Example: BoxConstraints.tight(Size.fromHeight(250)) . Not to be used for dialogBox = true.
  • readOnly bool whether to let the user choose the value to select or just present the selected value if any.
  • menuBackgroundColor Color background color of the menu whether in dialog box or menu mode.
  • rightToLeft bool mirrors the widgets display for right to left languages defaulted to app.
  • autofocus bool automatically focuses on the search field bringing up the keyboard defaulted to true.
  • selectedAggregateWidgetFn Function with parameter: list of widgets presenting selected values, returning Widget to be displayed to present the selected items.
  • padding double or EdgeInsets sets the padding around the DropdownButton, defaulted to 10.0.
  • setOpenDialog Function sets the function to call to set the function to call in order to open the dialog with the search terms string as a parameter, defaulted to null.
  • buildDropDownDialog Function controls the layout of the dropdown dialog.
  • dropDownDialogPadding EdgeInsets sets the padding between the screen and the dialog.
  • searchInputDecoration InputDecoration sets the search bar decoration.
  • itemsPerPage int if set, organizes the search list per page with the given number of items displayed per page.
  • currentPage PointerThisPlease of an int if itemsPerPage is set, holds the page number for the search items to be displayed.
  • customPaginationDisplay Widget Function(Widget listWidget, int totalFilteredItemsNb, Function updateSearchPage) if itemsPerPage is set, customizes the display and the handling of the pagination on the search list.
  • futureSearchFn Function used to search items from the network. Must return items (up to itemsPerPage if set). Must return an int with the total number of results (allows the handling of pagination).
  • futureSearchOrderOptions Map when futureSearchFn is set, can be used to display search order options specified in the form {"order1Name":{"icon":order1IconWidget,"asc":true},}. Please refer to the documentation example: https://github.com/lcuis/search_choices/blob/master/example/lib/main.dart.
  • futureSearchFilterOptions Map when futureSearchFn is set, can be used to display search filters specified in the form {"filter1Name":{"icon":filter1IconWidget,"values":["value1",{"value2":filter1Value2Widget}}}. Please refer to the documentation example: https://github.com/lcuis/search_choices/blob/master/example/lib/main.dart.
  • emptyListWidget String|Widget|Function with parameter: keyword returning String|Widget displayed instead of the list of items in case it is empty.
  • onTap Function called when the user clicks on the Widget before it opens the dialog or the menu. Note that this is not called in case the Widget is disabled.
  • futureSearchRetryButton Function called to customize the Error - retry button displayed when there is an issue with the future search.
  • searchDelay int in milliseconds applied before the search is initiated. This applies to future and non-future searches.
  • fieldPresentationFn Function returning a Widget to customize the display of the field.
  • fieldDecoration Decoration is the decoration of the SearchChoices Widget while displaying the hints or the selected values. Should differ when selection is not valid.
  • clearSearchIcon Widget sets the icon to be used to clear the search.
  • showDialogFn Function allows the control of the dialog display.
  • onSaved as in FormField.
  • autovalidateMode as in FormField.
  • restorationId as in FormField.
  • giveMeThePop Function to pass the pop function so that the menu or dialog can be closed from outside the widget.
  • buildFutureFilterOrOrderButton Function to customize the order and filter button in case of future search. Where: ** filter is true if building filter button and false while building order button. ** nbFilters is set to the number of filters applied if any. ** orderAsc true when the applied order is ascending. ** orderBy is the string by which the search is sorted.
  • searchResultDisplayFn to customize the display of the search result items within the dialog or menu.

Multiple choice constructor

Search choices Widget with a multiple choice that opens a dialog or a menu to let the user do the selection conveniently with a search.

  factory SearchChoices.multiple({
    Key? key,
    required List<DropdownMenuItem<T>> items,
    Function? onChanged,
    List<int> selectedItems = const [],
    TextStyle? style,
    dynamic searchHint,
    dynamic hint,
    dynamic disabledHint,
    dynamic icon = const Icon(Icons.arrow_drop_down),
    dynamic underline,
    dynamic doneButton = "Done",
    dynamic label,
    dynamic closeButton = "Close",
    bool displayClearIcon = true,
    Icon clearIcon = const Icon(Icons.clear),
    Color? iconEnabledColor,
    Color? iconDisabledColor,
    double iconSize = 24.0,
    bool isExpanded = false,
    bool isCaseSensitiveSearch = false,
    Function? searchFn,
    Function? onClear,
    Function? selectedValueWidgetFn,
    TextInputType keyboardType = TextInputType.text,
    Function? validator,
    Function? displayItem,
    bool dialogBox = true,
    BoxConstraints? menuConstraints,
    bool readOnly = false,
    Color? menuBackgroundColor,
    bool? rightToLeft,
    bool autofocus = true,
    Function? selectedAggregateWidgetFn,
    dynamic padding = 10.0,
    Function? setOpenDialog,
    Widget Function  (Widget titleBar,Widget searchBar, Widget list, Widget closeButton, BuildContext dropDownContext,)? buildDropDownDialog,
    EdgeInsets? dropDownDialogPadding,
    InputDecoration searchInputDecoration = const InputDecoration(
      prefixIcon: Icon(
        Icons.search,
        size: 24,
      ),
      contentPadding: EdgeInsets.symmetric(vertical: 12),
    ),
    int? itemsPerPage,
    PointerThisPlease<int>? currentPage,
    Widget Function(Widget listWidget, int totalFilteredItemsNb,
            Function updateSearchPage)?
        customPaginationDisplay,
    Future<Tuple2<List<DropdownMenuItem>, int>> Function(
            String? keyword,
            String? orderBy,
            bool? orderAsc,
            List<Tuple2<String, String>>? filters,
            int? pageNb)?
        futureSearchFn,
    Map<String, Map<String, dynamic>>? futureSearchOrderOptions,
    Map<String, Map<String, Object>>? futureSearchFilterOptions,
    List<T>? futureSelectedValues,
    dynamic emptyListWidget,
    Function? onTap,
    Function? futureSearchRetryButton,
    int? searchDelay,
    Widget Function(Widget fieldWidget,{bool selectionIsValid})? fieldPresentationFn,
    Decoration? fieldDecoration,
    Widget? clearSearchIcon,
    Future<void> Function(
      BuildContext context,
      Widget Function({
        String searchTerms,
      })
          menuWidget,
      String searchTerms,
    )?
        showDialogFn,
    FormFieldSetter<T>? onSaved,
    String? Function(List<dynamic>)? listValidator,
    AutovalidateMode autovalidateMode = AutovalidateMode.onUserInteraction,
    String? restorationId,
    Function(Function pop)? giveMeThePop,
    Widget Function({
      required bool filter,
      required BuildContext context,
      required Function onPressed,
      int? nbFilters,
      bool? orderAsc,
      String? orderBy,
    })?
        buildFutureFilterOrOrderButton,
    Widget Function({
        required List<Tuple3<int, DropdownMenuItem, bool>> itemsToDisplay,
        required ScrollController scrollController,
        required bool thumbVisibility,
        required Widget emptyListWidget,
        required void Function(int index, T value, bool itemSelected) itemTapped,
        required Widget Function(DropdownMenuItem item, bool isItemSelected)
        displayItem,
    })?
      searchResultDisplayFn,
  })
  • items with child: Widget displayed ; value: any object with .toString() used to match search keyword.
  • onChanged Function with parameter: selectedItems not returning executed after the selection is done.
  • selectedItems indexes of items to be preselected.
  • style used for the hint if it is given is String.
  • searchHint String|Widget|Function with no parameter returning String|Widget displayed at the top of the search dialog box.
  • hint String|Widget|Function with no parameter returning String|Widget displayed before any value is selected or after the selection is cleared.
  • disabledHint String|Widget|Function with no parameter returning String|Widget displayed instead of hint when the widget is displayed.
  • icon String|Widget|Function with parameter: selectedItems returning String|Widget displayed next to the selected items or the hint if none.
  • underline String|Widget|Function with parameter: selectedItems returning String|Widget displayed below the selected items or the hint if none.
  • doneButton String|Widget|Function with parameter: selectedItems returning String|Widget displayed at the top of the search dialog box. Cannot be null in multiple selection mode.
  • label String|Widget|Function with parameter: selectedItems returning String|Widget displayed above the selected items or the hint if none.
  • closeButton String|Widget|Function with parameter: selectedItems returning String|Widget displayed at the bottom of the search dialog box.
  • displayClearIcon whether or not to display an icon to clear the selected values.
  • clearIcon Icon to be used for clearing the selected values.
  • iconEnabledColor Color to be used for enabled icons.
  • iconDisabledColor Color to be used for disabled icons.
  • iconSize for the icons next to the selected values (icon and clearIcon).
  • isExpanded can be necessary to avoid pixel overflows (zebra symptom).
  • isCaseSensitiveSearch only used when searchFn is not specified.
  • searchFn Function with parameters: keyword, items returning List as the list of indexes for the items to be displayed.
  • onClear Function with no parameter not returning executed when the clear icon is tapped.
  • selectedValueWidgetFn Function with parameter: item returning Widget to be used to display the selected values.
  • keyboardType used for the search.
  • validator Function with parameter: selectedItems returning String displayed below selected values when not valid and null when valid.
  • displayItem Function with parameters: item, selected returning Widget to be displayed in the search list.
  • dialogBox whether the search should be displayed as a dialog box or as a menu below the selected values if any.
  • menuConstraints BoxConstraints used to define the zone where to display the search menu. Example: BoxConstraints.tight(Size.fromHeight(250)) . Not to be used for dialogBox = true.
  • readOnly bool whether to let the user choose the value to select or just present the selected value if any.
  • menuBackgroundColor Color background color of the menu whether in dialog box or menu mode.
  • rightToLeft bool mirrors the widgets display for right to left languages defaulted to app.
  • autofocus bool automatically focuses on the search field bringing up the keyboard defaulted to true.
  • selectedAggregateWidgetFn Function with parameter: list of widgets presenting selected values, returning Widget to be displayed to present the selected items.
  • padding double or EdgeInsets sets the padding around the DropdownButton, defaulted to 10.0.
  • setOpenDialog Function sets the function to call to set the function to call in order to open the dialog with the search terms string as a parameter, defaulted to null.
  • buildDropDownDialog Function controls the layout of the dropdown dialog.
  • dropDownDialogPadding EdgeInsets sets the padding between the screen and the dialog.
  • searchInputDecoration InputDecoration sets the search bar decoration.
  • itemsPerPage int if set, organizes the search list per page with the given number of items displayed per page.
  • currentPage PointerThisPlease if itemsPerPage is set, holds the page number for the search items to be displayed.
  • customPaginationDisplay Widget Function(Widget listWidget, int totalFilteredItemsNb, Function updateSearchPage) if itemsPerPage is set, customizes the display and the handling of the pagination on the search list.
  • futureSearchFn Future Function(String keyword, List itemsListToClearAndFill, int pageNb) used to search items from the network. Must return items (up to itemsPerPage if set). Must return an int with the total number of results (allows the handling of pagination).
  • futureSearchOrderOptions Map<String, Map<String,dynamic>> when futureSearchFn is set, can be used to display search order options specified in the form {"order1Name":{"icon":order1IconWidget,"asc":true},}. Please refer to the documentation example: https://github.com/lcuis/search_choices/blob/master/example/lib/main.dart.
  • futureSearchFilterOptions Map<String, Map<String, Object>> when futureSearchFn is set, can be used to display search filters specified in the form {"filter1Name":{"icon":filter1IconWidget,"values":["value1",{"value2":filter1Value2Widget}}}. Please refer to the documentation example: https://github.com/lcuis/search_choices/blob/master/example/lib/main.dart.
  • futureSelectedValues List contains the list of selected values in case of future search in multiple selection mode.
  • emptyListWidget String|Widget|Function with parameter: keyword returning String|Widget displayed instead of the list of items in case it is empty.
  • onTap Function called when the user clicks on the Widget before it opens the dialog or the menu. Note that this is not called in case the Widget is disabled.
  • futureSearchRetryButton Function called to customize the Error - retry button displayed when there is an issue with the future search.
  • searchDelay int in milliseconds applied before the search is initiated. This applies to future and non-future searches.
  • fieldPresentationFn Function returning a Widget to customize the display of the field.
  • fieldDecoration Decoration is the decoration of the SearchChoices Widget while displaying the hints or the selected values. Should differ when selection is not valid.
  • clearSearchIcon Widget sets the icon to be used to clear the search.
  • showDialogFn Function allows the control of the dialog display.
  • onSaved as in FormField.
  • listValidator Function with parameter: List returning String displayed below selected value when not valid and null when valid.
  • autovalidateMode as in FormField.
  • restorationId as in FormField.
  • giveMeThePop Function to pass the pop function so that the menu or dialog can be closed from outside the widget.
  • buildFutureFilterOrOrderButton Function to customize the order and filter button in case of future search. Where: ** filter is true if building filter button and false while building order button. ** nbFilters is set to the number of filters applied if any. ** orderAsc true when the applied order is ascending. ** orderBy is the string by which the search is sorted.
  • searchResultDisplayFn to customize the display of the search result items within the dialog or menu.

Example app usage

Clone repository:

git clone https://github.com/lcuis/search_choices.git

Go to plugin folder:

cd search_choices

Optionally enable web:

flutter config --enable-web

Create project:

flutter create .

To run automated tests:

flutter test

Optionally generate documentation:

pub global activate dartdoc
dartdoc

Go to example app folder:

cd example

To run web:

run -d chrome

To build web to folder build/web:

flutter build web

To run on a connected device:

flutter run

To build Android app to build/app/outputs/apk/release/app-release.apk:

flutter build apk

To build iOS app on Mac:

flutter build ios

Single dialog

SearchChoices.single(
        items: items,
        value: selectedValueSingleDialog,
        hint: "Select one",
        searchHint: "Select one",
        onChanged: (value) {
          setState(() {
            selectedValueSingleDialog = value;
          });
        },
        isExpanded: true,
      )

Multi dialog

SearchChoices.multiple(
        items: items,
        selectedItems: selectedItemsMultiDialog,
        hint: Padding(
          padding: const EdgeInsets.all(12.0),
          child: Text("Select any"),
        ),
        searchHint: "Select any",
        onChanged: (value) {
          setState(() {
            selectedItemsMultiDialog = value;
          });
        },
        closeButton: (selectedItems) {
          return (selectedItems.isNotEmpty
              ? "Save ${selectedItems.length == 1 ? '"' + items[selectedItems.first].value.toString() + '"' : '(' + selectedItems.length.toString() + ')'}"
              : "Save without selection");
        },
        isExpanded: true,
      )

Single done button dialog

SearchChoices.single(
        items: items,
        value: selectedValueSingleDoneButtonDialog,
        hint: "Select one",
        searchHint: "Select one",
        onChanged: (value) {
          setState(() {
            selectedValueSingleDoneButtonDialog = value;
          });
        },
        doneButton: "Done",
        displayItem: (item, selected) {
          return (Row(children: [
            selected
                ? Icon(
                    Icons.radio_button_checked,
                    color: Colors.grey,
                  )
                : Icon(
                    Icons.radio_button_unchecked,
                    color: Colors.grey,
                  ),
            SizedBox(width: 7),
            Expanded(
              child: item,
            ),
          ]));
        },
        isExpanded: true,
      )

Multi custom display dialog

SearchChoices.multiple(
        items: items,
        selectedItems: selectedItemsMultiCustomDisplayDialog,
        hint: Padding(
          padding: const EdgeInsets.all(12.0),
          child: Text("Select any"),
        ),
        searchHint: "Select any",
        onChanged: (value) {
          setState(() {
            selectedItemsMultiCustomDisplayDialog = value;
          });
        },
        displayItem: (item, selected) {
          return (Row(children: [
            selected
                ? Icon(
                    Icons.check,
                    color: Colors.green,
                  )
                : Icon(
                    Icons.check_box_outline_blank,
                    color: Colors.grey,
                  ),
            SizedBox(width: 7),
            Expanded(
              child: item,
            ),
          ]));
        },
        selectedValueWidgetFn: (item) {
          return (Center(
              child: Card(
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(10),
                    side: BorderSide(
                      color: Colors.brown,
                      width: 0.5,
                    ),
                  ),
                  margin: EdgeInsets.all(12),
                  child: Padding(
                    padding: const EdgeInsets.all(8),
                    child: Text(item.toString()),
                  ))));
        },
        doneButton: (selectedItemsDone, doneContext) {
          return (ElevatedButton(
              onPressed: () {
                Navigator.pop(doneContext);
                setState(() {});
              },
              child: Text("Save")));
        },
        closeButton: null,
        style: TextStyle(fontStyle: FontStyle.italic),
        searchFn: (String keyword, items) {
          List<int> ret = [];
          if (items != null && keyword.isNotEmpty) {
            keyword.split(" ").forEach((k) {
              int i = 0;
              items.forEach((item) {
                if (k.isNotEmpty &&
                    (item.value
                        .toString()
                        .toLowerCase()
                        .contains(k.toLowerCase()))) {
                  ret.add(i);
                }
                i++;
              });
            });
          }
          if (keyword.isEmpty) {
            ret = Iterable<int>.generate(items.length).toList();
          }
          return (ret);
        },
        clearIcon: Icon(Icons.clear_all),
        icon: Icon(Icons.arrow_drop_down_circle),
        label: "Label for multi",
        underline: Container(
          height: 1.0,
          decoration: BoxDecoration(
              border:
                  Border(bottom: BorderSide(color: Colors.teal, width: 3.0))),
        ),
        iconDisabledColor: Colors.brown,
        iconEnabledColor: Colors.indigo,
        dropDownDialogPadding: EdgeInsets.symmetric(
          vertical: 80,
          horizontal: 80,
        ),
        isExpanded: true,
      )

Multi select 3 dialog

SearchChoices.multiple(
        items: items,
        selectedItems: selectedItemsMultiSelect3Dialog,
        hint: "Select 3 items",
        searchHint: "Select 3",
        validator: (selectedItemsForValidator) {
          if (selectedItemsForValidator.length != 3) {
            return ("Must select 3");
          }
          return (null);
        },
        onChanged: (value) {
          setState(() {
            selectedItemsMultiSelect3Dialog = value;
          });
        },
        doneButton: (selectedItemsDone, doneContext) {
          return (ElevatedButton(
              onPressed: selectedItemsDone.length != 3
                  ? null
                  : () {
                      Navigator.pop(doneContext);
                      setState(() {});
                    },
              child: Text("Save")));
        },
        closeButton: (selectedItemsClose) {
          return (selectedItemsClose.length == 3 ? "Ok" : null);
        },
        isExpanded: true,
      )

Single menu

SearchChoices.single(
        items: items,
        value: selectedValueSingleMenu,
        hint: "Select one",
        searchHint: null,
        onChanged: (value) {
          setState(() {
            selectedValueSingleMenu = value;
          });
        },
        dialogBox: false,
        isExpanded: true,
        menuConstraints: BoxConstraints.tight(Size.fromHeight(350)),
      )

Multi menu

SearchChoices.multiple(
        items: items,
        selectedItems: selectedItemsMultiMenu,
        hint: "Select any",
        searchHint: "",
        doneButton: "Close",
        closeButton: SizedBox.shrink(),
        onChanged: (value) {
          setState(() {
            selectedItemsMultiMenu = value;
          });
        },
        dialogBox: false,
        isExpanded: true,
        menuConstraints: BoxConstraints.tight(Size.fromHeight(350)),
      )

Multi menu select all/none

SearchChoices.multiple(
        items: items,
        selectedItems: selectedItemsMultiMenuSelectAllNone,
        hint: "Select any",
        searchHint: "Select any",
        onChanged: (value) {
          setState(() {
            selectedItemsMultiMenuSelectAllNone = value;
          });
        },
        dialogBox: false,
        closeButton: (selectedItemsClose, closeContext, Function updateParent) {
          return Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: <Widget>[
              ElevatedButton(
                  onPressed: () {
                    setState(() {
                      selectedItemsClose.clear();
                      selectedItemsClose.addAll(
                          Iterable<int>.generate(items.length).toList());
                    });
                    updateParent(selectedItemsClose);
                  },
                  child: Text("Select all")),
              ElevatedButton(
                  onPressed: () {
                    setState(() {
                      selectedItemsClose.clear();
                    });
                    updateParent(selectedItemsClose);
                  },
                  child: Text("Select none")),
            ],
          );
        },
        isExpanded: true,
        menuConstraints: BoxConstraints.tight(Size.fromHeight(350)),
      )

Multi dialog select all/none without clear

SearchChoices.multiple(
        items: items,
        selectedItems: selectedItemsMultiDialogSelectAllNoneWoClear,
        hint: "Select any",
        searchHint: "Select any",
        displayClearIcon: false,
        onChanged: (value) {
          setState(() {
            selectedItemsMultiDialogSelectAllNoneWoClear = value;
          });
        },
        dialogBox: true,
        closeButton: (selectedItemsClose, closeContext, Function updateParent) {
          return Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: <Widget>[
              ElevatedButton(
                  onPressed: () {
                    setState(() {
                      selectedItemsClose.clear();
                      selectedItemsClose.addAll(
                          Iterable<int>.generate(items.length).toList());
                    });
                    updateParent(selectedItemsClose);
                  },
                  child: Text("Select all")),
              ElevatedButton(
                  onPressed: () {
                    setState(() {
                      selectedItemsClose.clear();
                    });
                    updateParent(selectedItemsClose);
                  },
                  child: Text("Select none")),
            ],
          );
        },
        isExpanded: true,
      )

Single dialog custom keyboard

SearchChoices.single(
        items: Iterable<int>.generate(20).toList().map((i) {
          return (DropdownMenuItem(
            child: Text(i.toString()),
            value: i.toString(),
          ));
        }).toList(),
        value: selectedValueSingleDialogCustomKeyboard,
        hint: "Select one number",
        searchHint: "Select one number",
        onChanged: (value) {
          setState(() {
            selectedValueSingleDialogCustomKeyboard = value;
          });
        },
        dialogBox: true,
        keyboardType: TextInputType.number,
        isExpanded: true,
      )

Single dialog object

SearchChoices.single(
        items: numberItems,
        value: selectedNumber,
        hint: "Select one number",
        searchHint: "Select one number",
        onChanged: (value) {
          setState(() {
            selectedNumber = value;
          });
        },
        dialogBox: true,
        isExpanded: true,
      )

Single dialog overflow

SearchChoices.single(
        items: [
          DropdownMenuItem(
            child: Text(
                "way too long text for a smartphone at least one that goes in a normal sized pair of trousers but maybe not for a gigantic screen like there is one at my cousin's home in a very remote country where I wouldn't want to go right now"),
            value:
                "way too long text for a smartphone at least one that goes in a normal sized pair of trousers but maybe not for a gigantic screen like there is one at my cousin's home in a very remote country where I wouldn't want to go right now",
          )
        ],
        value: selectedValueSingleDialogOverflow,
        hint: "Select one",
        searchHint: "Select one",
        onChanged: (value) {
          setState(() {
            selectedValueSingleDialogOverflow = value;
          });
        },
        dialogBox: true,
        isExpanded: true,
      )

Single dialog readOnly

SearchChoices.single(
        items: [
          DropdownMenuItem(
            child: Text("one item"),
            value: "one item",
          )
        ],
        value: "one item",
        hint: "Select one",
        searchHint: "Select one",
        disabledHint: "Disabled",
        onChanged: (value) {
          setState(() {});
        },
        dialogBox: true,
        isExpanded: true,
        readOnly: true,
      )

Single dialog disabled

SearchChoices.single(
        items: [
          DropdownMenuItem(
            child: Text("one item"),
            value: "one item",
          )
        ],
        value: "one item",
        hint: "Select one",
        searchHint: "Select one",
        disabledHint: "Disabled",
        onChanged: null,
        dialogBox: true,
        isExpanded: true,
      )

Single dialog editable items

This example lets the user add and remove items to and from the list of choices within a dialog. One can limit the number of items that can be added (100 here).

    input = TextFormField(
      validator: (value) {
        return ((value?.length ?? 0) < 6
            ? "must be at least 6 characters long"
            : null);
      },
      initialValue: inputString,
      onChanged: (value) {
        inputString = value;
      },
      autofocus: true,
    );
...
  addItemDialog() async {
    return await showDialog(
      context: MyApp.navKey.currentState?.overlay?.context ?? context,
      builder: (BuildContext alertContext) {
        return (AlertDialog(
          title: Text("Add an item"),
          content: Form(
            key: _formKey,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                input ?? SizedBox.shrink(),
                TextButton(
                  onPressed: () {
                    if (_formKey.currentState?.validate() ?? false) {
                      setState(() {
                        editableItems.add(DropdownMenuItem(
                          child: Text(inputString),
                          value: inputString,
                        ));
                      });
                      Navigator.pop(alertContext, inputString);
                    }
                  },
                  child: Text("Ok"),
                ),
                TextButton(
                  onPressed: () {
                    Navigator.pop(alertContext, null);
                  },
                  child: Text("Cancel"),
                ),
              ],
            ),
          ),
        ));
      },
    );
  }
...
SearchChoices.single(
        items: editableItems,
        value: selectedValueSingleDialogEditableItems,
        hint: "Select one",
        searchHint: "Select one",
        disabledHint: (Function updateParent) {
          return (TextButton(
            onPressed: () {
              addItemDialog().then((value) async {
                updateParent(value);
              });
            },
            child: Text("No choice, click to add one"),
          ));
        },
        closeButton:
            (String? value, BuildContext closeContext, Function updateParent) {
          return (editableItems.length >= 100
              ? "Close"
              : TextButton(
                  onPressed: () {
                    addItemDialog().then((value) async {
                      if (value != null &&
                          editableItems.indexWhere(
                                  (element) => element.value == value) !=
                              -1) {
                        Navigator.pop(
                            MyApp.navKey.currentState?.overlay?.context ??
                                context);
                        updateParent(value);
                      }
                    });
                  },
                  child: Text("Add and select item"),
                ));
        },
        onChanged: (String? value) {
          setState(() {
            if (!(value is NotGiven)) {
              selectedValueSingleDialogEditableItems = value;
            }
          });
        },
        displayItem: (item, selected, Function updateParent) {
          return (Row(children: [
            selected
                ? Icon(
                    Icons.check,
                    color: Colors.green,
                  )
                : Icon(
                    Icons.check_box_outline_blank,
                    color: Colors.transparent,
                  ),
            SizedBox(width: 7),
            Expanded(
              child: item,
            ),
            IconButton(
              icon: Icon(
                Icons.delete,
                color: Colors.red,
              ),
              onPressed: () {
                editableItems.removeWhere((element) => item == element);
                updateParent(null);
                setState(() {});
              },
            ),
          ]));
        },
        dialogBox: true,
        isExpanded: true,
        doneButton: "Done",
      )

Single menu editable items

This example lets the user add and remove items to and from the list of choices within a menu. One can limit the number of items that can be added (100 here).

SearchChoices.single(
        items: editableItems,
        value: selectedValueSingleMenuEditableItems,
        hint: "Select one",
        searchHint: "Select one",
        disabledHint: (Function updateParent) {
          return (TextButton(
            onPressed: () {
              addItemDialog().then((value) async {
                updateParent(value);
              });
            },
            child: Text("No choice, click to add one"),
          ));
        },
        closeButton:
            (String? value, BuildContext closeContext, Function updateParent) {
          return (editableItems.length >= 100
              ? "Close"
              : TextButton(
                  onPressed: () {
                    addItemDialog().then((value) async {
                      if (value != null &&
                          editableItems.indexWhere(
                                  (element) => element.value == value) !=
                              -1) {
                        updateParent(value, true);
                      }
                    });
                  },
                  child: Text("Add and select item"),
                ));
        },
        onChanged: (String? value, Function? pop) {
          setState(() {
            if (!(value is NotGiven)) {
              selectedValueSingleMenuEditableItems = value;
            }
          });
          if (pop != null && !(value is NotGiven) && value != null) {
            pop();
          }
        },
        displayItem: (DropdownMenuItem item, selected, Function updateParent) {
          bool deleteRequested = false;
          return ListTile(
            leading: selected
                ? Icon(
                    Icons.check,
                    color: Colors.green,
                  )
                : Icon(
                    Icons.check_box_outline_blank,
                    color: Colors.transparent,
                  ),
            title: item,
            trailing: IconButton(
              icon: Icon(
                Icons.delete,
                color: Colors.red,
              ),
              onPressed: () {
                deleteRequested = true;
                editableItems.removeWhere((element) => item == element);
                updateParent(selected ? null : NotGiven(), false);
                setState(() {});
              },
            ),
            onTap: () {
              if (!deleteRequested) {
                updateParent(item.value, true);
              }
            },
            horizontalTitleGap: 0,
          );
        },
        dialogBox: false,
        isExpanded: true,
        doneButton: "Done",
        menuConstraints: BoxConstraints.tight(Size.fromHeight(350)),
      )

Multi dialog editable items

Same example as previously but with multiple selection.

    input = TextFormField(
      validator: (value) {
        return ((value?.length ?? 0) < 6
            ? "must be at least 6 characters long"
            : null);
      },
      initialValue: inputString,
      onChanged: (value) {
        inputString = value;
      },
      autofocus: true,
    );
    super.initState();
  }
...
  addItemDialog() async {
    return await showDialog(
      context: MyApp.navKey.currentState?.overlay?.context ?? context,
      builder: (BuildContext alertContext) {
        return (AlertDialog(
          title: Text("Add an item"),
          content: Form(
            key: _formKey,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                input ?? SizedBox.shrink(),
                TextButton(
                  onPressed: () {
                    if (_formKey.currentState?.validate() ?? false) {
                      setState(() {
                        editableItems.add(DropdownMenuItem(
                          child: Text(inputString),
                          value: inputString,
                        ));
                      });
                      Navigator.pop(alertContext, inputString);
                    }
                  },
                  child: Text("Ok"),
                ),
                TextButton(
                  onPressed: () {
                    Navigator.pop(alertContext, null);
                  },
                  child: Text("Cancel"),
                ),
              ],
            ),
          ),
        ));
      },
    );
  }
...
SearchChoices.multiple(
        items: editableItems,
        selectedItems: editableSelectedItems,
        hint: "Select any",
        searchHint: "Select any",
        disabledHint: (Function updateParent) {
          return (TextButton(
            onPressed: () {
              addItemDialog().then((value) async {
                if (value != null) {
                  editableSelectedItems = [0];
                  updateParent(editableSelectedItems);
                }
              });
            },
            child: Text("No choice, click to add one"),
          ));
        },
        closeButton: (List<int> values, BuildContext closeContext,
            Function updateParent) {
          return (editableItems.length >= 100
              ? "Close"
              : TextButton(
                  onPressed: () {
                    addItemDialog().then((value) async {
                      if (value != null) {
                        int itemIndex = editableItems
                            .indexWhere((element) => element.value == value);
                        if (itemIndex != -1) {
                          editableSelectedItems.add(itemIndex);
                          Navigator.pop(
                              MyApp.navKey.currentState?.overlay?.context ??
                                  context);
                          updateParent(editableSelectedItems);
                        }
                      }
                    });
                  },
                  child: Text("Add and select item"),
                ));
        },
        onChanged: (values) {
          setState(() {
            if (!(values is NotGiven)) {
              editableSelectedItems = values;
            }
          });
        },
        displayItem: (item, selected, Function updateParent) {
          return (Row(children: [
            selected
                ? Icon(
                    Icons.check_box,
                    color: Colors.black,
                  )
                : Icon(
                    Icons.check_box_outline_blank,
                    color: Colors.black,
                  ),
            SizedBox(width: 7),
            Expanded(
              child: item,
            ),
            IconButton(
              icon: Icon(
                Icons.delete,
                color: Colors.red,
              ),
              onPressed: () {
                int indexOfItem = editableItems.indexOf(item);
                editableItems.removeWhere((element) => item == element);
                editableSelectedItems
                    .removeWhere((element) => element == indexOfItem);
                for (int i = 0; i < editableSelectedItems.length; i++) {
                  if (editableSelectedItems[i] > indexOfItem) {
                    editableSelectedItems[i]--;
                  }
                }
                updateParent(editableSelectedItems);
                setState(() {});
              },
            ),
          ]));
        },
        dialogBox: true,
        isExpanded: true,
        doneButton: "Done",
      )

Single dialog dark mode

Card(
        color: Colors.black,
        child: SearchChoices.single(
          items: items.map((item) {
            return (DropdownMenuItem(
              child: Text(
                item.value,
                style: TextStyle(color: Colors.white),
              ),
              value: item.value,
            ));
          }).toList(),
          value: selectedValueSingleDialogDarkMode,
          hint: Text(
            "Select one",
            style: TextStyle(color: Colors.white),
          ),
          searchHint: Text(
            "Select one",
            style: TextStyle(color: Colors.white),
          ),
          style: TextStyle(color: Colors.white, backgroundColor: Colors.black),
          closeButton: TextButton(
            onPressed: () {
              Navigator.pop(
                  MyApp.navKey.currentState?.overlay?.context ?? context);
            },
            child: Text(
              "Close",
              style: TextStyle(color: Colors.white),
            ),
          ),
          menuBackgroundColor: Colors.black,
          iconEnabledColor: Colors.white,
          iconDisabledColor: Colors.grey,
          onChanged: (value) {
            setState(() {
              selectedValueSingleDialogDarkMode = value;
            });
          },
          isExpanded: true,
        ),
      )

Single dialog ellipsis

SearchChoices.single(
        items: [
          DropdownMenuItem(
            child: Text(
              "way too long text for a smartphone at least one that goes in a normal sized pair of trousers but maybe not for a gigantic screen like there is one at my cousin's home in a very remote country where I wouldn't want to go right now",
              overflow: TextOverflow.ellipsis,
            ),
            value:
                "way too long text for a smartphone at least one that goes in a normal sized pair of trousers but maybe not for a gigantic screen like there is one at my cousin's home in a very remote country where I wouldn't want to go right now",
          )
        ],
        value: selectedValueSingleDialogEllipsis,
        hint: "Select one",
        searchHint: "Select one",
        onChanged: (value) {
          setState(() {
            selectedValueSingleDialogEllipsis = value;
          });
        },
        selectedValueWidgetFn: (item) {
          return DropdownMenuItem(
            child: (Text(
              item,
              overflow: TextOverflow.ellipsis,
            )),
          );
        },
        dialogBox: true,
        isExpanded: true,
      )

Single dialog right to left

In support for Arabic, Hebrew and other RTL languages.

SearchChoices.single(
        items: [
          "طنجة",
          "فاس‎",
          "أكادير‎",
          "تزنيت‎",
          "آكــلــو",
          "سيدي بيبي",
        ].map<DropdownMenuItem<String>>((string) {
          return (DropdownMenuItem<String>(
            child: Text(
              string,
              textDirection: TextDirection.rtl,
            ),
            value: string,
          ));
        }).toList(),
        value: selectedValueSingleDialogRightToLeft,
        hint: Row(
          textDirection: TextDirection.rtl,
          children: [
            Text(
              "ختار",
              textDirection: TextDirection.rtl,
            ),
          ],
        ),
        searchHint: Text(
          "ختار",
          textDirection: TextDirection.rtl,
        ),
        closeButton: TextButton(
          onPressed: () {
            Navigator.pop(
                MyApp.navKey.currentState?.overlay?.context ?? context);
          },
          child: SizedBox(
            width: 50,
            child: Text(
              "سدّ",
              maxLines: 1,
              softWrap: false,
              textDirection: TextDirection.rtl,
            ),
          ),
        ),
        onChanged: (value) {
          setState(() {
            selectedValueSingleDialogRightToLeft = value;
          });
        },
        isExpanded: true,
        rightToLeft: true,
        displayItem: (item, selected) {
          return (Row(textDirection: TextDirection.rtl, children: [
            selected
                ? Icon(
                    Icons.radio_button_checked,
                    color: Colors.grey,
                  )
                : Icon(
                    Icons.radio_button_unchecked,
                    color: Colors.grey,
                  ),
            SizedBox(width: 7),
            item,
            Expanded(
              child: SizedBox.shrink(),
            ),
          ]));
        },
        selectedValueWidgetFn: (item) {
          return DropdownMenuItem(
            child: Row(
              textDirection: TextDirection.rtl,
              children: <Widget>[
                (Text(
                  item,
                  textDirection: TextDirection.rtl,
                )),
              ],
            ),
          );
        },
      )

Update value from outside the plugin

A button external to the plugin can be used to set the selected value.

Column(
        children: [
          SearchChoices.single(
            items: items,
            value: selectedValueUpdateFromOutsideThePlugin,
            hint: Text('Select One'),
            searchHint: new Text(
              'Select One',
              style: new TextStyle(fontSize: 20),
            ),
            onChanged: (value) {
              setState(() {
                selectedValueUpdateFromOutsideThePlugin = value;
              });
            },
            isExpanded: true,
          ),
          TextButton(
            child: Text("Select dolor sit"),
            onPressed: () {
              setState(() {
                selectedValueUpdateFromOutsideThePlugin = "dolor sit";
              });
            },
          ),
        ],
      )

Multi select 3 menu no-autofocus

Doesn't bring up the keyboard automatically.

SearchChoices.multiple(
        items: items,
        selectedItems: selectedItemsMultiSelect3Menu,
        hint: "Select 3 items",
        searchHint: "Select 3",
        validator: (selectedItemsForValidatorWithMenu) {
          if (selectedItemsForValidatorWithMenu.length != 3) {
            return ("Must select 3");
          }
          return (null);
        },
        onChanged: (value) {
          setState(() {
            selectedItemsMultiSelect3Menu = value;
          });
        },
        isExpanded: true,
        dialogBox: false,
        menuConstraints: BoxConstraints.tight(Size.fromHeight(350)),
        autofocus: false,
      )

Multi dialog with count and wrap

Customization of the way the selection is displayed.

SearchChoices.multiple(
        items: items,
        selectedItems: selectedItemsMultiDialogWithCountAndWrap,
        hint: "Select items",
        searchHint: "Select items",
        onChanged: (value) {
          setState(() {
            selectedItemsMultiDialogWithCountAndWrap = value;
          });
        },
        isExpanded: true,
        selectedValueWidgetFn: (item) {
          return (Container(
            margin: const EdgeInsets.all(15.0),
            padding: const EdgeInsets.all(3.0),
            decoration:
                BoxDecoration(border: Border.all(color: Colors.blueAccent)),
            child: Text(
              item,
              overflow: TextOverflow.ellipsis,
            ),
          ));
        },
        selectedAggregateWidgetFn: (List<Widget> list) {
          return (Column(children: [
            Text("${list.length} items selected"),
            Wrap(children: list),
          ]));
        },
      )

Single dialog open and set search terms

A button external to the plugin can be used to open the dialog and set the search terms.

SearchChoices.single(
        label: Column(
          children: items.map((item) {
            return (ElevatedButton(
              child: item.child,
              onPressed: () {
                openDialog!(item.value.toString());
              },
            ));
          }).toList(),
        ),
        items: items,
        value: selectedValueSingleDialog,
        hint: "Select one",
        searchHint: "Select one",
        onChanged: (value) {
          setState(() {
            selectedValueSingleDialog = value;
          });
        },
        isExpanded: true,
        setOpenDialog: (externalOpenDialog) {
          openDialog = externalOpenDialog;
        },
      )

Single dialog custom dialog

Customize the way the dialog is displayed to define how to show the title bar, the search bar, the list and the close button.

SearchChoices.single(
        items: items,
        value: selectedValueSingleDialog,
        hint: "Select one",
        searchHint: "Select one",
        onChanged: (value) {
          setState(() {
            selectedValueSingleDialog = value;
          });
        },
        isExpanded: true,
        buildDropDownDialog: (
          Widget titleBar,
          Widget searchBar,
          Widget list,
          Widget closeButton,
          BuildContext dropDownContext,
        ) {
          return (AnimatedContainer(
            padding: MediaQuery.of(dropDownContext).viewInsets,
            duration: const Duration(milliseconds: 300),
            child: Card(
              margin: EdgeInsets.symmetric(vertical: 30, horizontal: 40),
              child: Container(
                padding: EdgeInsets.symmetric(vertical: 35, horizontal: 45),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.start,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  mainAxisSize: MainAxisSize.min,
                  children: <Widget>[
                    titleBar,
                    searchBar,
                    list,
                    closeButton,
                  ],
                ),
              ),
            ),
          ));
        },
      )

Single dialog custom decorations

Can customize the way the search bar and field are displayed.

        items: items,
        value: selectedValueSingleDialog,
        hint: Padding(
            padding: EdgeInsets.all(3),
            child: DropdownMenuItem(
              child: Text("Select one"),
            )),
        searchHint: "Select one",
        onChanged: (value) {
          setState(() {
            selectedValueSingleDialog = value;
          });
        },
        isExpanded: true,
        searchInputDecoration: InputDecoration(
          icon: Icon(Icons.airline_seat_flat),
          border: OutlineInputBorder(),
        ),
        fieldDecoration: BoxDecoration(
          color: Colors.grey.shade200,
          shape: BoxShape.rectangle,
          borderRadius: BorderRadius.circular(5),
          border: Border.all(
            color: Colors.blueGrey,
            width: 1,
            style: BorderStyle.solid,
          ),
        ),
        selectedValueWidgetFn: (selectedValue) {
          return (Padding(
            padding: EdgeInsets.all(3),
            child: DropdownMenuItem(child: Text(selectedValue)),
          ));
        },
      )

Single dialog paged

Useful when displaying a huge amount of items.

SearchChoices.single(
        items: items,
        value: selectedValueSingleDialogPaged,
        hint: "Select one",
        searchHint: "Search one",
        onChanged: (value) {
          setState(() {
            selectedValueSingleDialogPaged = value;
          });
        },
        isExpanded: true,
        itemsPerPage: 5,
        currentPage: currentPage,
      )

Multi dialog paged rtl

Pagination also works on multiple selection and with right to left languages such as Arabic and Hebrew. Useful when displaying a huge amount of items.

SearchChoices.multiple(
        items: [
          "طنجة",
          "فاس‎",
          "أكادير‎",
          "تزنيت‎",
          "آكــلــو",
          "سيدي بيبي",
        ].map<DropdownMenuItem<String>>((string) {
          return (DropdownMenuItem<String>(
            child: Text(
              string,
              textDirection: TextDirection.rtl,
            ),
            value: string,
          ));
        }).toList(),
        selectedItems: selectedItemsMultiDialogPaged,
        hint: Row(
          textDirection: TextDirection.rtl,
          children: [
            Text(
              "ختار",
              textDirection: TextDirection.rtl,
            ),
          ],
        ),
        searchHint: Text(
          "ختار",
          textDirection: TextDirection.rtl,
        ),
        closeButton: TextButton(
          onPressed: () {
            Navigator.pop(
                MyApp.navKey.currentState?.overlay?.context ?? context);
          },
          child: SizedBox(
            width: 50,
            child: Text(
              "سدّ",
              maxLines: 1,
              softWrap: false,
              textDirection: TextDirection.rtl,
            ),
          ),
        ),
        onChanged: (value) {
          setState(() {
            selectedItemsMultiDialogPaged = value;
          });
        },
        isExpanded: true,
        rightToLeft: true,
        displayItem: (item, selected) {
          return (Row(textDirection: TextDirection.rtl, children: [
            SizedBox(width: 7),
            selected
                ? Icon(
                    Icons.radio_button_checked,
                    color: Colors.grey,
                  )
                : Icon(
                    Icons.radio_button_unchecked,
                    color: Colors.grey,
                  ),
            SizedBox(width: 7),
            item,
            Expanded(
              child: SizedBox.shrink(),
            ),
          ]));
        },
        selectedValueWidgetFn: (item) {
          return DropdownMenuItem(
            child: Row(
              textDirection: TextDirection.rtl,
              children: <Widget>[
                (Text(
                  item,
                  textDirection: TextDirection.rtl,
                )),
              ],
            ),
          );
        },
        itemsPerPage: 5,
        currentPage: currentPage,
        doneButton: "قريب",
      )

Single dialog paged custom pagination

Custom pagination can let the user choose which page to display instead of having next and previous buttons. Useful when displaying a huge amount of items.

SearchChoices.single(
        items: items,
        value: selectedValueSingleDialogPaged,
        hint: "Select one",
        searchHint: "Search one",
        onChanged: (value) {
          setState(() {
            selectedValueSingleDialogPaged = value;
          });
        },
        isExpanded: true,
        itemsPerPage: 5,
        currentPage: currentPage,
        customPaginationDisplay: (Widget listWidget, int totalFilteredItemsNb,
            Function updateSearchPage) {
          return (Expanded(
              child: Column(children: [
            listWidget,
            SingleChildScrollView(
              scrollDirection: Axis.horizontal,
              child: Row(children: [
                Text("Page:"),
                SizedBox(
                  width: 10,
                ),
                Wrap(
                  spacing: 10,
                  children:
                      Iterable<int>.generate((totalFilteredItemsNb / 5).ceil())
                          .toList()
                          .map((i) {
                    return (SizedBox(
                      width: (31 + 9 * (i + 1).toString().length) + 0.0,
                      height: 30.0,
                      child: ElevatedButton(
                        child: Text("${i + 1}"),
                        onPressed: (i + 1) == currentPage.value
                            ? null
                            : () {
                                currentPage.value = i + 1;
                                updateSearchPage();
                              },
                      ),
                    ));
                  }).toList(),
                ),
              ]),
            ),
          ])));
        },
      )

Single menu paged

Showing pagination in a single selection menu. Useful when displaying a huge amount of items.

SearchChoices.single(
        items: items,
        value: selectedValueSingleDialogPaged,
        hint: "Select one",
        searchHint: null,
        onChanged: (value) {
          setState(() {
            selectedValueSingleDialogPaged = value;
          });
        },
        dialogBox: false,
        isExpanded: true,
        menuConstraints: BoxConstraints.tight(Size.fromHeight(350)),
        itemsPerPage: 5,
        currentPage: currentPage,
      )

Single dialog paged future

Future/network/internet/web/API/webservice call displayed paged in a dialog. This example doesn't work on web because of the get function call, though, the plugin features should work on web. This example shows that this would work with PHP-CRUD-API.

SearchChoices.single(
        value: selectedValueSingleDialogPagedFuture,
        hint: kIsWeb ? "Example not for web" : "Select one capital",
        searchHint: "Search capitals",
        onChanged: kIsWeb
            ? null
            : (value) {
                setState(() {
                  selectedValueSingleDialogPagedFuture = value;
                });
              },
        isExpanded: true,
        itemsPerPage: 10,
        currentPage: currentPage,
        selectedValueWidgetFn: (item) {
          return (Center(
              child: Card(
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(4),
                    side: BorderSide(
                      color: Colors.grey,
                      width: 1,
                    ),
                  ),
                  margin: EdgeInsets.all(1),
                  child: Padding(
                    padding: const EdgeInsets.all(6),
                    child: Text(item["capital"]),
                  ))));
        },
        futureSearchFn: (String? keyword, String? orderBy, bool? orderAsc,
            List<Tuple2<String, String>>? filters, int? pageNb) async {
          String filtersString = "";
          int i = 1;
          filters?.forEach((element) {
            filtersString += "&filter" +
                i.toString() +
                "=" +
                element.item1 +
                "," +
                element.item2;
            i++;
          });
          Response response = await get(Uri.parse(
                  "https://searchchoices.jod.li/exampleList.php?page=${pageNb ?? 1},10${orderBy == null ? "" : "&order=" + orderBy + "," + (orderAsc ?? true ? "asc" : "desc")}${(keyword == null || keyword.isEmpty) ? "" : "&filter=capital,cs," + keyword}$filtersString"))
              .timeout(Duration(
            seconds: 10,
          ));
          if (response.statusCode != 200) {
            throw Exception("failed to get data from internet");
          }
          dynamic data = jsonDecode(response.body);
          int nbResults = data["results"];
          List<DropdownMenuItem> results = (data["records"] as List<dynamic>)
              .map<DropdownMenuItem>((item) => DropdownMenuItem(
                    value: item,
                    child: Card(
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(4),
                        side: BorderSide(
                          color: Colors.blue,
                          width: 1,
                        ),
                      ),
                      margin: EdgeInsets.all(1),
                      child: Padding(
                        padding: const EdgeInsets.all(6),
                        child: Text(
                            "${item["capital"]} - ${item["country"]} - ${item["continent"]} - pop.: ${item["population"]}"),
                      ),
                    ),
                  ))
              .toList();
          return (Tuple2<List<DropdownMenuItem>, int>(results, nbResults));
        },
        futureSearchOrderOptions: {
          "country": {
            "icon": Wrap(children: [
              Icon(Icons.flag),
              Text(
                "Country",
              )
            ]),
            "asc": true
          },
          "capital": {
            "icon":
                Wrap(children: [Icon(Icons.location_city), Text("Capital")]),
            "asc": true
          },
          "continent": {"icon": "Continent", "asc": true},
          "population": {
            "icon": Wrap(children: [Icon(Icons.people), Text("Population")]),
            "asc": false
          },
        },
        futureSearchFilterOptions: {
          "continent": {
            "icon": Text("Continent"),
            "exclusive": true,
            "values": [
              {"eq,Africa": "Africa"},
              {"eq,Americas": "Americas"},
              {"eq,Asia": "Asia"},
              {"eq,Australia": "Australia"},
              {"eq,Europe": "Europe"},
              {"eq,Oceania": "Oceania"}
            ]
          },
          "population": {
            "icon": Wrap(children: [Icon(Icons.people), Text("Population")]),
            "exclusive": true,
            "values": [
              {
                "lt,1000": Wrap(children: [Icon(Icons.person), Text("<1,000")])
              },
              {
                "lt,100000":
                    Wrap(children: [Icon(Icons.person_add), Text("<100,000")])
              },
              {
                "lt,1000000": Wrap(
                    children: [Icon(Icons.nature_people), Text("<1,000,000")])
              },
              {
                "gt,1000000":
                    Wrap(children: [Icon(Icons.people), Text(">1,000,000")])
              },
              {
                "gt,10000000": Wrap(
                    children: [Icon(Icons.location_city), Text(">10,000,000")])
              },
            ]
          },
        },
        closeButton: (selectedItemsDone, doneContext) {
          return Row(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              SizedBox(
                height: 25,
                width: 48,
                child: (ElevatedButton(
                    onPressed: () {
                      Navigator.pop(doneContext);
                      setState(() {});
                    },
                    child: Icon(
                      Icons.close,
                      size: 17,
                    ))),
              ),
            ],
          );
        },
      )

Multi menu paged future

Future/network/internet/web/API/webservice call displayed paged in a menu. This example doesn't work on web because of the get function call, though, the plugin features should work on web. This example shows that this would work with PHP-CRUD-API.

SearchChoices.multiple(
        futureSelectedValues: selectedItemsMultiMenuPagedFuture,
        hint: kIsWeb ? "Example not for web" : "Select capitals",
        searchHint: "",
        dialogBox: false,
        onChanged: kIsWeb
            ? null
            : (value) {
                setState(() {
                  selectedItemsMultiMenuPagedFuture = value;
                });
              },
        isExpanded: true,
        itemsPerPage: 10,
        currentPage: currentPage,
        selectedValueWidgetFn: (item) {
          return (Center(
              child: Card(
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(4),
                    side: BorderSide(
                      color: Colors.grey,
                      width: 1,
                    ),
                  ),
                  margin: EdgeInsets.all(1),
                  child: Padding(
                    padding: const EdgeInsets.all(6),
                    child: Text(item["capital"]),
                  ))));
        },
        futureSearchFn: (String? keyword, String? orderBy, bool? orderAsc,
            List<Tuple2<String, String>>? filters, int? pageNb) async {
          String filtersString = "";
          int i = 1;
          filters?.forEach((element) {
            filtersString += "&filter" +
                i.toString() +
                "=" +
                element.item1 +
                "," +
                element.item2;
            i++;
          });
          Response response = await get(Uri.parse(
                  "https://searchchoices.jod.li/exampleList.php?page=${pageNb ?? 1},10${orderBy == null ? "" : "&order=" + orderBy + "," + (orderAsc ?? true ? "asc" : "desc")}${(keyword == null || keyword.isEmpty) ? "" : "&filter=capital,cs," + keyword}$filtersString"))
              .timeout(Duration(
            seconds: 10,
          ));
          if (response.statusCode != 200) {
            throw Exception("failed to get data from internet");
          }
          dynamic data = jsonDecode(response.body);
          int nbResults = data["results"];
          List<DropdownMenuItem> results = (data["records"] as List<dynamic>)
              .map<DropdownMenuItem>((item) => DropdownMenuItem(
                    value: item,
                    child: Card(
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(4),
                        side: BorderSide(
                          color: Colors.blue,
                          width: 1,
                        ),
                      ),
                      margin: EdgeInsets.all(1),
                      child: Padding(
                        padding: const EdgeInsets.all(6),
                        child: Text(
                            "${item["capital"]} - ${item["country"]} - ${item["continent"]} - pop.: ${item["population"]}"),
                      ),
                    ),
                  ))
              .toList();
          return (Tuple2<List<DropdownMenuItem>, int>(results, nbResults));
        },
        futureSearchOrderOptions: {
          "country": {
            "icon": Wrap(children: [
              Icon(Icons.flag),
              Text(
                "Country",
              )
            ]),
            "asc": true
          },
          "capital": {
            "icon":
                Wrap(children: [Icon(Icons.location_city), Text("Capital")]),
            "asc": true
          },
          "continent": {"icon": "Continent", "asc": true},
          "population": {
            "icon": Wrap(children: [Icon(Icons.people), Text("Population")]),
            "asc": false
          },
        },
        futureSearchFilterOptions: {
          "continent": {
            "icon": Text("Continent"),
            "exclusive": true,
            "values": [
              {"eq,Africa": "Africa"},
              {"eq,Americas": "Americas"},
              {"eq,Asia": "Asia"},
              {"eq,Australia": "Australia"},
              {"eq,Europe": "Europe"},
              {"eq,Oceania": "Oceania"}
            ]
          },
          "population": {
            "icon": Wrap(children: [Icon(Icons.people), Text("Population")]),
            "exclusive": true,
            "values": [
              {
                "lt,1000": Wrap(children: [Icon(Icons.person), Text("<1,000")])
              },
              {
                "lt,100000":
                    Wrap(children: [Icon(Icons.person_add), Text("<100,000")])
              },
              {
                "lt,1000000": Wrap(
                    children: [Icon(Icons.nature_people), Text("<1,000,000")])
              },
              {
                "gt,1000000":
                    Wrap(children: [Icon(Icons.people), Text(">1,000,000")])
              },
              {
                "gt,10000000": Wrap(
                    children: [Icon(Icons.location_city), Text(">10,000,000")])
              },
            ]
          },
        },
        menuConstraints: BoxConstraints.tight(Size.fromHeight(350)),
      )

Single dialog custom empty list

When the list gets empty after the keyword is typed, the dialog displays the given text with the keyword as a parameter.

SearchChoices.single(
        items: items,
        value: selectedValueSingleDialog,
        hint: "Select one",
        searchHint: "Select one",
        onChanged: (value) {
          setState(() {
            selectedValueSingleDialog = value;
          });
        },
        isExpanded: true,
        emptyListWidget: (String keyword) =>
            "No result with the \"$keyword\" keyword",
      )

Single dialog future custom empty list

When the future/network search keyword filters out all the results, the given emptyListWidget is displayed.

SearchChoices.single(
        value: selectedValueSingleDialogFuture,
        hint: kIsWeb ? "Example not for web" : "Select one capital",
        searchHint: "Search capitals",
        onChanged: kIsWeb
            ? null
            : (value) {
                setState(() {
                  selectedValueSingleDialogFuture = value;
                });
              },
        isExpanded: true,
        selectedValueWidgetFn: (item) {
          return (Center(
              child: Card(
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(4),
                    side: BorderSide(
                      color: Colors.grey,
                      width: 1,
                    ),
                  ),
                  margin: EdgeInsets.all(1),
                  child: Padding(
                    padding: const EdgeInsets.all(6),
                    child: Text(item["capital"]),
                  ))));
        },
        futureSearchFn: (String? keyword, String? orderBy, bool? orderAsc,
            List<Tuple2<String, String>>? filters, int? pageNb) async {
          String filtersString = "";
          int i = 1;
          filters?.forEach((element) {
            // This example doesn't have any futureSearchFilterOptions parameter, thus, this loop will never run anything.
            filtersString += "&filter" +
                i.toString() +
                "=" +
                element.item1 +
                "," +
                element.item2;
            i++;
          });
          Response response = await get(Uri.parse(
                  "https://searchchoices.jod.li/exampleList.php?page=${pageNb ?? 1},10${orderBy == null ? "" : "&order=" + orderBy + "," + (orderAsc ?? true ? "asc" : "desc")}${(keyword == null || keyword.isEmpty) ? "" : "&filter=capital,cs," + keyword}$filtersString"))
              .timeout(Duration(
            seconds: 10,
          ));
          if (response.statusCode != 200) {
            throw Exception("failed to get data from internet");
          }
          dynamic data = jsonDecode(response.body);
          int nbResults = data["results"];
          List<DropdownMenuItem> results = (data["records"] as List<dynamic>)
              .map<DropdownMenuItem>((item) => DropdownMenuItem(
                    value: item,
                    child: Card(
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(4),
                        side: BorderSide(
                          color: Colors.blue,
                          width: 1,
                        ),
                      ),
                      margin: EdgeInsets.all(1),
                      child: Padding(
                        padding: const EdgeInsets.all(6),
                        child: Text(
                            "${item["capital"]} - ${item["country"]} - ${item["continent"]} - pop.: ${item["population"]}"),
                      ),
                    ),
                  ))
              .toList();
          return (Tuple2<List<DropdownMenuItem>, int>(results, nbResults));
        },
        emptyListWidget: () => Text(
          "No result",
          style: TextStyle(
            fontStyle: FontStyle.italic,
            color: Colors.grey,
          ),
        ),
      )

Single dialog onTap

onTap parameter can be used to clear the selected value or to unfocus as shown in #26

SearchChoices.single(
        items: items,
        value: selectedValueSingleDialog,
        hint: "Select one",
        searchHint: "Select one",
        onChanged: (value) {
          setState(() {
            selectedValueSingleDialog = value;
          });
        },
        isExpanded: true,
        onTap: () {
          setState(() {
            selectedValueSingleDialog = null;
          });
        },
      )

Multi dialog paged future

Multiple choice dialog box search with pagination, with future items coming from the API and with filter and sort options.

SearchChoices.multiple(
        futureSelectedValues: selectedItemsMultiDialogPagedFuture,
        hint: kIsWeb ? "Example not for web" : "Select capitals",
        searchHint: "",
        dialogBox: true,
        onChanged: kIsWeb
            ? null
            : (value) {
          setState(() {
            selectedItemsMultiDialogPagedFuture = value;
          });
        },
        isExpanded: true,
        itemsPerPage: 10,
        currentPage: currentPage,
        selectedValueWidgetFn: (item) {
          return (Center(
              child: Card(
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(4),
                    side: BorderSide(
                      color: Colors.grey,
                      width: 1,
                    ),
                  ),
                  margin: EdgeInsets.all(1),
                  child: Padding(
                    padding: const EdgeInsets.all(6),
                    child: Text(item["capital"]),
                  ))));
        },
        futureSearchFn: (String? keyword, String? orderBy, bool? orderAsc,
            List<Tuple2<String, String>>? filters, int? pageNb) async {
          String filtersString = "";
          int i = 1;
          filters?.forEach((element) {
            filtersString += "&filter" +
                i.toString() +
                "=" +
                element.item1 +
                "," +
                element.item2;
            i++;
          });
          Response response = await get(Uri.parse(
              "https://searchchoices.jod.li/exampleList.php?page=${pageNb ??
                  1},10${orderBy == null ? "" : "&order=" + orderBy + "," +
                  (orderAsc ?? true ? "asc" : "desc")}${(keyword == null ||
                  keyword.isEmpty) ? "" : "&filter=capital,cs," +
                  keyword}$filtersString"))
              .timeout(Duration(
            seconds: 10,
          ));
          if (response.statusCode != 200) {
            throw Exception("failed to get data from internet");
          }
          dynamic data = jsonDecode(response.body);
          int nbResults = data["results"];
          List<DropdownMenuItem> results = (data["records"] as List<dynamic>)
              .map<DropdownMenuItem>((item) =>
              DropdownMenuItem(
                value: item,
                child: Card(
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(4),
                    side: BorderSide(
                      color: Colors.blue,
                      width: 1,
                    ),
                  ),
                  margin: EdgeInsets.all(10),
                  child: Padding(
                    padding: const EdgeInsets.all(6),
                    child: Text(
                        "${item["capital"]} - ${item["country"]} - ${item["continent"]} - pop.: ${item["population"]}"),
                  ),
                ),
              ))
              .toList();
          return (Tuple2<List<DropdownMenuItem>, int>(results, nbResults));
        },
        futureSearchOrderOptions: {
          "country": {
            "icon": Wrap(children: [
              Icon(Icons.flag),
              Text(
                "Country",
              )
            ]),
            "asc": true
          },
          "capital": {
            "icon":
            Wrap(children: [Icon(Icons.location_city), Text("Capital")]),
            "asc": true
          },
          "continent": {"icon": "Continent", "asc": true},
          "population": {
            "icon": Wrap(children: [Icon(Icons.people), Text("Population")]),
            "asc": false
          },
        },
        futureSearchFilterOptions: {
          "continent": {
            "icon": Text("Continent"),
            "exclusive": true,
            "values": [
              {"eq,Africa": "Africa"},
              {"eq,Americas": "Americas"},
              {"eq,Asia": "Asia"},
              {"eq,Australia": "Australia"},
              {"eq,Europe": "Europe"},
              {"eq,Oceania": "Oceania"}
            ]
          },
          "population": {
            "icon": Wrap(children: [Icon(Icons.people), Text("Population")]),
            "exclusive": true,
            "values": [
              {
                "lt,1000": Wrap(children: [Icon(Icons.person), Text("<1,000")])
              },
              {
                "lt,100000":
                Wrap(children: [Icon(Icons.person_add), Text("<100,000")])
              },
              {
                "lt,1000000": Wrap(
                    children: [Icon(Icons.nature_people), Text("<1,000,000")])
              },
              {
                "gt,1000000":
                Wrap(children: [Icon(Icons.people), Text(">1,000,000")])
              },
              {
                "gt,10000000": Wrap(
                    children: [Icon(Icons.location_city), Text(">10,000,000")])
              },
            ]
          },
        },
      )

Single dialog future custom error button

Single choice dialog box search with intentional future search error to show an example of futureSearchRetryButton usa.

SearchChoices.single(
        value: selectedValueSingleDialogFuture,
        hint: kIsWeb ? "Example not for web" : "Select one capital",
        searchHint: "Search capitals",
        onChanged: kIsWeb
            ? null
            : (value) {
          setState(() {
            selectedValueSingleDialogFuture = value;
          });
        },
        isExpanded: true,
        selectedValueWidgetFn: (item) {
          return (Center(
              child: Card(
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(4),
                    side: BorderSide(
                      color: Colors.grey,
                      width: 1,
                    ),
                  ),
                  margin: EdgeInsets.all(1),
                  child: Padding(
                    padding: const EdgeInsets.all(6),
                    child: Text(item["capital"]),
                  ))));
        },
        futureSearchFn: (String? keyword, String? orderBy, bool? orderAsc,
            List<Tuple2<String, String>>? filters, int? pageNb) async {
          String filtersString = "";
          int i = 1;
          filters?.forEach((element) {
            // This example doesn't have any futureSearchFilterOptions parameter, thus, this loop will never run anything.
            filtersString += "&filter" +
                i.toString() +
                "=" +
                element.item1 +
                "," +
                element.item2;
            i++;
          });
          Response response = await get(Uri.parse(
              "https://FAULTYsearchchoices.jod.li/exampleList.php?page=${pageNb ?? 1},10${orderBy == null ? "" : "&order=" + orderBy + "," + (orderAsc ?? true ? "asc" : "desc")}${(keyword == null || keyword.isEmpty) ? "" : "&filter=capital,cs," + keyword}$filtersString"))
              .timeout(Duration(
            seconds: 10,
          ));
          if (response.statusCode != 200) {
            throw Exception("failed to get data from internet");
          }
          dynamic data = jsonDecode(response.body);
          int nbResults = data["results"];
          List<DropdownMenuItem> results = (data["records"] as List<dynamic>)
              .map<DropdownMenuItem>((item) => DropdownMenuItem(
            value: item,
            child: Card(
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(4),
                side: BorderSide(
                  color: Colors.blue,
                  width: 1,
                ),
              ),
              margin: EdgeInsets.all(10),
              child: Padding(
                padding: const EdgeInsets.all(6),
                child: Text(
                    "${item["capital"]} - ${item["country"]} - ${item["continent"]} - pop.: ${item["population"]}"),
              ),
            ),
          ))
              .toList();
          return (Tuple2<List<DropdownMenuItem>, int>(results, nbResults));
        },
        futureSearchRetryButton: (Function onPressed) => Column(children: [
          SizedBox(height: 15),
          Center(
            child: ElevatedButton.icon(
                onPressed: (){onPressed();},
                icon: Icon(Icons.repeat),
                label: Text("Intentional error - retry")),
          )
        ]),
      )

Single dialog paged delayed

Delayed search lets the user some time before calling the search function and work for Future and non-Future cases.

SearchChoices.single(
            items: items,
            value: selectedValueSingleDialogPaged,
            hint: "Select one",
            searchHint: "Search one",
            onChanged: (value) {
              setState(() {
                selectedValueSingleDialogPaged = value;
              });
            },
            isExpanded: true,
            itemsPerPage: 5,
            currentPage: currentPage,
            searchDelay: 500,
          )

Single dialog paged future delayed

Delayed search lets the user some time before calling the search function and work for Future and non-Future cases.

SearchChoices.single(
            value: selectedValueSingleDialogPagedFuture,
            hint: kIsWeb ? "Example not for web" : "Select one capital",
            searchHint: "Search capitals",
            onChanged: kIsWeb
                ? null
                : (value) {
                    setState(() {
                      selectedValueSingleDialogPagedFuture = value;
                    });
                  },
            isExpanded: true,
            itemsPerPage: 10,
            currentPage: currentPage,
            selectedValueWidgetFn: (item) {
              return (Center(
                  child: Card(
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(4),
                        side: BorderSide(
                          color: Colors.grey,
                          width: 1,
                        ),
                      ),
                      margin: EdgeInsets.all(1),
                      child: Padding(
                        padding: const EdgeInsets.all(6),
                        child: Text(item["capital"]),
                      ))));
            },
            futureSearchFn: (String? keyword, String? orderBy, bool? orderAsc,
                List<Tuple2<String, String>>? filters, int? pageNb) async {
              print("searching for ${keyword ?? ""}");
              String filtersString = "";
              int i = 1;
              filters?.forEach((element) {
                filtersString += "&filter" +
                    i.toString() +
                    "=" +
                    element.item1 +
                    "," +
                    element.item2;
                i++;
              });
              Response response = await get(Uri.parse(
                      "https://searchchoices.jod.li/exampleList.php?page=${pageNb ?? 1},10${orderBy == null ? "" : "&order=" + orderBy + "," + (orderAsc ?? true ? "asc" : "desc")}${(keyword == null || keyword.isEmpty) ? "" : "&filter=capital,cs," + keyword}$filtersString"))
                  .timeout(Duration(
                seconds: 10,
              ));
              if (response.statusCode != 200) {
                throw Exception("failed to get data from internet");
              }
              dynamic data = jsonDecode(response.body);
              int nbResults = data["results"];
              List<DropdownMenuItem> results = (data["records"] as List<dynamic>)
                  .map<DropdownMenuItem>((item) => DropdownMenuItem(
                        value: item,
                        child: Card(
                          shape: RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(4),
                            side: BorderSide(
                              color: Colors.blue,
                              width: 1,
                            ),
                          ),
                          margin: EdgeInsets.all(10),
                          child: Padding(
                            padding: const EdgeInsets.all(6),
                            child: Text(
                                "${item["capital"]} - ${item["country"]} - ${item["continent"]} - pop.: ${item["population"]}"),
                          ),
                        ),
                      ))
                  .toList();
              return (Tuple2<List<DropdownMenuItem>, int>(results, nbResults));
            },
            futureSearchOrderOptions: {
              "country": {
                "icon": Wrap(children: [
                  Icon(Icons.flag),
                  Text(
                    "Country",
                  )
                ]),
                "asc": true
              },
              "capital": {
                "icon":
                    Wrap(children: [Icon(Icons.location_city), Text("Capital")]),
                "asc": true
              },
              "continent": {"icon": "Continent", "asc": true},
              "population": {
                "icon": Wrap(children: [Icon(Icons.people), Text("Population")]),
                "asc": false
              },
            },
            futureSearchFilterOptions: {
              "continent": {
                "icon": Text("Continent"),
                "exclusive": true,
                "values": [
                  {"eq,Africa": "Africa"},
                  {"eq,Americas": "Americas"},
                  {"eq,Asia": "Asia"},
                  {"eq,Australia": "Australia"},
                  {"eq,Europe": "Europe"},
                  {"eq,Oceania": "Oceania"}
                ]
              },
              "population": {
                "icon": Wrap(children: [Icon(Icons.people), Text("Population")]),
                "exclusive": true,
                "values": [
                  {
                    "lt,1000": Wrap(children: [Icon(Icons.person), Text("<1,000")])
                  },
                  {
                    "lt,100000":
                        Wrap(children: [Icon(Icons.person_add), Text("<100,000")])
                  },
                  {
                    "lt,1000000": Wrap(
                        children: [Icon(Icons.nature_people), Text("<1,000,000")])
                  },
                  {
                    "gt,1000000":
                        Wrap(children: [Icon(Icons.people), Text(">1,000,000")])
                  },
                  {
                    "gt,10000000": Wrap(
                        children: [Icon(Icons.location_city), Text(">10,000,000")])
                  },
                ],
              },
            },
            closeButton: (selectedItemsDone, doneContext) {
              return Row(
                mainAxisAlignment: MainAxisAlignment.end,
                children: [
                  SizedBox(
                    height: 25,
                    width: 48,
                    child: (ElevatedButton(
                        onPressed: () {
                          Navigator.pop(doneContext);
                          setState(() {});
                        },
                        child: Icon(
                          Icons.close,
                          size: 17,
                        ))),
                  ),
                ],
              );
            },
            searchDelay: 500,
            // Here, buildFutureFilterOrOrderButton doesn't change anything.
            // This is a way to make sure this parameter still works with automated
            // integration testing.
            buildFutureFilterOrOrderButton: ({
              required BuildContext context,
              required bool filter,
              required Function onPressed,
              int? nbFilters,
              bool? orderAsc,
              String? orderBy,
            }) {
              if (filter) {
                return (SizedBox(
                  height: 25,
                  width: 48,
                  child: (ElevatedButton(
                    child: Icon(
                      nbFilters == null || nbFilters == 0
                          ? Icons.filter
                          : nbFilters == 1
                              ? Icons.filter_1
                              : nbFilters == 2
                                  ? Icons.filter_2
                                  : nbFilters == 3
                                      ? Icons.filter_3
                                      : nbFilters == 4
                                          ? Icons.filter_4
                                          : nbFilters == 5
                                              ? Icons.filter_5
                                              : nbFilters == 6
                                                  ? Icons.filter_6
                                                  : nbFilters == 7
                                                      ? Icons.filter_7
                                                      : nbFilters == 8
                                                          ? Icons.filter_8
                                                          : nbFilters == 9
                                                              ? Icons.filter_9
                                                              : Icons
                                                                  .filter_9_plus_sharp,
                      size: 17,
                    ),
                    onPressed: () {
                      onPressed();
                    },
                  )),
                ));
              }

              Widget icon = Icon(
                Icons.sort,
                size: 17,
              );

              return SizedBox(
                height: 25,
                width: orderBy == null ? 48 : 70,
                child: (orderBy == null
                    ? ElevatedButton(
                        child: icon,
                        onPressed: () {
                          onPressed();
                        },
                      )
                    : ElevatedButton.icon(
                        label: Icon(
                          orderAsc ?? true
                              ? Icons.arrow_upward
                              : Icons.arrow_downward,
                          size: 17,
                        ),
                        icon: icon,
                        onPressed: () {
                          onPressed();
                        },
                      )),
              );
            },
            // Here, searchResultDisplayFn doesn't change anything.
            // This is a way to make sure this parameter still works with automated
            // integration testing.
            searchResultDisplayFn: ({
                required List<Tuple3<int, DropdownMenuItem, bool>> itemsToDisplay,
                required ScrollController scrollController,
                required bool thumbVisibility,
                required Widget emptyListWidget,
                required void Function(int index, dynamic value, bool itemSelected)
                  itemTapped,
                required Widget Function(DropdownMenuItem item, bool isItemSelected)
                  displayItem,
            }) {
                return Expanded(
                    child: Scrollbar(
                        controller: scrollController,
                        thumbVisibility: thumbVisibility,
                        child: itemsToDisplay.length == 0
                        ? emptyListWidget
                            : ListView.builder(
                            controller: scrollController,
                            itemBuilder: (context, index) {
                                int itemIndex = itemsToDisplay[index].item1;
                                DropdownMenuItem item = itemsToDisplay[index].item2;
                                bool isItemSelected = itemsToDisplay[index].item3;
                                return InkWell(
                                    onTap: () {
                                        itemTapped(
                                            itemIndex,
                                            item.value,
                                            isItemSelected,
                                        );
                                    },
                                    child: displayItem(
                                        item,
                                        isItemSelected,
                                    ),
                                );
                            },
                            itemCount: itemsToDisplay.length,
                        ),
                    ),
                );
            },
          )

Single dialog custom field presentation

Making use of fieldPresentationFn to display the result of the selection in a custom way.

SearchChoices.single(
            items: items,
            value: selectedValueSingleDialog,
            hint: "Select one",
            searchHint: "Select one",
            onChanged: (value) {
              setState(() {
                selectedValueSingleDialog = value;
              });
            },
            isExpanded: true,
            fieldPresentationFn: (Widget fieldWidget, {bool? selectionIsValid}) {
              return Container(
                padding: const EdgeInsets.all(12.0),
                child: InputDecorator(
                  decoration: InputDecoration(
                    labelText: 'Label',
                    isDense: true,
                    filled: true,
                    fillColor: Colors.green.shade100,
                    border: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(10)),
                    ),
                  ),
                  child: fieldWidget,
                ),
              );
            },
          )

Single custom showDialogFn

Customizing the call to showDialog.

SearchChoices.single(
            items: items,
            value: selectedValueSingleDialog,
            onChanged: (value) {
              setState(() {
                selectedValueSingleDialog = value;
              });
            },
            hint: "Select one",
            isExpanded: true,
            showDialogFn: (
              BuildContext context,
              Widget Function({String searchTerms}) menuWidget,
              String searchTerms,
            ) async {
              await showDialog(
                  barrierColor: Colors.pinkAccent,
                  context: context,
                  barrierDismissible: false,
                  builder: (BuildContext dialogContext) {
                    return (menuWidget(searchTerms: searchTerms));
                  });
            },
          )

Validator in form

The form field validator is called by the parent form validator.

      Form(
        key: _formKeyForValidator,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            TextFormField(
              validator: (String? value) {
                if (value == "ok") {
                  return (null);
                }
                return ("Not the expected value");
              },
            ),
            SearchChoices.single(
              items: items,
              value: selectedValueSingleDialog,
              onChanged: (value) {
                setState(() {
                  selectedValueSingleDialog = value;
                });
              },
              isExpanded: true,
              validator: (dynamic value) {
                if (value == null) {
                  return ("Must select a value");
                }
                if (!(value is String)) {
                  return ("Selected value must be a String");
                }
                if (value.startsWith("l")) {
                  return (null);
                }
                return ("Must start with 'l'");
              },
            ),
            SearchChoices.multiple(
              items: items,
              selectedItems: selectedItemsMultiDialog,
              onChanged: (value) {
                setState(() {
                  selectedItemsMultiDialog = value;
                });
              },
              isExpanded: true,
              validator: (dynamic value) {
                if (value == null) {
                  return ("Must select some values");
                }
                if (!(value is List<int>)) {
                  return ("Selection is of unexpected type");
                }
                if (value.length < 3) {
                  return ("Must select at least 3");
                }
                return (null);
              },
            ),
            TextButton(
              onPressed: () {
                if (_formKeyForValidator.currentState?.validate() ?? false) {
                  setState(() {
                    formResult = "All good";
                  });
                } else {
                  setState(() {
                    formResult = "Form is not valid!";
                  });
                }
              },
              child: const Text("Ok"),
            ),
            formResult == null
                ? SizedBox.shrink()
                : Text(formResult ?? "",
                    style: TextStyle(
                      color:
                          formResult == "All good" ? Colors.black : Colors.red,
                    )),
          ],
        ),
      )

Feature requests/comments/questions/bugs

Feel free to log your feature requests/comments/questions/bugs here: https://github.com/lcuis/search_choices/issues

Contributions

This solution is based on improvements done on a pull request that was probably already changing too many things to the great original repository: icemanbsi#11

I would be happy to merge pull request proposals provided that:

  • they don't break the compilation
  • they pass the automated testing
  • they provide the relevant adaptations to documentation and automated testing
  • they bring value
  • they don't completely transform the code
  • they are readable (though, I enjoy https://www.ioccc.org/ as a contest full of curiosities)

Contributions and forks are very welcome!

In your pull request, feel free to add your line in the contributors section below:

Contributors

CI/CD

Continuous integration/deployment status: CI-CD

Automated integration testing was done using Flutster. Here is the latest recorded video of this automated integration testing: https://searchchoices.jod.li/integration_test.mkv

search_choices's People

Contributors

develogo avatar github-actions[bot] avatar icemanbsi avatar lcuis avatar luis-cruzt avatar macacoazul01 avatar web4exposure-org avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

search_choices's Issues

child of dropdownmenu item not shown after selecting an item

I've a widget with a search_choices widget (which is otherwise working great!) and a list of results below that (depending on the selected item from the dropdown).

I can search for and select an item from the dropdown list, but when an item has been selected, the selected item isn't shown in the search_choices widget - and I can't figure out why. TIA for your input.

(1) widget when first built
1

(2) selecting an item
2

(3) after an item has been selected
3

The widget is as follows:

class MyWidget extends StatefulWidget {
  const MyWidget({Key? key}) : super(key: key);

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

class _MyWidgetState extends State<MyWidget> {
  
  late List<DropdownMenuItem<Model>> _items;

  // changed this
  Model? _selectedItem;

  @override
  void initState() {
    // get item list
    final itemList = // ...
    _items = _buildDropdownMenuItems(itemList);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.start,
      children: <Widget>[
        Center(
          child: Card(
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(10.0),
              side: const BorderSide(
                color: Colors.grey,
                width: 1.0,
              ),
            ),
            margin: const EdgeInsets.all(10.0),
            child: Padding(
              padding: const EdgeInsets.all(20.0),
              child: SearchChoices.single(
                items: _items,
                value: _selectedItem,
                hint: 'hint'.tr(),
                searchHint: 'hint2'.tr(),
                onChanged: (Model item) {
                  setState(() {
                    _selectedItem = item;
                  });
                },
                style: const TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.normal,
                ),
                isExpanded: true,
              ),
            ),
          ),
        ),
        Consumer(
          builder: (context, watch, child) {
            // results
          },
        )
      ],
    );
  }

  List<DropdownMenuItem<Model>> _buildDropdownMenuItems(
      List<Model> itemList) {
    final listItems = <DropdownMenuItem<<Model>>>[];
    for (final item in itemList)
      listItems.add(
        DropdownMenuItem<Model>(
          child: Text(
            item.itemName ?? '',
            style: const TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.normal,
            ),
          ),
          value: item,
        ),
      );
    return listItems;
  }
}

Text field does not unfocus after opening/close drop down dialog

I created TextField and type in something, and then press SearchChoices widget and open drop down dialog.
When I closed the dialog, TextField was still in focus. May I know is there any method to unfocus other text fields when open/close SearchChoices widget?

Margin and padding override for menu/dialog content

Currently it is hardcoded:

child: Card(
        color: widget.menuBackgroundColor,
        margin: EdgeInsets.symmetric(
            vertical: widget.dialogBox ? 10 : 5,
            horizontal: widget.dialogBox ? 10 : 4),
        child: Container(
          constraints: widget.menuConstraints,
          padding: EdgeInsets.symmetric(vertical: 15, horizontal: 15),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              titleBar(),
              searchBar(),
              list(),
              closeButtonWrapper(),
            ],
          ),
        ),
      ),

Need properties: menuMargin and menuPadding to be able to override these hardcoded values.

image

Search dialog not respecting theme colours

Hi guys,
I'm using a custom colour as my primary colour, and it's set in a themes file so that Theme.of(context).primaryColor is able to access it. However it appears that the colour for buttons and UI in search_choices is hard coded to blue.

Is there a way to pass theme data through to the widget without needing to use a completely custom solution via buildDropDownDialog?

[Enhancement] Allow setting an empty state widget in the search dialog if no results meet the search criteria

Currently, if I use SearchChoices.single for example, and the items provided in the dialog list don't meet the search criteria, the list is simply empty.

I tried to check if there's a way to show a custom widget instead of that list if no results are returned, but couldn't. I might have missed it somewhere in the documentation, if so, please let me know.

Thanks a lot for this library, I couldn't find anything that works as well as it for having a searchable list from a dropdown.

Allow for disabling innerItemsWidget

After I've selected my items, I don't necessarily want them populated in innerItemsWidget. Instead, I'd rather handle those selections a different way (in a separate list below the search widget for example).

It'd be great if after selecting, the searchHint remains in the search bar and nothing is populated.

if (list.isEmpty && hintIndex != null) {
  innerItemsWidget = items[hintIndex];
} else {
  innerItemsWidget = widget.selectedAggregateWidgetFn != null
      ? widget.selectedAggregateWidgetFn!(list)
      : Column(
          children: isPopulatingSearchBar ? list : [searchHint], // Something like this perhaps
        );
}

The argument type can't be assigned to the parameter type

Hi!
I'm building a web app using flutter web and got this issue when trying to implement search_choices:

error: The argument type 'List<DropdownMenuItem> (where DropdownMenuItem is defined in /Users/ak/Library/flutter/.pub-cache/git/flutter_web-cc38319841009fcbdf632d9b03aab18d28939438/packages/flutter_web/lib/src/material/dropdown.dart)' can't be assigned to the parameter type 'List<DropdownMenuItem> (where DropdownMenuItem is defined in /Users/ak/Library/flutter/packages/flutter/lib/src/material/dropdown.dart)'. (argument_type_not_assignable at [mc_web_app] lib/RequestsPage/AddEditRequestPage.dart:236)

My flutter doctor is:

[✓] Flutter (Channel beta, 1.19.0-4.1.pre, on Mac OS X 10.15.5 19F101, locale ru-RU)
    • Flutter version 1.19.0-4.1.pre at /Users/ak/Library/flutter
    • Framework revision f994b76974 (8 days ago), 2020-06-09 15:53:13 -0700
    • Engine revision 9a28c3bcf4
    • Dart version 2.9.0 (build 2.9.0-14.1.beta)

 
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
    • Android SDK at /Users/ak/Library/Android/sdk
    • Platform android-29, build-tools 29.0.2
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6222593)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 11.5)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 11.5, Build version 11E608c
    • CocoaPods version 1.8.4

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 4.0)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin version 46.0.2
    • Dart plugin version 193.7361
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6222593)

[✓] VS Code (version 1.45.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.11.0

[✓] Connected device (2 available)
    • Web Server • web-server • web-javascript • Flutter Tools
    • Chrome     • chrome     • web-javascript • Google Chrome 83.0.4103.97

• No issues found!

My pubspec.yaml:

name: mc_web_app

version: 1.0.0+1

environment:
  sdk: ">=2.2.2 <3.0.0"

dependencies:
  protobuf: ^1.0.1
  provider: any

  service_worker: ^0.2.0
  googleapis_auth: ^0.2.3+5
  http: ^0.12.1
  intl: ^0.16.1
  search_choices: ^1.0.17

  firebase: any
  firebase_web: ^5.0.9

  flutter_web: any
  flutter_web_ui: any

dev_dependencies:
  build_runner: ^1.10.0
  build_web_compilers: ^2.11.0
  test: ^1.14.6

dependency_overrides:
  provider:
    git:
      url: https://github.com/kevmoo/provider
      ref: flutter_web

  flutter_web:
    git:
      ref: cc38319
      url: https://github.com/flutter/flutter_web
      path: packages/flutter_web

  flutter_web_ui:
    git:
      url: https://github.com/flutter/flutter_web
      path: packages/flutter_web_ui

  firebase:
    git:
      url: https://github.com/FirebaseExtended/firebase-dart

My code is also below:

import 'package:search_choices/search_choices.dart';
...

  var _housesList = List<DropdownMenuItem<dynamic>>();

...

@override
  void initState() {
    if (!widget.editRequest) _db.getHouses().then((value) {
      var list = value.map((e) => DropdownMenuItem(value: e,
        child: Text('${e.city}, ${e.street}, ${e.houseNumber}'))).toList();
      _housesList = list;
    });
    super.initState();
  }

...

RaisedButton(
  child: Text('some text'),
    onPressed: () {
      SearchChoices.single(items: _housesList, onChanged: () { /*...*/ }); // ERROE GOES HERE
    },
  )

What this issue is about? I mean that I know that this issue appears when your Widget is not compatible with flutter_web, but site said that it's compatible with web.

Ability to customize selected items container

    Widget innerItemsWidget;
    List<Widget> list = List<Widget>();
    selectedItems?.forEach((item) {
      list.add(widget.selectedValueWidgetFn != null
          ? widget.selectedValueWidgetFn(widget.items[item].value)
          : items[item]);
    });
    if (list.isEmpty && hintIndex != null) {
      innerItemsWidget = items[hintIndex];
    } else {
      innerItemsWidget = Column( // <<< it could be Wrap, Row, ... - make it customizable e.g. selectedValuesStageFn 
        children: list,
      );
    }

Get a "Unhandled Exception: This widget has been unmounted" when selecting an item

Full error:
[ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: This widget has been unmounted, so the State no longer has a context (and should be considered defunct).
Consider canceling any active work during "dispose" or using the "mounted" getter to determine if the State is still active.
#0 State.context. (package:flutter/src/widgets/framework.dart:909:9)
#1 State.context (package:flutter/src/widgets/framework.dart:915:6)
#2 _SearchChoicesState.showDialogOrMenu (package:search_choices/search_choices.dart:1196:39)

#3 _SearchChoicesState.build. (package:search_choices/search_choices.dart:1277:21)

Here's the code:

Widget selectVehicle() {

  return Column(
      children: [
        Padding(
          padding: const EdgeInsets.only(left: 0.0, right: 0.0, top: 10.0),
          child: Row(
            children: <Widget>[
              WidgetAnimator(
                Text(
                  LocaleText.of(context).vehicle,
                  textAlign: TextAlign.left,
                  style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
                ),
              ),
            ],
            mainAxisAlignment: MainAxisAlignment.start,
          ),
        ),
        DecoratedBox(
          decoration: ShapeDecoration(
            color: Colors.transparent,
            shape: RoundedRectangleBorder(
              side: BorderSide(
                  width: 3.0,
                  style: BorderStyle.solid,
                  color: Theme.of(context).textTheme.caption.color),
              borderRadius: BorderRadius.all(Radius.circular(10.0)),
            ),
          ),
          child: Padding(
            padding: const EdgeInsets.only(top: 0.0, left: 0, right: 0),
            //child: SearchableDropdown(
            child: SearchChoices.single(
              hint: Text(
                '${selectedVehicle['name'] != null ? selectedVehicle['name'] : LocaleText.of(context).unassigned}',
                style: new TextStyle(
                  fontSize: 14,
                  color: Theme.of(context).textTheme.subtitle2.color,
                ),
                overflow: TextOverflow.ellipsis,
              ),
              underline: SizedBox(),
              key: UniqueKey(),
              items: devicesList?.map((item) {
                return new DropdownMenuItem<dynamic>(
                    child: ListWidgetAnimator(
                      Text(
                        '${item['name']} ${item['brand']} ${item['model']}',
                        style: new TextStyle(
                          fontSize: 14,
                          color: Theme.of(context).textTheme.subtitle2.color,
                        ),
                        overflow: TextOverflow.ellipsis,
                      ),
                    ),
                    value: item);
              })?.toList(),
              selectedValueWidgetFn: (item) {
                return Container(
                  transform: Matrix4.translationValues(0, 0, 0),
                  alignment: Alignment.centerLeft,
                  child: Text(
                    '${item['name']} ${item['brand']} ${item['model']}',
                    style: new TextStyle(
                      fontSize: 16,
                      color: Theme.of(context).textTheme.subtitle2.color,
                    ),
                    overflow: TextOverflow.ellipsis,
                  ),
                );
              },

              menuBackgroundColor: Theme.of(context).textTheme.headline4.color,
              isExpanded: true,
              value: selectedVehicle,
              isCaseSensitiveSearch: false,
              searchHint: new Text(
                LocaleText.of(context).selectEquipmentError,
                overflow: TextOverflow.ellipsis,
                style: new TextStyle(
                  fontSize: 14,
                ),
              ),
              searchFn: (String keyword, items) {
                List<int> ret = List<int>();
                if (keyword != null && items != null && keyword.isNotEmpty) {
                  keyword.split(" ").forEach((k) {
                    int i = 0;
                    items.forEach((item) {
                      if (k.isNotEmpty &&
                          (item.value['name']
                                  .toString()
                                  .toLowerCase()
                                  .contains(k.toLowerCase()) ||
                              item.value['brand']
                                  .toString()
                                  .toLowerCase()
                                  .contains(k.toLowerCase()) ||
                              item.value['model']
                                  .toString()
                                  .toLowerCase()
                                  .contains(k.toLowerCase()) ||
                              item.value['description']
                                  .toString()
                                  .toLowerCase()
                                  .contains(k.toLowerCase()))) {
                        ret.add(i);
                      }
                      i++;
                    });
                  });
                }
                if (keyword.isEmpty) {
                  ret = Iterable<int>.generate(items.length).toList();
                }
                return (ret);
              },

              closeButton: LocaleText.of(context).cancel,
              onChanged: (value) {
               
                // Hide keyboard
                FocusScope.of(context).unfocus();

                if (value != null) {
                  selectedVehicle = value;
                  if (mounted)
                    setState(() {
                     
                    });
                }
              },
            ),
          ),
        ),
      ],
    );
}```

Hide search input

Feature request:
Boolean to show/hide search input

I'm using the search_choices widget in a long list of filters, and some items only have two or three selections within their dropdown list. It would be really neat to have a bool to toggle the search itself on or off, so that I can show something along the lines of:

displaySearch: (items.length > 3),

Center `searchHint`

Is there a way to center searchHint? I tried the snippet below but it didn't work.

searchHint: Center(child: Text("Select one")),

CloseButton doesnt work while using menuConstraints

Hello,

first I would say thank you for creating this widget, it helps a lot for me.

I want to ask after I add menuConstraints, the close button under the list doesn't work. how can I solve this problem thank you :D

How to remove paddings in the widget

Screenshot 2020-04-17 at 01 22 34

I have 3 search_choices, I noticed that if the widget has loaded list but has no data selected it is without padding(like: select province on the screenshot). If I select some item widget now has padding on the top and bottom(country dropdown on the screen.) And if dropdown doesn't have any data and is disabled it also has top and bottom padding. Is there any possibility to get rid of them and make the same height for all dropdowns?

Defined padding is only applied when no item is selected

I have an search_choices where I have passed the padding as 0, and this padding is properly applied when we have no item selected.

image

I added a yellow container surrounding the search_choices to see the size it takes up.
"Todos" is just the hint used when there is no item selected. In this case, the padding I passed works fine.

image

As soon as I select an item, the padding is not respected anymore.
image

Not able to search by items

I want to search by the item(text), not the value, because I want to pick the ID of the item.

example -

Item = Digital Marketing
ID = jdsds-dsdscsd-erereddsds

I want to search by item, and select the id

image

Add customization option for Clear button.

In case no selection is made, I need an option to not display clear button (currently it is displayed as disabled, which is quite confusing). This new option should display clear button only if there is selection.

image

Also it should be possible to specify position of clear button, before or after dropdown button:
image

Multi editable list example

There is a working example of editable list in single selection mode. The equivalent example in multiple selection mode would be needed to make sure it is possible.

Make padding adjustable.

Currently padding is hardcode, and actually it is a little bit excessive by default, i.e. padding: EdgeInsets.all(10.0),
Please make it adjustable.

image

SearchChoices.single increases container size after selecting value

Hi

I have a container that has size X before selecting a value. Then after selecting value the container increases in size. How can I keep the size the same always?
Attached is an example:
1 - Top drop-down, I have selected a value
2 - Bottom drop-down I have not yet selected.

Also, the text after selecting is always right-aligned.

they are both configured the same way

Screenshot_1592140398

iOS Compile error - 'search_choices_for_push-Swift.h' file not found

Getting the following error on building for iOS

XCode Version 11.3.1
Tried to compile example project

.../pub-cache/hosted/pub.dartlang.org/search_choices-1.0.12/ios/Classes/SearchChoicesForPushPlugin.m:8:9: fatal error: 'search_choices_for_push-Swift.h' file not found
#import "search_choices_for_push-Swift.h"
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.
note: Using new build system
note: Planning build
note: Constructing build description

flutter doctor (tested on stable and beta channels )

⇒ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel beta, v1.15.17, on Mac OS X 10.15.3 19D76, locale en-ZA)

[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 11.3.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 3.6)
[✓] IntelliJ IDEA Ultimate Edition (version 2019.3.3)
[✓] Connected device (3 available)

• No issues found!

onClear ( Closure call with mismatched arguments: function)

Hi

I can't clear the field state. Whats can be wrong?

onClear: () => setState(() => _selectedCategoryTitle = ''),

Closure call with mismatched arguments: function '_PartsBoardLayoutState.build.'
Receiver: Closure: (dynamic) => void
Tried calling: _PartsBoardLayoutState.build.(null, Instance of 'StatefulElement')
Found: _PartsBoardLayoutState.build.(dynamic) => void

Suggestion to remove duplicated validation text

Screenshot 2020-04-27 at 23 47 13

When we are adding validation, we can see the error message. And after pressing for search validation text is written 2. times.

Also, it would be great to have the possibility to validate dropdown after form submit button was pressed(I mean when we complete full form with all fields)
I know it's a well-known issue I read tickets in searchable_dropdown, but looks like it's an important issue as lots of people need this widget as part of the form.

SearchChoices - Is it possible to pre populate the search string

Hi

Is it possible to set the search string on the widget and then have it open by setting its focus?

Example:

I have a widget with an image that contained a string value. see image 1

When the user taps the image I want to automatically set the search string in SearchChoices and have the dialog open and the lookups filtered. see image 2
image1
image2

Horizontal result display

Is there any method to display the selected items horizontally?
selectedValueWidgetFn always return vertical items display.

bug: future search dialog box iOS: filter doesn't change list

Examples of future search with a dialog box on iOS are affected by a bug.
Selection of a filter doesn't refresh the list.
Selection of an order refreshes the list and takes the previously selected filter into consideration.
All works fine in a menu, not in a dialog box.

Select all doesnt work and Is this library still maintained?

Is this library still maintained?

My code:
List bairros = List();
List bairrosEscolhidos = List();
List bairrosEscolhidosInt = List();

SearchChoices.multiple(
doneButton: (selectedItemsDone, doneContext) {
return (RaisedButton(
color: temaPadrao.primaryColor,
onPressed: () {
Navigator.pop(doneContext);
},
child: Text(
'Salvar',
style: TextStyle(color: Colors.white),
)));
},
items:
bairros.map<DropdownMenuItem>((String value) {
return DropdownMenuItem(
value: value,
child: Text(value),
);
}).toList(),
selectedItems: bairrosEscolhidosInt,
selectedValueWidgetFn: (item) {
return (Center(
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
side: BorderSide(
color: temaPadrao.primaryColor,
width: size.paddingPequeno / 4,
),
),
margin: EdgeInsets.all(size.paddingPequeno),
child: Padding(
padding: EdgeInsets.all(size.paddingPequeno),
child: Text(item.toString()),
))));
},
hint: Padding(
padding: EdgeInsets.all(size.paddingMedio),
child: Text('Selecione os Bairros e Distritos'),
),
style: TextStyle(fontSize: size.textPequeno),
searchHint: Text(
'Selecione os Bairros e Distritos',
style: TextStyle(fontSize: size.textPequeno),
),
onChanged: (value) {
setState(() {
bairrosEscolhidosInt.clear();
bairrosEscolhidos.clear();
bairrosEscolhidosInt = value;
for (var x in value) {
bairrosEscolhidos.add(cidades[x]);
}
});
},
closeButton: (selectedItemsClose) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
RaisedButton(
onPressed: () {
setState(() {
bairrosEscolhidos.clear();
bairrosEscolhidosInt.clear();
bairrosEscolhidosInt.addAll(
Iterable.generate(bairros.length)
.toList());
});
},
child: Text("Selecionar Todos")),
RaisedButton(
onPressed: () {
setState(() {
bairrosEscolhidos.clear();
bairrosEscolhidosInt.clear();
});
},
child: Text("Limpar Seleção")),
],
);
},
isExpanded: true,
),

onTap function doesn't work

items: dropdownElemnts?.data?.sectors!.map((Sector value) { return DropdownMenuItem<String>( onTap: () { print('clicked'); }, value: value.id.toString(), child: Text(value.name!), ); }).toList(),

Actually i have two issues the first one ,the search feature doesn't work when the value and child pramters take different values as shown..with the below code search feature will work
value: value.name, child: Text(value.name!),

the second issue onTap function doesn't work at all

Can SearchChoices handle a list of a few thousand items?

I'm building a client application for the Mastodon platform and users are supposed to be able to select any registered instance to log into. What I'm currently doing to achieve this is calling https://instances.social/api/doc/#api-Instances-ListInstances, storing all the instances in a list, and giving that to SearchChoices.single. But that response is paginated in the order of a few thousand pages of instances, and I'm wondering if SearchChoices can handle a list made of all the instances in those few thousand pages - right now I'm only passing the first page. Would this be a problem?

Needed possibility to configure dialog size and even dialog itself.

Currently there is a property: menuConstraints, but it is ignored in case of dialogBox = true
As result on large screens dialog takes almost all available space, which looks bad.
Please add property dialogConstraints to be able to limit dialog size.
Also it would be absolutely perfect if option for overriding showDialog() call is added, like:

OnShowDialog onShowDialog;

typedef OnShowDialog  = Future Function(BuildContext context, Widget menuWidget);

That way it will be possible to configure showDialog() exactly the way it is needed for my application, even replacing standard dialog to custom one.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.