chayanforyou

dropdown_overlay_example_4 (final)

Aug 12th, 2025
1,920
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Dart 11.47 KB | None | 0 0
  1. import 'dart:async';
  2.  
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter_vector_icons/flutter_vector_icons.dart';
  5.  
  6. /// Type definitions for the dropdown widget
  7. typedef ItemFetcher<T> = FutureOr<List<T>> Function();
  8. typedef ItemLabel<T> = String Function(T item);
  9. typedef ItemWidgetBuilder<T> = Widget Function(BuildContext context, T item);
  10. typedef ItemSelected<T> = void Function(T item);
  11. typedef DropdownBuilder<T> = Widget Function(
  12.   BuildContext context,
  13.   List<T> items,
  14.   void Function(T) selectItem,
  15.   bool loading,
  16.   String? error,
  17. );
  18.  
  19. /// A customizable dropdown widget with asynchronous item loading and filtering
  20. class CustomDropdown<T> extends StatefulWidget {
  21.   final ItemFetcher<T> items;
  22.   final T? selectedItem;
  23.   final ItemLabel<T> itemLabel;
  24.   final ItemWidgetBuilder<T>? itemBuilder;
  25.   final ItemSelected<T>? onSelected;
  26.   final InputDecoration? decoration;
  27.   final DropdownBuilder<T>? dropdownBuilder;
  28.   final FormFieldValidator<T>? validator;
  29.   final AutovalidateMode? autovalidateMode;
  30.   final bool showSearch;
  31.  
  32.   const CustomDropdown({
  33.     super.key,
  34.     required this.items,
  35.     required this.selectedItem,
  36.     required this.itemLabel,
  37.     this.itemBuilder,
  38.     this.dropdownBuilder,
  39.     this.onSelected,
  40.     this.decoration,
  41.     this.validator,
  42.     this.autovalidateMode,
  43.     this.showSearch = false,
  44.   }) : assert(
  45.           dropdownBuilder != null || itemBuilder != null,
  46.           'You must provide itemBuilder if dropdownBuilder is null',
  47.         );
  48.  
  49.   @override
  50.   State<CustomDropdown<T>> createState() => _CustomDropdownState<T>();
  51. }
  52.  
  53. class _CustomDropdownState<T> extends State<CustomDropdown<T>> with WidgetsBindingObserver {
  54.   final TextEditingController _controller = TextEditingController();
  55.   final GlobalKey _fieldKey = GlobalKey();
  56.   final OverlayPortalController _overlayController = OverlayPortalController();
  57.   final ValueNotifier<double> _keyboardHeight = ValueNotifier(0);
  58.   final ValueNotifier<bool> _isOpen = ValueNotifier(false);
  59.   final ValueNotifier<_DropdownState<T>> _state = ValueNotifier(_DropdownState<T>());
  60.  
  61.   @override
  62.   void initState() {
  63.     super.initState();
  64.     WidgetsBinding.instance.addObserver(this);
  65.   }
  66.  
  67.   @override
  68.   void dispose() {
  69.     WidgetsBinding.instance.removeObserver(this);
  70.     _controller.dispose();
  71.     _state.dispose();
  72.     _isOpen.dispose();
  73.     _keyboardHeight.dispose();
  74.     super.dispose();
  75.   }
  76.  
  77.   @override
  78.   void didChangeMetrics() {
  79.     final bottomInset = View.of(context).viewInsets.bottom / View.of(context).devicePixelRatio;
  80.     _keyboardHeight.value = bottomInset;
  81.   }
  82.  
  83.   /// Loads items asynchronously and updates state
  84.   Future<void> _loadItems() async {
  85.     _state.value = _state.value.copyWith(loading: true, error: null);
  86.     try {
  87.       final items = await widget.items();
  88.       if (!mounted) return;
  89.       _state.value = _state.value.copyWith(
  90.         allItems: items,
  91.         filteredItems: items,
  92.         loading: false,
  93.       );
  94.     } catch (e) {
  95.       if (!mounted) return;
  96.       _state.value = _state.value.copyWith(
  97.         error: 'Failed to load items',
  98.         loading: false,
  99.       );
  100.     }
  101.   }
  102.  
  103.   /// Toggle the dropdown overlay
  104.   void _toggleOverlay() {
  105.     if (_isOpen.value) {
  106.       _hideOverlay();
  107.     } else {
  108.       _showOverlay();
  109.     }
  110.   }
  111.  
  112.   /// Shows the dropdown overlay
  113.   void _showOverlay() {
  114.     _loadItems();
  115.     _overlayController.show();
  116.     _isOpen.value = true;
  117.   }
  118.  
  119.   /// Hides the dropdown overlay
  120.   void _hideOverlay() {
  121.     _overlayController.hide();
  122.     _isOpen.value = false;
  123.     _state.value = _state.value.copyWith(filteredItems: _state.value.allItems);
  124.   }
  125.  
  126.   /// Filters items based on search query
  127.   void _filterItems(String query) {
  128.     final filtered = _state.value.allItems
  129.         .where((item) => widget.itemLabel(item).toLowerCase().contains(query.toLowerCase()))
  130.         .toList();
  131.     _state.value = _state.value.copyWith(filteredItems: filtered);
  132.   }
  133.  
  134.   /// Handles item selection
  135.   void _selectItem(T item) {
  136.     final label = widget.itemLabel(item);
  137.     _controller.text = label;
  138.  
  139.     final fieldState = _fieldKey.currentState as FormFieldState<T>?;
  140.     fieldState?.didChange(item);
  141.  
  142.     widget.onSelected?.call(item);
  143.     _hideOverlay();
  144.   }
  145.  
  146.   @override
  147.   Widget build(BuildContext context) {
  148.     return OverlayPortal(
  149.       controller: _overlayController,
  150.       overlayChildBuilder: (context) {
  151.         return ValueListenableBuilder<double>(
  152.           valueListenable: _keyboardHeight,
  153.           builder: (_, keyboardHeight, __) {
  154.             if (_fieldKey.currentContext == null) return const SizedBox.shrink();
  155.  
  156.             final fieldBox = _fieldKey.currentContext!.findRenderObject() as RenderBox;
  157.             final fieldSize = fieldBox.size;
  158.             final fieldOffset = fieldBox.localToGlobal(Offset.zero);
  159.  
  160.             final mq = MediaQuery.of(context);
  161.             const minOverlayHeight = 80.0;
  162.             const maxOverlayHeightCap = 400.0;
  163.  
  164.             final spaceBelow = mq.size.height - fieldOffset.dy - fieldSize.height - keyboardHeight - mq.padding.bottom;
  165.             final spaceAbove = mq.size.height - keyboardHeight - mq.padding.top - 10;
  166.  
  167.             final openUpward = spaceBelow < 300 && spaceAbove > spaceBelow;
  168.  
  169.             final availableHeight = openUpward ? spaceAbove : spaceBelow;
  170.             final maxHeight = availableHeight.clamp(minOverlayHeight, maxOverlayHeightCap);
  171.  
  172.             final top = openUpward
  173.                 ? mq.size.height - keyboardHeight - mq.padding.bottom - maxHeight
  174.                 : fieldOffset.dy + fieldSize.height;
  175.  
  176.             return Stack(
  177.               children: [
  178.                 // Background tap handler to close overlay
  179.                 Positioned.fill(
  180.                   child: GestureDetector(
  181.                     behavior: HitTestBehavior.opaque,
  182.                     onTap: _hideOverlay,
  183.                   ),
  184.                 ),
  185.                 // Dropdown content
  186.                 Positioned(
  187.                   left: fieldOffset.dx,
  188.                   top: top,
  189.                   width: fieldSize.width,
  190.                   height: maxHeight,
  191.                   child: Material(
  192.                     elevation: 8,
  193.                     color: Colors.white,
  194.                     shape: const RoundedRectangleBorder(
  195.                       borderRadius: BorderRadius.all(
  196.                         Radius.circular(12),
  197.                       ),
  198.                     ),
  199.                     child: Column(
  200.                       children: [
  201.                         if (widget.showSearch)
  202.                           Padding(
  203.                             padding: const EdgeInsets.all(8),
  204.                             child: TextField(
  205.                               decoration: InputDecoration(
  206.                                 hintText: 'Search...',
  207.                                 prefixIcon: const Icon(EvilIcons.search),
  208.                                 enabledBorder: OutlineInputBorder(
  209.                                   borderSide: BorderSide(color: Colors.grey.shade300),
  210.                                   borderRadius: BorderRadius.circular(16),
  211.                                 ),
  212.                               ),
  213.                               onChanged: _filterItems,
  214.                             ),
  215.                           ),
  216.                         Expanded(
  217.                           child: ValueListenableBuilder<_DropdownState<T>>(
  218.                             valueListenable: _state,
  219.                             builder: (context, state, _) {
  220.                               if (widget.dropdownBuilder != null) {
  221.                                 return widget.dropdownBuilder!(
  222.                                     context, state.filteredItems, _selectItem, state.loading, state.error);
  223.                               }
  224.                               if (state.loading) {
  225.                                 return const Center(child: CircularProgressIndicator());
  226.                               }
  227.                               if (state.error != null) {
  228.                                 return ListTile(
  229.                                   leading: const Icon(Icons.error_outline),
  230.                                   title: Text(state.error!),
  231.                                 );
  232.                               }
  233.                               if (state.filteredItems.isEmpty) {
  234.                                 return const ListTile(title: Text('No data available'));
  235.                               }
  236.                               return ListView.separated(
  237.                                 padding: EdgeInsets.zero,
  238.                                 shrinkWrap: true,
  239.                                 itemCount: state.filteredItems.length,
  240.                                 separatorBuilder: (_, __) => const Divider(height: 1),
  241.                                 itemBuilder: (context, index) {
  242.                                   final item = state.filteredItems[index];
  243.                                   return InkWell(
  244.                                     onTap: () => _selectItem(item),
  245.                                     child: widget.itemBuilder!(context, item),
  246.                                   );
  247.                                 },
  248.                               );
  249.                             },
  250.                           ),
  251.                         ),
  252.                       ],
  253.                     ),
  254.                   ),
  255.                 ),
  256.               ],
  257.             );
  258.           },
  259.         );
  260.       },
  261.       child: FormField<T>(
  262.         key: _fieldKey,
  263.         initialValue: widget.selectedItem,
  264.         validator: widget.validator,
  265.         autovalidateMode: widget.autovalidateMode,
  266.         builder: (FormFieldState<T> field) {
  267.           if (field.value != widget.selectedItem) {
  268.             WidgetsBinding.instance.addPostFrameCallback((_) {
  269.               if (mounted) {
  270.                 field.didChange(widget.selectedItem);
  271.                 final label = widget.itemLabel(field.value as T);
  272.                 _controller.text = label;
  273.               }
  274.             });
  275.           }
  276.  
  277.           return TextField(
  278.             controller: _controller,
  279.             readOnly: true,
  280.             decoration: (widget.decoration ??
  281.                     InputDecoration(
  282.                       hintText: 'Select...',
  283.                       suffixIcon: ValueListenableBuilder<bool>(
  284.                         valueListenable: _isOpen,
  285.                         builder: (_, isOpen, __) {
  286.                           return Icon(
  287.                             isOpen ? EvilIcons.chevron_up : EvilIcons.chevron_down,
  288.                           );
  289.                         },
  290.                       ),
  291.                     ))
  292.                 .copyWith(
  293.               errorText: field.errorText,
  294.             ),
  295.             onTap: _toggleOverlay,
  296.           );
  297.         },
  298.       ),
  299.     );
  300.   }
  301. }
  302.  
  303. /// State class for dropdown data
  304. class _DropdownState<T> {
  305.   final List<T> allItems;
  306.   final List<T> filteredItems;
  307.   final bool loading;
  308.   final String? error;
  309.  
  310.   _DropdownState({
  311.     this.allItems = const [],
  312.     this.filteredItems = const [],
  313.     this.loading = true,
  314.     this.error,
  315.   });
  316.  
  317.   _DropdownState<T> copyWith({
  318.     List<T>? allItems,
  319.     List<T>? filteredItems,
  320.     bool? loading,
  321.     String? error,
  322.   }) {
  323.     return _DropdownState<T>(
  324.       allItems: allItems ?? this.allItems,
  325.       filteredItems: filteredItems ?? this.filteredItems,
  326.       loading: loading ?? this.loading,
  327.       error: error ?? this.error,
  328.     );
  329.   }
  330. }
  331.  
Advertisement
Add Comment
Please, Sign In to add comment