flutter_easy_dropdown 2.0.0 copy "flutter_easy_dropdown: ^2.0.0" to clipboard
flutter_easy_dropdown: ^2.0.0 copied to clipboard

A feature-rich, customizable searchable dropdown with multi-selection, native pagination, async search, Material 3 styling, and multiple display modes (dialog, menu, bottomSheet).

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:flutter_easy_dropdown/flutter_easy_dropdown.dart';

void main() {
  runApp(const EasyDropdownExampleApp());
}

class EasyDropdownExampleApp extends StatelessWidget {
  const EasyDropdownExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Easy Dropdown Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6750A4),
          brightness: Brightness.light,
        ),
        useMaterial3: true,
      ),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6750A4),
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      themeMode: ThemeMode.system,
      home: const HomePage(),
    );
  }
}

class UserModel {
  final int id;
  final String name;
  final String email;
  final String role;

  const UserModel({
    required this.id,
    required this.name,
    required this.email,
    required this.role,
  });

  factory UserModel.fromJson(Map<String, dynamic> json) {
    return UserModel(
      id: json['id'] as int? ?? 0,
      name: json['name'] as String? ?? '',
      email: json['email'] as String? ?? '',
      role: json['company'] != null && json['company']['bs'] != null
          ? json['company']['bs'].toString()
          : 'Member',
    );
  }

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is UserModel && runtimeType == other.runtimeType && id == other.id;

  @override
  int get hashCode => id.hashCode;

  @override
  String toString() => name;
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage>
    with SingleTickerProviderStateMixin {
  late final TabController _tabController;

  final _formKey = GlobalKey<FormState>();

  // Single select state
  String? _selectedCountry;
  UserModel? _selectedUser;
  String? _selectedCity;

  // Multi select state
  List<String> _selectedLanguages = ['Dart', 'Flutter'];
  List<UserModel> _selectedTeamMembers = [];

  // Offline demo items
  final List<String> _countries = [
    'United States',
    'Canada',
    'United Kingdom',
    'Germany',
    'France',
    'Australia',
    'India',
    'Japan',
    'Brazil',
    'Singapore',
  ];

  final List<String> _languages = [
    'Dart',
    'Flutter',
    'Kotlin',
    'Swift',
    'TypeScript',
    'Python',
    'Rust',
    'Go',
    'C++',
    'Java',
  ];

  final List<String> _cities = [
    'New York',
    'San Francisco',
    'London',
    'Berlin',
    'Paris',
    'Tokyo',
    'Sydney',
    'Toronto',
  ];

  final Dio _dio = Dio();

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 4, vsync: this);
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  Future<List<UserModel>> _fetchOnlineUsers(String filter) async {
    try {
      final response = await _dio.get<List<dynamic>>(
        'https://jsonplaceholder.typicode.com/users',
      );
      final rawList = response.data ?? [];
      final users = rawList
          .map((e) => UserModel.fromJson(e as Map<String, dynamic>))
          .toList();

      if (filter.isEmpty) return users;
      return users
          .where((u) =>
              u.name.toLowerCase().contains(filter.toLowerCase()) ||
              u.email.toLowerCase().contains(filter.toLowerCase()))
          .toList();
    } catch (_) {
      // Mock fallback data if network fails
      return [
        const UserModel(
            id: 1, name: 'Alice Smith', email: 'alice@example.com', role: 'Dev'),
        const UserModel(
            id: 2, name: 'Bob Johnson', email: 'bob@example.com', role: 'Design'),
        const UserModel(
            id: 3,
            name: 'Charlie Brown',
            email: 'charlie@example.com',
            role: 'PM'),
      ];
    }
  }

