search_choices 1.0.17

  • Readme
  • Changelog
  • Example
  • Installing
  • 92

search_choices #

Widget to let the user search through a keyword string typed on a customizable keyboard in a single or multiple choices list presented as a dropdown in a dialog box or a menu.

Platforms #

This widget has been successfully tested on iOS, Android and Chrome.

Examples #

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

See code below.

Example nameDemonstration
Single dialogSingle dialog
Multi dialogMulti dialog
Single done button
dialog
Single done button dialog
Multi custom display
dialog
Multi custom display dialog
Multi select 3 dialogMulti select 3 dialog
Single menuSingle menu
Multi menuMulti 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 objectSingle dialog object
Single dialog overflowSingle dialog overflow
Single dialog readOnlySingle dialog readOnly
Single dialog disabledSingle dialog disabled
Single dialog
editable items
Single dialog editable items
Multi dialog
editable items
Multi dialog editable items
Single dialog dark
mode
Single dialog dark mode
Single dialog ellipsisSingle dialog ellipsis
Single dialog right
to left
Single dialog right to left

Demonstration #

An Android demonstration is available here: https://searchchoices.jod.li/app-release.apk

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,
    @required 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,
    Function selectedAggregateWidgetFn,
}
)
  • 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
  • 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 false.
  • 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.

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.

