searchable_dropdown_pro 1.1.0
searchable_dropdown_pro: ^1.1.0 copied to clipboard
A highly flexible, production-ready searchable dropdown for Flutter. Supports custom text fields, custom validators, lazy-loaded items, composited overlay positioning, and toggleable search — all with [...]
example/lib/main.dart
// ignore_for_file: avoid_print
import 'package:flutter/material.dart';
import 'package:searchable_dropdown_pro/searchable_dropdown_pro.dart';
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'searchable_dropdown_pro demo',
theme: ThemeData(
colorSchemeSeed: Colors.indigo,
useMaterial3: true,
),
home: const DemoPage(),
);
}
}
class Country {
final String name;
final String flag;
const Country(this.name, this.flag);
}
const _countries = [
Country('Afghanistan', '🇦🇫'),
Country('Albania', '🇦🇱'),
Country('Algeria', '🇩🇿'),
Country('Argentina', '🇦🇷'),
Country('Australia', '🇦🇺'),
Country('Austria', '🇦🇹'),
Country('Bangladesh', '🇧🇩'),
Country('Belgium', '🇧🇪'),
Country('Brazil', '🇧🇷'),
Country('Canada', '🇨🇦'),
Country('Chile', '🇨🇱'),
Country('China', '🇨🇳'),
Country('Colombia', '🇨🇴'),
Country('Czech Republic', '🇨🇿'),
Country('Denmark', '🇩🇰'),
Country('Egypt', '🇪🇬'),
Country('Finland', '🇫🇮'),
Country('France', '🇫🇷'),
Country('Germany', '🇩🇪'),
Country('Greece', '🇬🇷'),
Country('Hungary', '🇭🇺'),
Country('India', '🇮🇳'),
Country('Indonesia', '🇮🇩'),
Country('Iran', '🇮🇷'),
Country('Iraq', '🇮🇶'),
Country('Ireland', '🇮🇪'),
Country('Israel', '🇮🇱'),
Country('Italy', '🇮🇹'),
Country('Japan', '🇯🇵'),
Country('Jordan', '🇯🇴'),
Country('Kenya', '🇰🇪'),
Country('Malaysia', '🇲🇾'),
Country('Mexico', '🇲🇽'),
Country('Morocco', '🇲🇦'),
Country('Nepal', '🇳🇵'),
Country('Netherlands', '🇳🇱'),
Country('New Zealand', '🇳🇿'),
Country('Nigeria', '🇳🇬'),
Country('Norway', '🇳🇴'),
Country('Pakistan', '🇵🇰'),
Country('Peru', '🇵🇪'),
Country('Philippines', '🇵🇭'),
Country('Poland', '🇵🇱'),
Country('Portugal', '🇵🇹'),
Country('Romania', '🇷🇴'),
Country('Russia', '🇷🇺'),
Country('Saudi Arabia', '🇸🇦'),
Country('Singapore', '🇸🇬'),
Country('South Africa', '🇿🇦'),
Country('South Korea', '🇰🇷'),
Country('Spain', '🇪🇸'),
Country('Sri Lanka', '🇱🇰'),
Country('Sweden', '🇸🇪'),
Country('Switzerland', '🇨🇭'),
Country('Thailand', '🇹🇭'),
Country('Turkey', '🇹🇷'),
Country('Ukraine', '🇺🇦'),
Country('United Arab Emirates', '🇦🇪'),
Country('United Kingdom', '🇬🇧'),
Country('United States', '🇺🇸'),
Country('Vietnam', '🇻🇳'),
];
class DemoPage extends StatefulWidget {
const DemoPage({super.key});
@override
State<DemoPage> createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
final _formKey = GlobalKey<FormState>();
final _ctrl = SearchableDropdownController();
String? _selectedFruit;
Country? _selectedCountry;
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('searchable_dropdown_pro')),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(24),
children: [
_Section(
title: '1 · Minimal (small list, no search needed)',
child: SearchableDropdownPro<String>(
isSearchable: false,
items: const ['Small', 'Medium', 'Large', 'XL'],
displayString: (s) => s,
onSelected: (s) => print('Size: $s'),
hintText: 'Select size',
),
),
_Section(
title: '2 · Searchable (fruits)',
child: SearchableDropdownPro<String>(
items: const [
'Apple', 'Apricot', 'Avocado', 'Banana', 'Blueberry',
'Cherry', 'Coconut', 'Date', 'Dragonfruit', 'Elderberry',
'Fig', 'Grape', 'Guava', 'Kiwi', 'Lemon', 'Lime', 'Lychee',
'Mango', 'Melon', 'Orange', 'Papaya', 'Peach', 'Pear',
'Pineapple', 'Plum', 'Pomegranate', 'Raspberry', 'Strawberry',
'Watermelon',
],
displayString: (s) => s,
onSelected: (s) => setState(() => _selectedFruit = s),
hintText: 'Pick a fruit',
),
),
if (_selectedFruit != null)
Padding(
padding: const EdgeInsets.only(top: 4, left: 4, bottom: 8),
child: Text('Selected: $_selectedFruit',
style: Theme.of(context).textTheme.bodySmall),
),
_Section(
title: '3 · Custom item tiles (countries)',
child: SearchableDropdownPro<Country>(
items: _countries,
displayString: (c) => c.name,
onSelected: (c) => setState(() => _selectedCountry = c),
hintText: 'Select country',
selectedItem: _selectedCountry,
decoration: SearchableDropdownDecoration(
selectedItemColor:
Theme.of(context).colorScheme.primaryContainer,
),
itemBuilder: (ctx, country, isSelected, onTap) {
return ListTile(
leading: Text(country.flag,
style: const TextStyle(fontSize: 22)),
title: Text(country.name),
selected: isSelected,
onTap: onTap,
);
},
),
),
_Section(
title: '4 · With validation (required)',
child: SearchableDropdownPro<String>(
items: const ['Option A', 'Option B', 'Option C'],
displayString: (s) => s,
onSelected: (_) {},
hintText: 'Required field',
validator: (v) =>
(v == null || v.isEmpty) ? 'Please select an option' : null,
autovalidateMode: AutovalidateMode.onUserInteraction,
),
),
_Section(
title: '5 · Custom field builder',
child: SearchableDropdownPro<String>(
items: const ['Red', 'Green', 'Blue', 'Yellow', 'Purple'],
displayString: (s) => s,
onSelected: (_) {},
hintText: 'Colour',
fieldBuilder: (ctx, controller, focusNode, suffixIcon) {
return TextField(
controller: controller,
focusNode: focusNode,
decoration: InputDecoration(
hintText: 'My custom field',
prefixIcon: const Icon(Icons.palette_outlined),
suffixIcon: suffixIcon,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
),
),
);
},
),
),
_Section(
title: '6 · Programmatic control',
child: Column(
children: [
SearchableDropdownPro<String>(
items: const ['Alpha', 'Beta', 'Gamma', 'Delta'],
displayString: (s) => s,
onSelected: (_) {},
hintText: 'Controlled dropdown',
controller: _ctrl,
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: [
ElevatedButton(
onPressed: _ctrl.open,
child: const Text('Open'),
),
ElevatedButton(
onPressed: _ctrl.close,
child: const Text('Close'),
),
ElevatedButton(
onPressed: _ctrl.clear,
child: const Text('Clear'),
),
ElevatedButton(
onPressed: () => _ctrl.setText('Beta'),
child: const Text('Set "Beta"'),
),
],
),
],
),
),
_Section(
title: '7 · Disabled',
child: SearchableDropdownPro<String>(
enabled: false,
items: const ['A', 'B', 'C'],
displayString: (s) => s,
onSelected: (_) {},
hintText: 'Disabled dropdown',
),
),
_Section(
title: '8 · Custom theme (dark card)',
child: SearchableDropdownPro<String>(
items: const [
'Cyberpunk', 'Noir', 'Vaporwave', 'Synthwave', 'Lofi',
],
displayString: (s) => s,
onSelected: (_) {},
hintText: 'Pick a vibe',
decoration: SearchableDropdownDecoration(
dropdownColor: const Color(0xFF1A1A2E),
itemTextStyle: const TextStyle(color: Colors.white),
dividerColor: Colors.white12,
borderRadius: BorderRadius.circular(16),
noMatchText: 'Vibe not found 👾',
suffixIcon:
const Icon(Icons.expand_more, color: Colors.purpleAccent),
),
),
),
const SizedBox(height: 32),
FilledButton(
onPressed: () => _formKey.currentState?.validate(),
child: const Text('Validate form'),
),
const SizedBox(height: 80),
],
),
),
);
}
}
class _Section extends StatelessWidget {
const _Section({required this.title, required this.child});
final String title;
final Widget child;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: Theme.of(context)
.textTheme
.labelMedium
?.copyWith(color: Theme.of(context).colorScheme.primary)),
const SizedBox(height: 8),
child,
],
),
);
}
}