  Future<List<String>> _fetchPaginatedItems(String filter, int offset) async {
    // Simulate network delay
    await Future<void>.delayed(const Duration(milliseconds: 600));

    final allItems = List.generate(100, (i) => 'Item #${i + 1} - Database Record');
    final filtered = filter.isEmpty
        ? allItems
        : allItems
            .where((item) =>
                item.toLowerCase().contains(filter.toLowerCase()))
            .toList();

    const pageSize = 15;
    if (offset >= filtered.length) return [];
    final end = (offset + pageSize).clamp(0, filtered.length);
    return filtered.sublist(offset, end);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Easy Dropdown'),
        elevation: 1,
        bottom: TabBar(
          controller: _tabController,
          isScrollable: true,
          tabs: const [
            Tab(icon: Icon(Icons.check_circle_outline), text: 'Single Selection'),
            Tab(icon: Icon(Icons.checklist_rounded), text: 'Multi Selection'),
            Tab(icon: Icon(Icons.cloud_sync_rounded), text: 'Async & Pagination'),
            Tab(icon: Icon(Icons.dynamic_form_rounded), text: 'Modes & Form'),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: [
          _buildSingleSelectionTab(),
          _buildMultiSelectionTab(),
          _buildAsyncPaginationTab(),
          _buildModesAndFormTab(),
        ],
      ),
    );
  }

