import 'package:dropdown_button2/dropdown_button2.dart'; import 'form_field_controller.dart'; import 'package:flutter/material.dart'; class FlutterDropDown extends StatefulWidget { const FlutterDropDown({ Key? key, required this.controller, this.hintText, this.searchHintText, required this.options, this.optionLabels, this.onChanged, this.onChangedForMultiSelect, this.icon, this.width, this.height, this.maxHeight, this.fillColor, this.searchHintTextStyle, this.searchCursorColor, required this.textStyle, required this.elevation, required this.borderWidth, required this.borderRadius, required this.borderColor, required this.margin, this.hidesUnderline = false, this.disabled = false, this.isOverButton = false, this.isSearchable = false, this.isMultiSelect = false, }) : super(key: key); final FormFieldController controller; final String? hintText; final String? searchHintText; final List options; final List? optionLabels; final Function(T?)? onChanged; final Function(List?)? onChangedForMultiSelect; final Widget? icon; final double? width; final double? height; final double? maxHeight; final Color? fillColor; final TextStyle? searchHintTextStyle; final Color? searchCursorColor; final TextStyle textStyle; final double elevation; final double borderWidth; final double borderRadius; final Color borderColor; final EdgeInsetsGeometry margin; final bool hidesUnderline; final bool disabled; final bool isOverButton; final bool isSearchable; final bool isMultiSelect; @override State> createState() => _FlutterDropDownState(); } class _FlutterDropDownState extends State> { final TextEditingController _textEditingController = TextEditingController(); void Function() get listener => widget.isMultiSelect ? () {} : () => widget.onChanged!(widget.controller.value); @override void initState() { widget.controller.addListener(listener); super.initState(); } @override void dispose() { widget.controller.removeListener(listener); super.dispose(); } List selectedItems = []; @override Widget build(BuildContext context) { final optionToDisplayValue = Map.fromEntries( widget.options.asMap().entries.map((option) => MapEntry( option.value, widget.optionLabels == null || widget.optionLabels!.length < option.key + 1 ? option.value.toString() : widget.optionLabels![option.key])), ); final value = widget.options.contains(widget.controller.value) ? widget.controller.value : null; final items = widget.options .map( (option) => DropdownMenuItem( value: option, child: Text( optionToDisplayValue[option] ?? '', style: widget.textStyle, ), ), ) .toList(); final hintText = widget.hintText != null ? Text(widget.hintText!, style: widget.textStyle) : null; void Function(T?)? onChanged = widget.disabled || widget.isMultiSelect ? null : (value) => widget.controller.value = value; final dropdownWidget = _useDropdown2() ? _buildDropdown( value, items, onChanged, hintText, optionToDisplayValue, widget.isSearchable, widget.disabled, widget.isMultiSelect, widget.onChangedForMultiSelect, ) : _buildLegacyDropdown(value, items, onChanged, hintText); final childWidget = DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(widget.borderRadius), border: Border.all( color: widget.borderColor, width: widget.borderWidth, ), color: widget.fillColor, ), child: Padding( padding: widget.margin, child: widget.hidesUnderline ? DropdownButtonHideUnderline(child: dropdownWidget) : dropdownWidget, ), ); if (widget.height != null || widget.width != null) { return Container( width: widget.width, height: widget.height, child: childWidget, ); } return childWidget; } Widget _buildLegacyDropdown( T? value, List>? items, void Function(T?)? onChanged, Text? hintText, ) { return DropdownButton( value: value, hint: hintText, items: items, elevation: widget.elevation.toInt(), onChanged: onChanged, icon: widget.icon, isExpanded: true, dropdownColor: widget.fillColor, focusColor: Colors.transparent, ); } Widget _buildDropdown( T? value, List>? items, void Function(T?)? onChanged, Text? hintText, Map optionLabels, bool isSearchable, bool disabled, bool isMultiSelect, Function(List?)? onChangedForMultiSelect, ) { final overlayColor = MaterialStateProperty.resolveWith((states) => states.contains(MaterialState.focused) ? Colors.transparent : null); final iconStyleData = widget.icon != null ? IconStyleData(icon: widget.icon!) : const IconStyleData(); return DropdownButton2( value: isMultiSelect ? selectedItems.isEmpty ? null : selectedItems.last : value, hint: hintText, items: isMultiSelect ? widget.options.map((item) { return DropdownMenuItem( value: item, // Disable default onTap to avoid closing menu when selecting an item enabled: false, child: StatefulBuilder( builder: (context, menuSetState) { final isSelected = selectedItems.contains(item); return InkWell( onTap: () { isSelected ? selectedItems.remove(item) : selectedItems.add(item); onChangedForMultiSelect!(selectedItems); //This rebuilds the StatefulWidget to update the button's text setState(() {}); //This rebuilds the dropdownMenu Widget to update the check mark menuSetState(() {}); }, child: Container( height: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Row( children: [ if (isSelected) const Icon(Icons.check_box_outlined) else const Icon(Icons.check_box_outline_blank), const SizedBox(width: 16), Expanded( child: Text( item as String, style: widget.textStyle, ), ), ], ), ), ); }, ), ); }).toList() : items, iconStyleData: iconStyleData, buttonStyleData: ButtonStyleData( elevation: widget.elevation.toInt(), overlayColor: overlayColor, ), menuItemStyleData: MenuItemStyleData(overlayColor: overlayColor), dropdownStyleData: DropdownStyleData( elevation: widget.elevation!.toInt(), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), color: widget.fillColor, ), isOverButton: widget.isOverButton, maxHeight: widget.maxHeight, ), // onChanged is handled by the onChangedForMultiSelect function onChanged: isMultiSelect ? disabled ? null : (val) {} : onChanged, isExpanded: true, selectedItemBuilder: isMultiSelect ? (context) { return widget.options.map( (item) { return Container( alignment: AlignmentDirectional.center, child: Text( selectedItems.join(', '), style: const TextStyle( fontSize: 14, overflow: TextOverflow.ellipsis, ), maxLines: 1, ), ); }, ).toList(); } : null, dropdownSearchData: isSearchable ? DropdownSearchData( searchController: _textEditingController, searchInnerWidgetHeight: 50, searchInnerWidget: Container( height: 50, padding: const EdgeInsets.only( top: 8, bottom: 4, right: 8, left: 8, ), child: TextFormField( expands: true, maxLines: null, controller: _textEditingController, cursorColor: widget.searchCursorColor, decoration: InputDecoration( isDense: true, contentPadding: const EdgeInsets.symmetric( horizontal: 10, vertical: 8, ), hintText: widget.searchHintText, hintStyle: widget.searchHintTextStyle, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), ), ), ), searchMatchFn: (item, searchValue) { return (optionLabels[item.value] ?? '') .toLowerCase() .contains(searchValue.toLowerCase()); }, ) : null, // This to clear the search value when you close the menu onMenuStateChange: isSearchable ? (isOpen) { if (!isOpen) { _textEditingController.clear(); } } : null, ); } bool _useDropdown2() { return widget.isMultiSelect || widget.isSearchable || !widget.isOverButton || widget.maxHeight != null; } }