popup_dropdown 1.0.0
popup_dropdown: ^1.0.0 copied to clipboard
A highly customizable popup dropdown widget for Flutter with form validation, animated icon rotation, error states, and zero external dependencies.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:popup_dropdown/popup_dropdown.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'PopupDropdown Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: const ExamplePage(),
);
}
}
// ── Sample data models ──────────────────────────────────────────────────────
class Country {
final String name;
final String flag;
const Country(this.name, this.flag);
}
class Category {
final int id;
final String name;
final IconData icon;
const Category(this.id, this.name, this.icon);
}
// ── Example page ────────────────────────────────────────────────────────────
class ExamplePage extends StatefulWidget {
const ExamplePage({super.key});
@override
State<ExamplePage> createState() => _ExamplePageState();
}
class _ExamplePageState extends State<ExamplePage> {
final _formKey = GlobalKey<FormState>();
// State for each example
String? _selectedFruit;
Country? _selectedCountry;
Category? _selectedCategory;
String? _selectedPriority;
String? _styledValue;
String? _validatedValue;
final _fruits = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry', 'Fig'];
final _countries = [
const Country('United States', '🇺🇸'),
const Country('Sri Lanka', '🇱🇰'),
const Country('United Kingdom', '🇬🇧'),
const Country('Australia', '🇦🇺'),
const Country('Japan', '🇯🇵'),
const Country('Germany', '🇩🇪'),
];
final _categories = [
const Category(1, 'Electronics', Icons.devices),
const Category(2, 'Clothing', Icons.checkroom),
const Category(3, 'Food & Beverage', Icons.restaurant),
const Category(4, 'Books', Icons.book),
const Category(5, 'Sports', Icons.sports),
];
final _priorities = ['Low', 'Medium', 'High', 'Critical'];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('PopupDropdown Examples'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(20),
children: [
_sectionHeader('1 · Basic (String list)'),
PopupDropdown<String>(
items: _fruits,
labelExtractor: (f) => f,
hint: 'Pick a fruit',
title: 'Favourite Fruit',
selectedItem: _selectedFruit,
onSelected: (v) => setState(() => _selectedFruit = v),
),
_sectionHeader('2 · Object list with leading icon'),
PopupDropdown<Country>(
items: _countries,
labelExtractor: (c) => '${c.flag} ${c.name}',
hint: 'Choose country',
title: 'Country',
selectedItem: _selectedCountry,
onSelected: (v) => setState(() => _selectedCountry = v),
),
_sectionHeader('3 · Leading icon builder per item'),
PopupDropdown<Category>(
items: _categories,
labelExtractor: (c) => c.name,
hint: 'Select category',
title: 'Category',
isRequired: true,
selectedItem: _selectedCategory,
onSelected: (v) => setState(() => _selectedCategory = v),
itemLeadingBuilder: (c) => Icon(c.icon, size: 20),
),
_sectionHeader('4 · Custom trailing per item'),
PopupDropdown<String>(
items: _priorities,
labelExtractor: (p) => p,
hint: 'Select priority',
title: 'Task Priority',
selectedItem: _selectedPriority,
onSelected: (v) => setState(() => _selectedPriority = v),
itemTrailingBuilder: (item, isSelected) => Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: _priorityColor(item).withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: Text(
item,
style: TextStyle(
color: _priorityColor(item),
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
),
),
_sectionHeader('5 · Custom colours & no dividers'),
PopupDropdown<String>(
items: _fruits,
labelExtractor: (f) => f,
hint: 'Pick a fruit',
title: 'Styled Dropdown',
selectedItem: _styledValue,
onSelected: (v) => setState(() => _styledValue = v),
fillColor: Colors.indigo.shade50,
borderColor: Colors.indigo.shade200,
focusedBorderColor: Colors.indigo,
borderRadius: 20,
height: 52,
suffixIcon: Icons.expand_more,
suffixIconColor: Colors.indigo,
selectedCheckColor: Colors.indigo,
showDividers: false,
rotateSuffixIcon: true,
selectedStyle: const TextStyle(
color: Colors.indigo,
fontWeight: FontWeight.w700,
),
),
_sectionHeader('6 · Form validation'),
PopupDropdown<String>(
items: _fruits,
labelExtractor: (f) => f,
hint: 'Required field',
title: 'Validated Dropdown',
isRequired: true,
selectedItem: _validatedValue,
onSelected: (v) => setState(() => _validatedValue = v),
validator: (val) =>
val == null ? 'Please select an option' : null,
autovalidateMode: AutovalidateMode.onUserInteraction,
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Form is valid! ✅')),
);
}
},
child: const Text('Validate Form'),
),
),
_sectionHeader('7 · Disabled state'),
PopupDropdown<String>(
items: _fruits,
labelExtractor: (f) => f,
hint: 'Cannot open',
title: 'Disabled',
selectedItem: 'Banana',
enabled: false,
),
const SizedBox(height: 40),
],
),
),
);
}
Widget _sectionHeader(String text) {
return Padding(
padding: const EdgeInsets.only(top: 4, bottom: 4),
child: Text(
text,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.grey.shade600,
letterSpacing: 0.3,
),
),
);
}
Color _priorityColor(String priority) {
switch (priority) {
case 'Low':
return Colors.green;
case 'Medium':
return Colors.orange;
case 'High':
return Colors.deepOrange;
case 'Critical':
return Colors.red;
default:
return Colors.grey;
}
}
}