chayanforyou

dropdown_overlay_example_3

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