dropinity 0.0.1
dropinity: ^0.0.1 copied to clipboard
a powerful and customizable paginated dropdown widget for Flutter
dropinity #
A powerful and highly customizable Flutter dropdown widget with built-in search functionality and pagination support. Dropinity makes it easy to handle both local and remote data sources with a beautiful, animated interface.
Features #
- π Intelligent Search: Real-time filtering with custom search logic
- π‘ API Integration: Seamless pagination support through Pagify package
- πΎ Dual Mode: Works with both static lists and dynamic API data
- π¨ Fully Customizable: Complete control over UI, styling, and behavior
- π Smooth Animations: Beautiful expand/collapse transitions
- π― Type Safe: Full generic type support for your models
Dependencies #
Dropinity depends on the Pagify package for pagination functionality.
Installation #
Add to your pubspec.yaml
:
dependencies:
dropinity: ^0.0.1
Then run:
flutter pub get
Usage #
Dropinity has two modes of operation:
1. Local Mode (Static Data) #
Perfect for predefined lists or data that doesn't require API calls.
typedef DropinityLocal<Model> = Dropinity<void, Model>;
DropinityLocal<Model>(
dropdownTitle: Text(
'Select Country',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
listHeight: 300,
buttonData: ButtonData(
buttonWidth: double.infinity,
buttonHeight: 55,
hint: Text('Choose a country...'),
selectedItemWidget: (country) => Text(
country ?? '',
style: TextStyle(fontSize: 16),
),
buttonBorderRadius: BorderRadius.circular(12),
buttonBorderColor: Colors.blue,
),
textFieldData: TextFieldData(
controller: TextEditingController(),
title: 'Search',
prefixIcon: Icon(Icons.search),
borderRadius: 8,
onSearch: (pattern, country) {
if (pattern == null || country == null) return false;
return country.toLowerCase().contains(pattern.toLowerCase());
},
),
values: [
'United States',
'Canada',
'Mexico',
'Brazil',
'Argentina',
'United Kingdom',
'Germany',
'France',
],
valuesData: ValuesData(
itemBuilder: (context, index, country) {
return Container(
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: Text(country, style: TextStyle(fontSize: 15)),
);
},
),
onChanged: (selectedCountry) {
print('Selected: $selectedCountry');
},
)
2. API Mode (Paginated Remote Data) #
Ideal for large datasets with server-side pagination and filtering.
// Create a controller
final controller = PagifyController<User>();
Dropify<ApiResponse, User>.withApiRequest(
dropdownTitle: Text(
'Select User',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
listHeight: 400,
buttonData: ButtonData(
buttonWidth: double.infinity,
buttonHeight: 55,
hint: Row(
children: [
Icon(Icons.person_outline, color: Colors.grey),
SizedBox(width: 8),
Text('Search users...'),
],
),
selectedItemWidget: (user) => Row(
children: [
CircleAvatar(
radius: 16,
backgroundImage: NetworkImage(user?.avatarUrl ?? ''),
),
SizedBox(width: 12),
Text(user?.name ?? ''),
],
),
expandedListIcon: Icon(Icons.keyboard_arrow_up, color: Colors.blue),
collapsedListIcon: Icon(Icons.keyboard_arrow_down, color: Colors.grey),
),
textFieldData: TextFieldData(
controller: TextEditingController(),
title: 'Search by name or email',
prefixIcon: Icon(Icons.search),
borderRadius: 10,
fillColor: Colors.grey[100],
onSearch: (pattern, user) {
if (pattern == null || user == null) return false;
return user.name.toLowerCase().contains(pattern.toLowerCase()) ||
user.email.toLowerCase().contains(pattern.toLowerCase());
},
),
pagifyData: SearchableDropdownPagifyData(
controller: controller,
asyncCall: (context, page) async {
// Your API call
final response = await ApiService.getUsers(page: page);
return response;
},
mapper: (response) {
return PagifyData(
data: response.users,
total: response.total,
totalPages: response.totalPages,
);
},
errorMapper: (response) => response.error,
itemBuilder: (context, data, index, user) {
return Container(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 16),
child: Row(
children: [
CircleAvatar(
radius: 20,
backgroundImage: NetworkImage(user.avatarUrl),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user.name,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
Text(
user.email,
style: TextStyle(
fontSize: 13,
color: Colors.grey[600],
),
),
],
),
),
],
),
);
},
loadingBuilder: Center(
child: CircularProgressIndicator(),
),
errorBuilder: (error) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: Colors.red),
SizedBox(height: 16),
Text('Error: ${error.message}'),
],
),
),
emptyListView: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox_outlined, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('No users found'),
],
),
),
noConnectionText: 'No internet connection',
padding: EdgeInsets.all(8),
),
onChanged: (selectedUser) {
print('Selected: ${selectedUser.name}');
},
)
API Reference #
Constructors #
Dropify()
- Local Mode
const Dropify({
Key? key,
Widget? dropdownTitle,
double? listHeight,
required ButtonData<Model> buttonData,
required TextFieldData<Model> textFieldData,
required List<Model> values,
required ValuesData<Model> valuesData,
required FutureOr<void> Function(Model val) onChanged,
})
Dropify.withApiRequest()
- API Mode
const Dropify.withApiRequest({
Key? key,
Widget? dropdownTitle,
double? listHeight,
required ButtonData<Model> buttonData,
required TextFieldData<Model> textFieldData,
required SearchableDropdownPagifyData<FullResponse, Model> pagifyData,
required FutureOr<void> Function(Model val) onChanged,
})
Configuration Classes #
ButtonData
Defines the appearance and behavior of the dropdown button.
ButtonData<Model>({
required Widget Function(Model? selectedElement) selectedItemWidget,
Color? buttonBorderColor,
BorderRadius? buttonBorderRadius,
Widget? hint,
Color? color,
EdgeInsetsGeometry? padding,
double buttonWidth = double.infinity,
double buttonHeight = 50,
Widget? expandedListIcon,
Widget? collapsedListIcon,
})
Properties:
selectedItemWidget
: Builder for displaying the selected itemhint
: Widget shown when no item is selectedbuttonWidth
: Width of the button (default:double.infinity
)buttonHeight
: Height of the button (default:50
)buttonBorderRadius
: Border radius of the buttonbuttonBorderColor
: Color of the button bordercolor
: Background color of the buttonpadding
: Internal padding of the buttonexpandedListIcon
: Icon shown when dropdown is opencollapsedListIcon
: Icon shown when dropdown is closed
TextFieldData
Configures the search field inside the dropdown.
TextFieldData<Model>({
required bool Function(String? pattern, Model? element) onSearch,
TextEditingController? controller,
String? title,
Widget? prefixIcon,
Widget? suffixIcon,
double? borderRadius,
Color? borderColor,
EdgeInsetsGeometry? contentPadding,
Color? fillColor,
int? maxLength,
TextStyle? style,
})
Properties:
onSearch
: Function that defines search logic (returnstrue
if item matches)controller
: Text editing controller for the search fieldtitle
: Label/hint text for the search fieldprefixIcon
: Icon at the start of the search fieldsuffixIcon
: Icon at the end of the search fieldborderRadius
: Border radius of the search fieldborderColor
: Color of the search field bordercontentPadding
: Internal padding of the search fieldfillColor
: Background color of the search fieldmaxLength
: Maximum character lengthstyle
: Text style for the search field
ValuesData (Local Mode Only)
Defines how items are displayed in local mode.
ValuesData<Model>({
required Widget Function(BuildContext context, int i, Model element) itemBuilder,
})
Properties:
itemBuilder
: Builder function for each list item
SearchableDropdownPagifyData (API Mode Only)
Configures pagination and API integration.
SearchableDropdownPagifyData<FullResponse, Model>({
required Future<FullResponse> Function(BuildContext context, int page) asyncCall,
required PagifyData<Model> Function(FullResponse response) mapper,
required PagifyErrorMapper errorMapper,
required Widget Function(BuildContext context, List<Model> data, int index, Model element) itemBuilder,
required PagifyController<Model> controller,
EdgeInsetsGeometry padding = const EdgeInsets.all(0),
double? itemExtent,
FutureOr<void> Function(PagifyAsyncCallStatus)? onUpdateStatus,
FutureOr<void> Function()? onLoading,
FutureOr<void> Function(BuildContext, List<dynamic>)? onSuccess,
FutureOr<void> Function(BuildContext, int, PagifyException)? onError,
bool ignoreErrorBuilderWhenErrorOccursAndListIsNotEmpty = true,
bool showNoDataAlert = false,
Widget? loadingBuilder,
Widget Function(PagifyException)? errorBuilder,
Widget? emptyListView,
String? noConnectionText,
})
Properties:
controller
: Pagify controller instanceasyncCall
: Function to fetch paginated datamapper
: Maps API response to PagifyDataerrorMapper
: Maps errors from the responseitemBuilder
: Builder for each list itemloadingBuilder
: Widget shown while loadingerrorBuilder
: Widget shown on erroremptyListView
: Widget shown when list is emptypadding
: Padding around the listitemExtent
: Fixed height for list itemsnoConnectionText
: Message for no internet connectiononUpdateStatus
: Callback for status changesonLoading
: Callback when loading startsonSuccess
: Callback on successful loadonError
: Callback on error
Advanced Examples #
Custom Styling Example #
Dropify<dynamic, Product>(
dropdownTitle: Container(
padding: EdgeInsets.symmetric(vertical: 8),
child: Text(
'Select Product',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue[900],
),
),
),
listHeight: 350,
buttonData: ButtonData(
buttonWidth: double.infinity,
buttonHeight: 60,
color: Colors.blue[50],
buttonBorderRadius: BorderRadius.circular(16),
buttonBorderColor: Colors.blue[300],
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
hint: Row(
children: [
Icon(Icons.shopping_cart_outlined, color: Colors.blue),
SizedBox(width: 12),
Text(
'Choose a product...',
style: TextStyle(color: Colors.grey[600]),
),
],
),
selectedItemWidget: (product) => Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.check, color: Colors.white, size: 16),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
product?.name ?? '',
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(
'\$${product?.price.toStringAsFixed(2)}',
style: TextStyle(color: Colors.green, fontSize: 12),
),
],
),
),
],
),
expandedListIcon: Icon(Icons.expand_less, color: Colors.blue, size: 28),
collapsedListIcon: Icon(Icons.expand_more, color: Colors.blue, size: 28),
),
textFieldData: TextFieldData(
controller: TextEditingController(),
title: 'Search products by name or SKU',
prefixIcon: Icon(Icons.search, color: Colors.blue),
suffixIcon: Icon(Icons.filter_list, color: Colors.grey),
borderRadius: 12,
borderColor: Colors.blue[200],
fillColor: Colors.white,
contentPadding: EdgeInsets.all(16),
onSearch: (pattern, product) {
if (pattern == null || product == null) return false;
return product.name.toLowerCase().contains(pattern.toLowerCase()) ||
product.sku.toLowerCase().contains(pattern.toLowerCase());
},
),
values: products, // Your product list
valuesData: ValuesData(
itemBuilder: (context, index, product) {
return Container(
margin: EdgeInsets.only(bottom: 8),
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey[200]!),
),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue[100],
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.inventory_2, color: Colors.blue),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: TextStyle(fontWeight: FontWeight.w600),
),
Text(
'SKU: ${product.sku}',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
),
Text(
'\$${product.price.toStringAsFixed(2)}',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
],
),
);
},
),
onChanged: (product) {
print('Selected: ${product.name}');
},
)
Form Integration Example #
class MyForm extends StatefulWidget {
@override
_MyFormState createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
final _formKey = GlobalKey<FormState>();
Country? selectedCountry;
City? selectedCity;
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
// Country Dropdown
Dropify<dynamic, Country>(
dropdownTitle: Text('Country *'),
buttonData: ButtonData(
buttonWidth: double.infinity,
hint: Text('Select country'),
selectedItemWidget: (country) => Text(country?.name ?? ''),
),
textFieldData: TextFieldData(
controller: TextEditingController(),
onSearch: (pattern, country) =>
country?.name.toLowerCase().contains(pattern?.toLowerCase() ?? '') ?? false,
),
values: countries,
valuesData: ValuesData(
itemBuilder: (context, i, country) => ListTile(
title: Text(country.name),
),
),
onChanged: (country) {
setState(() {
selectedCountry = country;
selectedCity = null; // Reset city when country changes
});
},
),
SizedBox(height: 16),
// City Dropdown (dependent on country)
if (selectedCountry != null)
Dropify<dynamic, City>(
dropdownTitle: Text('City *'),
buttonData: ButtonData(
buttonWidth: double.infinity,
hint: Text('Select city'),
selectedItemWidget: (city) => Text(city?.name ?? ''),
),
textFieldData: TextFieldData(
controller: TextEditingController(),
onSearch: (pattern, city) =>
city?.name.toLowerCase().contains(pattern?.toLowerCase() ?? '') ?? false,
),
values: selectedCountry!.cities,
valuesData: ValuesData(
itemBuilder: (context, i, city) => ListTile(
title: Text(city.name),
),
),
onChanged: (city) {
setState(() => selectedCity = city);
},
),
SizedBox(height: 24),
ElevatedButton(
onPressed: () {
if (selectedCountry != null && selectedCity != null) {
// Submit form
print('Country: ${selectedCountry!.name}');
print('City: ${selectedCity!.name}');
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Please select all fields')),
);
}
},
child: Text('Submit'),
),
],
),
);
}
}
Performance Considerations #
- For Large Datasets: Use API mode (
Dropify.withApiRequest
) with pagination - Search Optimization: Implement efficient
onSearch
logic to avoid performance issues - State Management: The widget uses
maintainState: true
to preserve state when toggling visibility - Memory Usage: Local mode keeps all data in memory, so use API mode for very large datasets
- Controller Disposal: Always dispose controllers in
dispose()
method
@override
void dispose() {
textController.dispose();
pagifyController.dispose();
super.dispose();
}
Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.
License #
This project is licensed under the MIT License.
Support #
For issues and feature requests, please file an issue on the GitHub repository.
For questions about the Pagify package, refer to the Pagify documentation.
Made with β€οΈ for the Flutter community by Ahmed Emara linkedIn