flutter_easy_dropdown 2.0.0
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).
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,
),
),
],
);
}
}