chayanforyou

dropdown_overlay_example_1

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