  Widget _buildSingleSelectionTab() {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        _buildSectionHeader('Basic Searchable Dropdown',
            'Search with clear button and item highlight'),
        const SizedBox(height: 8),
        DropdownSearch<String>(
          popupTitle: const Text(
            'Select a Country',
            style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
          ),
          items: _countries,
          showSearchBox: true,
          showClearButton: true,
          showSelectedItem: true,
          label: 'Country',
          hint: 'Select or search a country',
          selectedItem: _selectedCountry,
          onChanged: (val) => setState(() => _selectedCountry = val),
        ),
        const SizedBox(height: 24),
        _buildSectionHeader('Custom Item Builder & Favorite Chips',
            'Rich custom UI for dropdown items and quick chips'),
        const SizedBox(height: 8),
        DropdownSearch<String>(
          items: _cities,
          showSearchBox: true,
          showFavoriteItems: true,
          favoriteItems: (items) => ['New York', 'London', 'Tokyo'],
          label: 'City',
          hint: 'Choose your destination',
          selectedItem: _selectedCity,
          onChanged: (val) => setState(() => _selectedCity = val),
          popupItemBuilder: (context, item, isSelected) {
            return ListTile(
              leading: const Icon(Icons.location_city_rounded),
              title: Text(item),
              selected: isSelected,
              selectedColor: Theme.of(context).colorScheme.primary,
            );
          },
        ),
        const SizedBox(height: 24),
        _buildSectionHeader(
            'Disabled Items Example', 'Items starting with "U" are disabled'),
        const SizedBox(height: 8),
        DropdownSearch<String>(
          items: _countries,
          showSearchBox: true,
          label: 'Country (Some Disabled)',
          hint: 'Select Country',
          popupItemDisabled: (item) => item.startsWith('U'),
          onChanged: (val) {},
        ),
      ],
    );
  }

  Widget _buildMultiSelectionTab() {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        _buildSectionHeader('Multi-Selection with Chips Display',
            'Tap to select multiple items, click chip x to remove'),
        const SizedBox(height: 8),
        DropdownSearch<String>.multiSelection(
          popupTitle: const Text(
            'Select Programming Languages',
            style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
          ),
          items: _languages,
          showSearchBox: true,
          showClearButton: true,
          label: 'Skills',
          hint: 'Select programming languages',
          selectedItems: _selectedLanguages,
          onChanged: (items) => setState(() => _selectedLanguages = items),
        ),
        const SizedBox(height: 24),
        _buildSectionHeader('Custom Multi-Selection with Models',
            'Select team members in BottomSheet mode'),
        const SizedBox(height: 8),
        DropdownSearch<UserModel>.multiSelection(
          mode: Mode.bottomSheet,
          popupTitle: const Text(
            'Assign Team Members',
            style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
          ),
          asyncItems: _fetchOnlineUsers,
          itemAsString: (u) => u.name,
          compareFn: (u1, u2) => u1.id == u2?.id,
          showSearchBox: true,
          label: 'Team Members',
          hint: 'Select members',
          selectedItems: _selectedTeamMembers,
          onChanged: (items) => setState(() => _selectedTeamMembers = items),
          popupItemBuilder: (context, item, isSelected) {
            return ListTile(
              leading: CircleAvatar(
                child: Text(item.name.isNotEmpty ? item.name[0] : '?'),
              ),
              title: Text(item.name),
              subtitle: Text(item.email),
              trailing: isSelected
                  ? Icon(Icons.check_circle_rounded,
                      color: Theme.of(context).colorScheme.primary)
                  : null,
            );
          },
        ),
      ],
    );
  }

  Widget _buildAsyncPaginationTab() {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        _buildSectionHeader('Online API Async Search (Dio)',
            'Fetches users from REST API with live debounced search'),
        const SizedBox(height: 8),
        DropdownSearch<UserModel>(
          popupTitle: const Text(
            'Search Users Online',
            style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
          ),
          asyncItems: _fetchOnlineUsers,
          itemAsString: (u) => '${u.name} (${u.email})',
          compareFn: (u1, u2) => u1.id == u2?.id,
          showSearchBox: true,
          showClearButton: true,
          isFilteredOnline: true,
          label: 'User Account',
          hint: 'Search by name or email',
          selectedItem: _selectedUser,
          onChanged: (u) => setState(() => _selectedUser = u),
          popupItemBuilder: (context, item, isSelected) {
            return ListTile(
              leading: CircleAvatar(
                child: Text(item.name.isNotEmpty ? item.name[0] : '?'),
              ),
              title: Text(item.name),
              subtitle: Text('${item.email} • ${item.role}'),
              selected: isSelected,
            );
          },
        ),
        const SizedBox(height: 24),
        _buildSectionHeader('Infinite Scroll / Native Pagination',
            'Zero external dependencies, automatically loads more on scroll'),
        const SizedBox(height: 8),
        DropdownSearch<String>(
          popupTitle: const Text(
            'Paginated Database Records',
            style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
          ),
          showSearchBox: true,
          onLoadMore: _fetchPaginatedItems,
          label: 'Paginated Items (100 total)',
          hint: 'Scroll down inside popup to load more',
          onChanged: (val) {},
        ),
      ],
    );
  }

  Widget _buildModesAndFormTab() {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        _buildSectionHeader('Presentation Modes', 'Dialog, BottomSheet, Menu'),
        const SizedBox(height: 8),
        DropdownSearch<String>(
          mode: Mode.menu,
          items: _countries,
          showSearchBox: true,
          label: 'Menu Mode Popup',
          hint: 'Opens as context menu',
          onChanged: (val) {},
        ),
        const SizedBox(height: 16),
        DropdownSearch<String>(
          mode: Mode.bottomSheet,
          items: _countries,
          showSearchBox: true,
          label: 'BottomSheet Mode Popup',
          hint: 'Opens from bottom of screen',
          onChanged: (val) {},
        ),
        const SizedBox(height: 24),
        _buildSectionHeader('Form Validation Integration',
            'Integrates with Flutter Form and FormField validators'),
        const SizedBox(height: 8),
        Form(
          key: _formKey,
          child: Column(
            children: [
              DropdownSearch<String>(
                items: _countries,
                showSearchBox: true,
                label: 'Required Country *',
                hint: 'Must select a country',
                validator: (item) {
                  if (item == null || item.isEmpty) {
                    return 'Please select a country';
                  }
                  if (item == 'Brazil') {
                    return 'Brazil is currently unavailable';
                  }
                  return null;
                },
                onChanged: (val) {},
              ),
              const SizedBox(height: 12),
              FilledButton.icon(
                onPressed: () {
                  if (_formKey.currentState?.validate() == true) {
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(
                        content: Text('Form is valid and submitted!'),
                        backgroundColor: Colors.green,
                      ),
                    );
                  }
                },
                icon: const Icon(Icons.send_rounded),
                label: const Text('Validate & Submit Form'),
              ),
            ],
          ),
        ),
      ],
    );
  }

  Widget _buildSectionHeader(String title, String subtitle) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          title,
          style: const TextStyle(
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
        ),
        Text(
          subtitle,
          style: TextStyle(
            fontSize: 12,
            color: Theme.of(context).colorScheme.outline,
          ),
        ),
      ],
    );
  }
}
5
likes
160
points
134
downloads

Documentation

API reference

Publisher

verified publishertherohitsoni.in

Weekly Downloads

A feature-rich, customizable searchable dropdown with multi-selection, native pagination, async search, Material 3 styling, and multiple display modes (dialog, menu, bottomSheet).

Repository (GitHub)
View/report issues

License

BSD-3-Clause (license)

Dependencies

dio, flutter

More

Packages that depend on flutter_easy_dropdown