SearchChoices<T>.multiple(
{
    Key key,
    @required List<DropdownMenuItem<T>> items,
    @required 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,
    Function selectedAggregateWidgetFn,
}
)
  • 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
  • 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 false.
  • 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.

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 (RaisedButton(
        onPressed: () {
          Navigator.pop(doneContext);
          setState(() {});
        },
        child: Text("Save")));
  },
  closeButton: null,
  style: TextStyle(fontStyle: FontStyle.italic),
  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
                  .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,
  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 (RaisedButton(
          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>[
          RaisedButton(
              onPressed: () {
                setState(() {
                  selectedItemsClose.clear();
                  selectedItemsClose.addAll(
                      Iterable<int>.generate(items.length).toList());
                });
                updateParent(selectedItemsClose);
              },
              child: Text("Select all")),
          RaisedButton(
              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>[
          RaisedButton(
              onPressed: () {
                setState(() {
                  selectedItemsClose.clear();
                  selectedItemsClose.addAll(
                      Iterable<int>.generate(items.length).toList());
                });
                updateParent(selectedItemsClose);
              },
              child: Text("Select all")),
          RaisedButton(
              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. One can limit the number of items that can be added (100 here).

    input = TextFormField(
      validator: (value) {
        return (value.length < 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,
      builder: (BuildContext alertContext) {
        return (AlertDialog(
          title: Text("Add an item"),
          content: Form(
            key: _formKey,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                input,
                FlatButton(
                  onPressed: () {
                    if (_formKey.currentState.validate()) {
                      setState(() {
                        editableItems.add(DropdownMenuItem(
                          child: Text(inputString),
                          value: inputString,
                        ));
                      });
                      Navigator.pop(alertContext, inputString);
                    }
                  },
                  child: Text("Ok"),
                ),
                FlatButton(
                  onPressed: () {
                    Navigator.pop(alertContext, null);
                  },
                  child: Text("Cancel"),
                ),
              ],
            ),
          ),
        ));
      },
    );
  }
...
SearchChoices.single(
    items: editableItems,
    value: selectedValueSingleDialogEditableItems,
    hint: "Select one",
    searchHint: "Select one",
    disabledHint: (Function updateParent) {
      return (FlatButton(
        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"
          : FlatButton(
              onPressed: () {
                addItemDialog().then((value) async {
                  if (value != null &&
                      editableItems.indexWhere(
                              (element) => element.value == value) !=
                          -1) {
                    Navigator.pop(
                        MyApp.navKey.currentState.overlay.context);
                    updateParent(value);
                  }
                });
              },
              child: Text("Add and select item"),
            ));
    },
    onChanged: (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",
)

Multi dialog editable items

Same example as previously but with multiple selection.

    input = TextFormField(
      validator: (value) {
        return (value.length < 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,
      builder: (BuildContext alertContext) {
        return (AlertDialog(
          title: Text("Add an item"),
          content: Form(
            key: _formKey,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                input,
                FlatButton(
                  onPressed: () {
                    if (_formKey.currentState.validate()) {
                      setState(() {
                        editableItems.add(DropdownMenuItem(
                          child: Text(inputString),
                          value: inputString,
                        ));
                      });
                      Navigator.pop(alertContext, inputString);
                    }
                  },
                  child: Text("Ok"),
                ),
                FlatButton(
                  onPressed: () {
                    Navigator.pop(alertContext, null);
                  },
                  child: Text("Cancel"),
                ),
              ],
            ),
          ),
        ));
      },
    );
  }
...
SearchChoices.multiple(
    items: editableItems,
    selectedItems: editableSelectedItems,
    hint: "Select any",
    searchHint: "Select any",
    disabledHint: (Function updateParent) {
      return (FlatButton(
        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"
          : FlatButton(
              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);
                      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: FlatButton(
        onPressed: () {
          Navigator.pop(MyApp.navKey.currentState.overlay.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 (Text(
        item,
        overflow: TextOverflow.ellipsis,
      ));
    },
    dialogBox: true,
    isExpanded: true,
)

Single dialog right to left #

In support for Arabic and Hebrew languages.

SearchChoices.single(
    items: ["طنجة", "فاس‎", "أكادير‎", "تزنيت‎", "آكــلــو", "سيدي بيبي"]
        .map<DropdownMenuItem<String>>((string) {
      return (DropdownMenuItem<String>(
        child: Text(
          string,
          textDirection: TextDirection.rtl,
        ),
        value: string,
      ));
    }).toList(),
    value: selectedValueSingleDialogRightToLeft,
    hint: Text(
      "ختار",
      textDirection: TextDirection.rtl,
    ),
    searchHint: Text(
      "ختار",
      textDirection: TextDirection.rtl,
    ),
    closeButton: FlatButton(
      onPressed: () {
        Navigator.pop(MyApp.navKey.currentState.overlay.context);
      },
      child: Text(
        "سدّ",
        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 Row(
        textDirection: TextDirection.rtl,
        children: <Widget>[
          (Text(
            item,
            textDirection: TextDirection.rtl,
          )),
        ],
      );
    },
)

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: https://github.com/icemanbsi/searchable_dropdown/pull/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

1.0.17 #

1.0.16 #

1.0.15 #

1.0.14 #

  • Surrounded the DropdownDialog with a StatefulBuilder to allow the refresh of the display when there is a call to updateParent through a setState.

1.0.13 #

  • surrounded a setState by an if statement to check that mounted is set to true
  • corrected issue 8: ios compile error: search_choices_for_push-Swift.h file not found. Thanks @jaspervdbijl !

1.0.12 #

  • improvement of the selection update logic
  • consecutive adaptation of the examples

1.0.11 #

1.0.10 #

1.0.9 #

  • adapted dropdown list display to support multiple selection editable list
  • added an example of multiple selection editable list

1.0.8 #

  • added rightToLeft parameter to README

1.0.7 #

  • made it possible to make a dark mode without changing the theme with an example
  • added an example for ellipsis text overflow handling
  • made it possible to support right to left languages with an example

1.0.6 #

  • selectable items can be added and removed by the user

1.0.5 #

  • added demonstration for disabled and readOnly
  • test formatted
  • added readOnly and menuBackgroundColor parameters
  • clear icon disabled when Widget is disabled

1.0.4 #

  • minor changes to CI/CD
  • git cleanups

1.0.3 #

  • minor improvements to README

1.0.2 #

  • minor improvements to README

1.0.1 #

  • CI/CD improved automated deployment

0.0.1 #

Improvements on a pull request that was probably changing too many things to be accepted of the great icemanbsi's searchable_dropdown: https://github.com/icemanbsi/searchable_dropdown/pull/11 Part of the pull request I proposed:

  • keyboardType
  • validator
  • label
  • searchFn
  • multipleSelection

New with this repository:

  • demo app with a gallery as tabs or as a single page (change through a switch)
  • automated testing
  • continuous integration (CI)
  • continuous deployment (CD)
  • license is MIT
  • split constructors between single and multiple selection
  • comments on the constructors
  • selection can be done in a menu, not just a dialog box
  • solution to allow flexibility to provide a String, a Widget or a Function that returns either one of String or Widget for several components of the Widget
  • documentation
  • keyboard doesn't overlap dialog

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'package:search_choices/search_choices.dart';

class ExampleNumber {
  int number;

  static final Map<int, String> map = {
    0: "zero",
    1: "one",
    2: "two",
    3: "three",
    4: "four",
    5: "five",
    6: "six",
    7: "seven",
    8: "eight",
    9: "nine",
    10: "ten",
    11: "eleven",
    12: "twelve",
    13: "thirteen",
    14: "fourteen",
    15: "fifteen",
  };

  String get numberString {
    return (map.containsKey(number) ? map[number] : "unknown");
  }

  ExampleNumber(this.number);

  String toString() {
    return ("$number $numberString");
  }

  static List<ExampleNumber> get list {
    return (map.keys.map((num) {
      return (ExampleNumber(num));
    })).toList();
  }
}

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  static final navKey = new GlobalKey<NavigatorState>();
  const MyApp({Key navKey}) : super(key: navKey);
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  bool asTabs = false;
  String selectedValueSingleDialog;
  String selectedValueSingleDoneButtonDialog;
  String selectedValueSingleMenu;
  String selectedValueSingleDialogCustomKeyboard;
  String selectedValueSingleDialogOverflow;
  String selectedValueSingleDialogEditableItems;
  String selectedValueSingleDialogDarkMode;
  String selectedValueSingleDialogEllipsis;
  String selectedValueSingleDialogRightToLeft;
  String selectedValueUpdateFromOutsideThePlugin;
  ExampleNumber selectedNumber;
  List<int> selectedItemsMultiDialog = [];
  List<int> selectedItemsMultiCustomDisplayDialog = [];
  List<int> selectedItemsMultiSelect3Dialog = [];
  List<int> selectedItemsMultiMenu = [];
  List<int> selectedItemsMultiMenuSelectAllNone = [];
  List<int> selectedItemsMultiDialogSelectAllNoneWoClear = [];
  List<int> editableSelectedItems = [];
  List<DropdownMenuItem> items = [];
  List<DropdownMenuItem> editableItems = [];
  final _formKey = GlobalKey<FormState>();
  String inputString = "";
  TextFormField input;
  List<DropdownMenuItem<ExampleNumber>> numberItems =
      ExampleNumber.list.map((exNum) {
    return (DropdownMenuItem(child: Text(exNum.numberString), value: exNum));
  }).toList();
  List<int> selectedItemsMultiSelect3Menu = [];
  List<int> selectedItemsMultiDialogWithCountAndWrap = [];

  static const String appTitle = "Search Choices demo";
  final String loremIpsum =
      "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";

  @override
  void initState() {
    String wordPair = "";
    loremIpsum
        .toLowerCase()
        .replaceAll(",", "")
        .replaceAll(".", "")
        .split(" ")
        .forEach((word) {
      if (wordPair.isEmpty) {
        wordPair = word + " ";
      } else {
        wordPair += word;
        if (items.indexWhere((item) {
              return (item.value == wordPair);
            }) ==
            -1) {
          items.add(DropdownMenuItem(
            child: Text(wordPair),
            value: wordPair,
          ));
        }
        wordPair = "";
      }
    });
    input = TextFormField(
      validator: (value) {
        return (value.length < 6 ? "must be at least 6 characters long" : null);
      },
      initialValue: inputString,
      onChanged: (value) {
        inputString = value;
      },
      autofocus: true,
    );
    super.initState();
  }

  List<Widget> get appBarActions {
    return ([
      Center(child: Text("Tabs:")),
      Switch(
        activeColor: Colors.white,
        value: asTabs,
        onChanged: (value) {
          setState(() {
            asTabs = value;
          });
        },
      )
    ]);
  }

  addItemDialog() async {
    return await showDialog(
      context: MyApp.navKey.currentState.overlay.context,
      builder: (BuildContext alertContext) {
        return (AlertDialog(
          title: Text("Add an item"),
          content: Form(
            key: _formKey,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                input,
                FlatButton(
                  onPressed: () {
                    if (_formKey.currentState.validate()) {
                      setState(() {
                        editableItems.add(DropdownMenuItem(
                          child: Text(inputString),
                          value: inputString,
                        ));
                      });
                      Navigator.pop(alertContext, inputString);
                    }
                  },
                  child: Text("Ok"),
                ),
                FlatButton(
                  onPressed: () {
                    Navigator.pop(alertContext, null);
                  },
                  child: Text("Cancel"),
                ),
              ],
            ),
          ),
        ));
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    Map<String, Widget> widgets;
    widgets = {
      "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 (RaisedButton(
              onPressed: () {
                Navigator.pop(doneContext);
                setState(() {});
              },
              child: Text("Save")));
        },
        closeButton: null,
        style: TextStyle(fontStyle: FontStyle.italic),
        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
                        .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,
        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 (RaisedButton(
              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>[
              RaisedButton(
                  onPressed: () {
                    setState(() {
                      selectedItemsClose.clear();
                      selectedItemsClose.addAll(
                          Iterable<int>.generate(items.length).toList());
                    });
                    updateParent(selectedItemsClose);
                  },
                  child: Text("Select all")),
              RaisedButton(
                  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>[
              RaisedButton(
                  onPressed: () {
                    setState(() {
                      selectedItemsClose.clear();
                      selectedItemsClose.addAll(
                          Iterable<int>.generate(items.length).toList());
                    });
                    updateParent(selectedItemsClose);
                  },
                  child: Text("Select all")),
              RaisedButton(
                  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": SearchChoices.single(
        items: editableItems,
        value: selectedValueSingleDialogEditableItems,
        hint: "Select one",
        searchHint: "Select one",
        disabledHint: (Function updateParent) {
          return (FlatButton(
            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"
              : FlatButton(
                  onPressed: () {
                    addItemDialog().then((value) async {
                      if (value != null &&
                          editableItems.indexWhere(
                                  (element) => element.value == value) !=
                              -1) {
                        Navigator.pop(
                            MyApp.navKey.currentState.overlay.context);
                        updateParent(value);
                      }
                    });
                  },
                  child: Text("Add and select item"),
                ));
        },
        onChanged: (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",
      ),
      "Multi dialog editable items": SearchChoices.multiple(
        items: editableItems,
        selectedItems: editableSelectedItems,
        hint: "Select any",
        searchHint: "Select any",
        disabledHint: (Function updateParent) {
          return (FlatButton(
            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"
              : FlatButton(
                  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);
                          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: FlatButton(
            onPressed: () {
              Navigator.pop(MyApp.navKey.currentState.overlay.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 (Text(
            item,
            overflow: TextOverflow.ellipsis,
          ));
        },
        dialogBox: true,
        isExpanded: true,
      ),
      "Single dialog right to left": SearchChoices.single(
        items: ["طنجة", "فاس‎", "أكادير‎", "تزنيت‎", "آكــلــو", "سيدي بيبي"]
            .map<DropdownMenuItem<String>>((string) {
          return (DropdownMenuItem<String>(
            child: Text(
              string,
              textDirection: TextDirection.rtl,
            ),
            value: string,
          ));
        }).toList(),
        value: selectedValueSingleDialogRightToLeft,
        hint: Text(
          "ختار",
          textDirection: TextDirection.rtl,
        ),
        searchHint: Text(
          "ختار",
          textDirection: TextDirection.rtl,
        ),
        closeButton: FlatButton(
          onPressed: () {
            Navigator.pop(MyApp.navKey.currentState.overlay.context);
          },
          child: Text(
            "سدّ",
            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 Row(
            textDirection: TextDirection.rtl,
            children: <Widget>[
              (Text(
                item,
                textDirection: TextDirection.rtl,
              )),
            ],
          );
        },
      ),
      "Update value from outside the plugin": 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,
          ),
          FlatButton(
            child: Text("Select dolor sit"),
            onPressed: () {
              setState(() {
                selectedValueUpdateFromOutsideThePlugin = "dolor sit";
              });
            },
          ),
        ],
      ),
      "Multi select 3 menu no-autofocus": 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": 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),
          ]));
        },
      ),
    };

    return MaterialApp(
      navigatorKey: MyApp.navKey,
      home: asTabs
          ? DefaultTabController(
              length: widgets.length,
              child: Scaffold(
                appBar: AppBar(
                  title: const Text(appTitle),
                  actions: appBarActions,
                  bottom: TabBar(
                    isScrollable: true,
                    tabs: Iterable<int>.generate(widgets.length)
                        .toList()
                        .map((i) {
                      return (Tab(
                        text: (i + 1).toString(),
                      ));
                    }).toList(), //widgets.keys.toList().map((k){return(Tab(text: k));}).toList(),
                  ),
                ),
                body: Container(
                  padding: EdgeInsets.all(20),
                  child: TabBarView(
                    children: widgets
                        .map((k, v) {
                          return (MapEntry(
                              k,
                              SingleChildScrollView(
                                scrollDirection: Axis.vertical,
                                child: Column(children: [
                                  Text(k),
                                  SizedBox(
                                    height: 20,
                                  ),
                                  v,
                                ]),
                              )));
                        })
                        .values
                        .toList(),
                  ),
                ),
              ),
            )
          : Scaffold(
              appBar: AppBar(
                title: const Text(appTitle),
                actions: appBarActions,
              ),
              body: SingleChildScrollView(
                scrollDirection: Axis.vertical,
                child: Column(
                  children: widgets
                      .map((k, v) {
                        return (MapEntry(
                            k,
                            Center(
                                child: Card(
                                    shape: RoundedRectangleBorder(
                                      borderRadius: BorderRadius.circular(10),
                                      side: BorderSide(
                                        color: Colors.grey,
                                        width: 1.0,
                                      ),
                                    ),
                                    margin: EdgeInsets.all(20),
                                    child: Padding(
                                      padding: const EdgeInsets.all(20.0),
                                      child: Column(
                                        children: <Widget>[
                                          Text("$k:"),
                                          v,
                                        ],
                                      ),
                                    )))));
                      })
                      .values
                      .toList(),
                ),
              ),
            ),
    );
  }
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  search_choices: ^1.0.17

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:search_choices/search_choices.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
85
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
92
Learn more about scoring.

We analyzed this package on Jul 11, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.8.4
  • pana: 0.13.14
  • Flutter: 1.17.5

Analysis suggestions

Package does not support Flutter platform linux

Because:

  • package:search_choices/search_choices.dart that declares support for platforms: android, ios, macos, web

Package does not support Flutter platform windows

Because:

  • package:search_choices/search_choices.dart that declares support for platforms: android, ios, macos, web

Package not compatible with SDK dart

Because:

  • search_choices that is a package requiring null.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.1.0 <3.0.0
flutter 0.0.0
Transitive dependencies
collection 1.14.12 1.14.13
meta 1.1.8 1.2.2
sky_engine 0.0.99
typed_data 1.1.6 1.2.0
vector_math 2.0.8 2.1.0-nullsafety
Dev dependencies
flutter_test