dropinity 0.0.4
dropinity: ^0.0.4 copied to clipboard
a powerful and customizable paginated dropdown widget for Flutter
Dropinity #
A powerful and customizable Flutter dropdown widget with built-in search functionality and seamless pagination support for both local and remote data sources.
Table of Contents #
- Features
- Demo
- Installation
- Getting Started
- Usage Examples
- API Reference
- Advanced Features
- Best Practices
- Troubleshooting
- Contributing
- License
Features #
✨ Core Capabilities
- 🔍 Real-time search filtering with customizable search logic
- 📡 Seamless API integration with automatic pagination via Pagify
- 💾 Dual mode support: local data lists or remote API endpoints
- 🎨 Fully customizable UI components (buttons, text fields, list items)
- 🎭 Smooth animations with customizable curves
- 🎯 Type-safe implementation using generics
- ✅ Built-in form validation support
- 🎮 Programmatic control via
DropinityController - 🔄 State persistence when toggling dropdown
- 📦 Offline caching support for API responses
- ☑️ Multi-selection mode with pre-populated initial values
- 🔔 Expand / collapse lifecycle callbacks
- 📱 Platform-agnostic (iOS, Android, Web, Desktop)
Demo #
Note: Add screenshots or GIFs of your widget in action here for better visibility on pub.dev
// See examples below for working code
Installation #
Add dropinity to your pubspec.yaml:
dependencies:
dropinity: ^0.0.3
Then run:
flutter pub get
Import the package:
import 'package:dropinity/custom_drop_down/dropinity.dart';
Getting Started #
Basic Usage #
For simple dropdown with a predefined list of items:
import 'package:dropinity/custom_drop_down/dropinity.dart';
import 'package:flutter/material.dart';
class SimpleDropdownExample extends StatelessWidget {
final controller = DropinityController();
@override
Widget build(BuildContext context) {
return Dropinity<void, String>(
controller: controller,
buttonData: ButtonData(
hint: Text('Select a fruit'),
selectedItemWidget: (fruit) => Text(fruit ?? ''),
),
textFieldData: TextFieldData(
onSearch: (pattern, fruit) =>
fruit?.toLowerCase().contains(pattern?.toLowerCase() ?? '') ?? false,
),
values: ['Apple', 'Banana', 'Orange', 'Mango', 'Grape'],
valuesData: ValuesData(
itemBuilder: (context, index, fruit) => ListTile(
title: Text(fruit),
leading: Icon(Icons.check_circle_outline),
),
),
onChanged: (selectedFruit) {
print('Selected: $selectedFruit');
},
);
}
}
With API Integration #
For dropdown with paginated API data:
import 'package:dropinity/custom_drop_down/dropinity.dart';
import 'package:pagify/pagify.dart';
import 'package:flutter/material.dart';
class User {
final String id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
}
class ApiResponse {
final List<User> users;
final int totalPages;
ApiResponse({required this.users, required this.totalPages});
}
class ApiDropdownExample extends StatefulWidget {
@override
_ApiDropdownExampleState createState() => _ApiDropdownExampleState();
}
class _ApiDropdownExampleState extends State<ApiDropdownExample> {
final _dropinityController = DropinityController();
final _pagifyController = PagifyController<User>();
@override
void dispose() {
_dropinityController.dispose();
_pagifyController.dispose();
super.dispose();
}
Future<ApiResponse> _fetchUsers(int page) async {
// Your API call here
final response = await http.get(Uri.parse('https://api.example.com/users?page=$page'));
// Parse and return data
return ApiResponse(users: parsedUsers, totalPages: totalPages);
}
@override
Widget build(BuildContext context) {
return Dropinity<ApiResponse, User>.withApiRequest(
controller: _dropinityController,
buttonData: ButtonData(
hint: Text('Select a user'),
selectedItemWidget: (user) => Text(user?.name ?? ''),
),
textFieldData: TextFieldData(
onSearch: (pattern, user) =>
user?.name.toLowerCase().contains(pattern?.toLowerCase() ?? '') ?? false,
),
pagifyData: DropinityPagifyData(
controller: _pagifyController,
asyncCall: (context, page) => _fetchUsers(page),
mapper: (response) => PagifyData(
data: response.users,
paginationData: PaginationData(
totalPages: response.totalPages,
perPage: 20,
),
),
errorMapper: PagifyErrorMapper(
// Configure error handling
),
itemBuilder: (context, data, index, user) => ListTile(
title: Text(user.name),
subtitle: Text(user.email),
leading: CircleAvatar(child: Text(user.name[0])),
),
),
onChanged: (user) {
print('Selected user: ${user.name}');
},
);
}
}
Usage Examples #
1. Custom Styling #
Create a beautifully styled dropdown:
Dropinity<void, String>(
controller: DropinityController(),
listHeight: 300,
curve: Curves.easeInOutCubic,
listBackgroundColor: Colors.grey[50]!,
buttonData: ButtonData(
hint: Row(
children: [
Icon(Icons.category, color: Colors.blue),
SizedBox(width: 8),
Text('Choose category', style: TextStyle(color: Colors.grey[600])),
],
),
selectedItemWidget: (item) => Text(
item ?? '',
style: TextStyle(fontWeight: FontWeight.w600),
),
buttonHeight: 56,
buttonBorderRadius: BorderRadius.circular(16),
buttonBorderColor: Colors.blue.shade200,
color: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 16),
expandedListIcon: Icon(Icons.arrow_drop_up, color: Colors.blue),
collapsedListIcon: Icon(Icons.arrow_drop_down, color: Colors.grey),
),
textFieldData: TextFieldData(
title: 'Search categories',
prefixIcon: Icon(Icons.search, color: Colors.blue),
borderRadius: 12,
borderColor: Colors.blue.shade100,
fillColor: Colors.white,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
onSearch: (pattern, item) =>
item?.toLowerCase().contains(pattern?.toLowerCase() ?? '') ?? false,
),
values: ['Electronics', 'Clothing', 'Food', 'Books', 'Toys'],
valuesData: ValuesData(
itemBuilder: (context, index, category) => Container(
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: Row(
children: [
Icon(Icons.label, color: Colors.blue.shade300, size: 20),
SizedBox(width: 12),
Text(category, style: TextStyle(fontSize: 15)),
],
),
),
),
onChanged: (category) => print('Selected: $category'),
)
2. Form Validation #
Integrate with Flutter forms:
class ValidatedForm extends StatefulWidget {
@override
_ValidatedFormState createState() => _ValidatedFormState();
}
class _ValidatedFormState extends State<ValidatedForm> {
final _formKey = GlobalKey<FormState>();
final _dropinityController = DropinityController();
String? selectedCountry;
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
Dropinity<void, String>(
controller: _dropinityController,
autoValidateMode: AutovalidateMode.onUserInteraction,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please select a country';
}
return null;
},
errorWidget: (errorMsg) => Padding(
padding: EdgeInsets.only(top: 8),
child: Text(
errorMsg,
style: TextStyle(color: Colors.red, fontSize: 12),
),
),
buttonData: ButtonData(
hint: Text('Select country *'),
selectedItemWidget: (country) => Text(country ?? ''),
),
textFieldData: TextFieldData(
onSearch: (pattern, country) =>
country?.toLowerCase().contains(pattern?.toLowerCase() ?? '') ?? false,
),
values: ['USA', 'Canada', 'UK', 'Germany', 'France'],
valuesData: ValuesData(
itemBuilder: (context, index, country) => ListTile(
title: Text(country),
),
),
onChanged: (country) {
setState(() => selectedCountry = country);
},
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
print('Form is valid!');
}
},
child: Text('Submit'),
),
],
),
);
}
@override
void dispose() {
_dropinityController.dispose();
super.dispose();
}
}
3. Programmatic Control #
Control dropdown behavior with the controller:
class ControlledDropdown extends StatefulWidget {
@override
_ControlledDropdownState createState() => _ControlledDropdownState();
}
class _ControlledDropdownState extends State<ControlledDropdown> {
final _controller = DropinityController();
@override
Widget build(BuildContext context) {
return Column(
children: [
Dropinity<void, String>(
controller: _controller,
buttonData: ButtonData(
hint: Text('Select option'),
selectedItemWidget: (item) => Text(item ?? ''),
),
textFieldData: TextFieldData(
onSearch: (pattern, item) =>
item?.toLowerCase().contains(pattern?.toLowerCase() ?? '') ?? false,
),
values: ['Option 1', 'Option 2', 'Option 3'],
valuesData: ValuesData(
itemBuilder: (context, index, item) => ListTile(title: Text(item)),
),
onChanged: (item) => print(item),
),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => _controller.expand(),
child: Text('Open'),
),
SizedBox(width: 8),
ElevatedButton(
onPressed: () => _controller.collapse(),
child: Text('Close'),
),
SizedBox(width: 8),
ElevatedButton(
onPressed: () {
if (_controller.isExpanded) {
_controller.collapse();
} else {
_controller.expand();
}
},
child: Text('Toggle'),
),
],
),
],
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
4. Dependent Dropdowns #
Create cascading dropdowns:
class Country {
final String name;
final List<String> cities;
Country({required this.name, required this.cities});
}
class DependentDropdowns extends StatefulWidget {
@override
_DependentDropdownsState createState() => _DependentDropdownsState();
}
class _DependentDropdownsState extends State<DependentDropdowns> {
final _countryController = DropinityController();
final _cityController = DropinityController();
final countries = [
Country(name: 'USA', cities: ['New York', 'Los Angeles', 'Chicago']),
Country(name: 'Canada', cities: ['Toronto', 'Vancouver', 'Montreal']),
Country(name: 'UK', cities: ['London', 'Manchester', 'Birmingham']),
];
Country? selectedCountry;
String? selectedCity;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Select Country', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Dropinity<void, Country>(
controller: _countryController,
buttonData: ButtonData(
hint: Text('Choose a country'),
selectedItemWidget: (country) => Text(country?.name ?? ''),
),
textFieldData: TextFieldData(
onSearch: (pattern, country) =>
country?.name.toLowerCase().contains(pattern?.toLowerCase() ?? '') ?? false,
),
values: countries,
valuesData: ValuesData(
itemBuilder: (context, index, country) => ListTile(
title: Text(country.name),
),
),
onChanged: (country) {
setState(() {
selectedCountry = country;
selectedCity = null; // Reset city when country changes
});
},
),
SizedBox(height: 20),
if (selectedCountry != null) ...[
Text('Select City', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Dropinity<void, String>(
controller: _cityController,
buttonData: ButtonData(
hint: Text('Choose a city'),
selectedItemWidget: (city) => Text(city ?? ''),
),
textFieldData: TextFieldData(
onSearch: (pattern, city) =>
city?.toLowerCase().contains(pattern?.toLowerCase() ?? '') ?? false,
),
values: selectedCountry!.cities,
valuesData: ValuesData(
itemBuilder: (context, index, city) => ListTile(
title: Text(city),
),
),
onChanged: (city) {
setState(() => selectedCity = city);
},
),
],
],
);
}
@override
void dispose() {
_countryController.dispose();
_cityController.dispose();
super.dispose();
}
}
5. Type-safe with Custom Models #
Use your own data models:
class Product {
final String id;
final String name;
final double price;
final String category;
Product({
required this.id,
required this.name,
required this.price,
required this.category,
});
}
// Usage
Dropinity<void, Product>(
controller: DropinityController(),
buttonData: ButtonData(
hint: Text('Select product'),
selectedItemWidget: (product) => Text(product?.name ?? ''),
initialValue: products.first, // Set initial value
),
textFieldData: TextFieldData(
title: 'Search products',
prefixIcon: Icon(Icons.search),
onSearch: (pattern, product) {
if (pattern == null || pattern.isEmpty) return true;
final query = pattern.toLowerCase();
return product?.name.toLowerCase().contains(query) ??
false || product?.category.toLowerCase().contains(query) ?? false;
},
),
values: products,
valuesData: ValuesData(
itemBuilder: (context, index, product) => ListTile(
title: Text(product.name),
subtitle: Text('${product.category} - \$${product.price}'),
trailing: Icon(Icons.arrow_forward_ios, size: 16),
),
),
onChanged: (product) {
print('Selected: ${product.name} - \$${product.price}');
},
)
6. Multi-Selection #
Allow users to pick multiple items at once:
Dropinity<void, String>(
controller: DropinityController(),
enableMultiSelection: true,
initialValues: const ['Apple', 'Mango'],
buttonData: ButtonData(
hint: Text('Select fruits'),
selectedItemWidget: (fruit) => Text(fruit ?? ''),
),
textFieldData: TextFieldData(
onSearch: (pattern, fruit) =>
fruit?.toLowerCase().contains(pattern?.toLowerCase() ?? '') ?? false,
),
values: ['Apple', 'Banana', 'Orange', 'Mango', 'Grape'],
valuesData: ValuesData(
itemBuilder: (context, index, fruit) => ListTile(title: Text(fruit)),
),
onChanged: (fruit) => print('Last tapped: $fruit'),
onListChanged: (selected) => print('All selected: $selected'),
)
7. Offline Caching #
Persist paginated API responses so the dropdown works without a network connection:
Dropinity<ApiResponse, User>.withApiRequest(
controller: _dropinityController,
buttonData: ButtonData(
hint: Text('Select user'),
selectedItemWidget: (user) => Text(user?.name ?? ''),
),
pagifyData: DropinityPagifyData(
asyncCall: (context, page) => _fetchUsers(page),
mapper: (response) => PagifyData(
data: response.users,
paginationData: PaginationData(totalPages: response.totalPages, perPage: 20),
),
errorMapper: PagifyErrorMapper(),
itemBuilder: (context, data, index, user) => ListTile(title: Text(user.name)),
// Cache configuration
cacheKey: 'users_dropdown',
cacheToJson: (user) => {'id': user.id, 'name': user.name, 'email': user.email},
cacheFromJson: (json) => User(id: json['id'], name: json['name'], email: json['email']),
onSaveCache: (key, items) {
// Persist using shared_preferences, Hive, etc.
prefs.setString(key, jsonEncode(items));
},
onReadCache: (key) {
final raw = prefs.getString(key);
if (raw == null) return null;
return (jsonDecode(raw) as List).cast<Map<String, dynamic>>();
},
),
onChanged: (user) => print('Selected: ${user?.name}'),
)
8. Expand / Collapse Callbacks #
React to the dropdown opening and closing:
Dropinity<void, String>(
controller: DropinityController(),
onExpand: () => print('Dropdown opened'),
onCollapse: () => print('Dropdown closed'),
// ... rest of config
)
API Reference #
Dropinity Widget #
Constructors
Default Constructor (Local Mode)
Dropinity<void, Model>({
required DropinityController controller,
required ButtonData<Model> buttonData,
required List<Model> values,
required ValuesData<Model> valuesData,
required Function(Model) onChanged,
TextFieldData<Model>? textFieldData,
String? Function(Model?)? validator,
AutovalidateMode? autoValidateMode,
Widget Function(String)? errorWidget,
Widget? dropdownTitle,
double? listHeight,
Curve curve = Curves.linear,
Color listBackgroundColor = Colors.white,
bool maintainState = false,
bool enableMultiSelection = false,
List<Model> initialValues = const [],
void Function(List<Model>)? onListChanged,
void Function()? onExpand,
void Function()? onCollapse,
})
API Constructor (Remote Mode)
Dropinity<FullResponse, Model>.withApiRequest({
required DropinityController controller,
required ButtonData<Model> buttonData,
required DropinityPagifyData<FullResponse, Model> pagifyData,
required Function(Model) onChanged,
TextFieldData<Model>? textFieldData,
String? Function(Model?)? validator,
AutovalidateMode? autoValidateMode,
Widget Function(String)? errorWidget,
Widget? dropdownTitle,
double? listHeight,
Curve curve = Curves.linear,
Color listBackgroundColor = Colors.white,
bool maintainState = false,
bool enableMultiSelection = false,
List<Model> initialValues = const [],
void Function(List<Model>)? onListChanged,
void Function()? onExpand,
void Function()? onCollapse,
})
ButtonData #
Configuration for the dropdown button:
| Parameter | Type | Default | Description |
|---|---|---|---|
selectedItemWidget |
Widget Function(Model?) |
required | Builder for selected item display |
hint |
Widget? |
null |
Placeholder widget when nothing is selected |
initialValue |
Model? |
null |
Pre-selected value (triggers onChanged on init) |
buttonWidth |
double |
double.infinity |
Button width |
buttonHeight |
double |
50 |
Button height |
color |
Color? |
Colors.white |
Background color |
buttonBorderColor |
Color? |
Colors.grey[300] |
Border color |
buttonBorderRadius |
BorderRadius? |
BorderRadius.circular(12) |
Border radius |
padding |
EdgeInsetsGeometry? |
EdgeInsets.all(12) |
Internal padding |
prefixIcon |
Widget? |
null |
Leading icon inside the button |
expandedListIcon |
Widget? |
Icon(Icons.arrow_drop_up) |
Icon when dropdown is open |
collapsedListIcon |
Widget? |
Icon(Icons.arrow_drop_down) |
Icon when dropdown is closed |
TextFieldData #
Configuration for the search text field:
| Parameter | Type | Default | Description |
|---|---|---|---|
onSearch |
bool Function(String?, Model) |
required | Search filter logic |
controller |
TextEditingController? |
null |
Text field controller |
title |
String? |
null |
Label/hint text |
prefixIcon |
Widget? |
null |
Leading icon |
suffixIcon |
Widget? |
null |
Trailing icon |
borderRadius |
double? |
null |
Border radius |
borderColor |
Color? |
null |
Border color |
contentPadding |
EdgeInsetsGeometry? |
null |
Internal padding |
fillColor |
Color? |
null |
Background color |
maxLength |
int? |
null |
Maximum character length |
style |
TextStyle? |
null |
Text style |
ValuesData #
Configuration for local data list:
| Parameter | Type | Description |
|---|---|---|
itemBuilder |
Widget Function(BuildContext, int, Model) |
Builder for list items |
DropinityPagifyData #
Configuration for API-based pagination:
| Parameter | Type | Description |
|---|---|---|
controller |
PagifyController<Model>? |
Pagination controller from Pagify |
asyncCall |
Future<FullResponse> Function(BuildContext, int) |
API call function |
mapper |
PagifyData<Model> Function(FullResponse) |
Response to data mapper |
errorMapper |
PagifyErrorMapper |
Error handling mapper |
itemBuilder |
Widget Function(BuildContext, PagifyData, int, Model) |
List item builder |
padding |
EdgeInsetsGeometry |
List padding |
itemExtent |
double? |
Fixed item height |
loadingBuilder |
Widget? |
Custom loading indicator |
errorBuilder |
Widget Function(PagifyException)? |
Custom error widget (receives typed exception) |
emptyListView |
Widget? |
Widget when list is empty |
noConnectionText |
String? |
No connection message |
showNoDataAlert |
bool |
Show alert when the API returns an empty list |
ignoreErrorBuilderWhenErrorOccursAndListIsNotEmpty |
bool |
Suppress error widget if list already has data |
cacheKey |
String? |
Key used to store/read cached data |
cacheToJson |
Map<String, dynamic> Function(Model)? |
Converts a model item to JSON for caching |
cacheFromJson |
Model Function(Map<String, dynamic>)? |
Restores a model item from cached JSON |
onSaveCache |
void Function(String key, List<Map<String, dynamic>>)? |
Called to persist the cache |
onReadCache |
List<Map<String, dynamic>>? Function(String key)? |
Called to restore the cache |
onUpdateStatus |
FutureOr<void> Function(PagifyAsyncCallStatus)? |
Status change callback |
onLoading |
FutureOr<void> Function()? |
Loading state callback |
onSuccess |
FutureOr<void> Function(BuildContext, List<dynamic>)? |
Success callback |
onError |
FutureOr<void> Function(BuildContext, int, PagifyException)? |
Error callback |
DropinityController #
Methods to programmatically control the dropdown:
final controller = DropinityController();
// Methods
controller.expand(); // Open the dropdown
controller.collapse(); // Close the dropdown
controller.dispose(); // Clean up resources
// Properties
controller.isExpanded; // Returns true if dropdown is open
controller.isCollapsed; // Returns true if dropdown is closed
Advanced Features #
Using TypeDefs for Cleaner Code #
For local-only dropdowns, use a typedef to simplify the syntax:
typedef DropinityLocal<Model> = Dropinity<void, Model>;
// Now you can use:
DropinityLocal<String>(
// ... configuration
)
// Instead of:
Dropinity<void, String>(
// ... configuration
)
Custom Search Logic #
Implement complex search patterns:
TextFieldData<Product>(
onSearch: (pattern, product) {
if (pattern == null || pattern.isEmpty) return true;
final query = pattern.toLowerCase();
// Search across multiple fields
return product?.name.toLowerCase().contains(query) ??
false ||
product?.category.toLowerCase().contains(query) ??
false ||
product?.id.contains(query) ??
false;
},
)
Error Handling in API Mode #
Comprehensive error handling with Pagify:
DropinityPagifyData<ApiResponse, User>(
// ... other configuration
errorMapper: PagifyErrorMapper(
errorWhenDio: (dioError) {
// Handle Dio errors
return PagifyApiRequestException(
dioError.message ?? 'Unknown error',
pagifyFailure: RequestFailureData(
statusCode: dioError.response?.statusCode,
statusMsg: dioError.response?.statusMessage,
),
);
},
),
errorBuilder: (exception) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: Colors.red),
SizedBox(height: 16),
Text(exception.message, style: TextStyle(color: Colors.red)),
SizedBox(height: 16),
ElevatedButton(
onPressed: () => _pagifyController.refresh(),
child: Text('Retry'),
),
],
),
),
// Keep showing existing list items even when an error occurs on next page load
ignoreErrorBuilderWhenErrorOccursAndListIsNotEmpty: true,
onError: (context, statusCode, exception) {
// Log errors, show snackbar, etc.
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${exception.message}')),
);
},
)
Best Practices #
1. Controller Lifecycle Management #
Always dispose controllers to prevent memory leaks:
@override
void dispose() {
_dropinityController.dispose();
_pagifyController?.dispose(); // If using API mode
super.dispose();
}
2. Choose the Right Mode #
- Local Mode: For < 1000 items, simple static lists, or client-side filtering
- API Mode: For large datasets, server-side pagination, or dynamic data
3. Optimize Search Performance #
// ✅ Good - Case-insensitive, null-safe
onSearch: (pattern, item) =>
item?.toLowerCase().contains(pattern?.toLowerCase() ?? '') ?? false
// ❌ Avoid - Missing null checks, complex operations
onSearch: (pattern, item) =>
item.toUpperCase().split('').reversed.join().contains(pattern)
4. Use Type Aliases #
typedef DropinityLocal<T> = Dropinity<void, T>;
// Cleaner syntax for local dropdowns
DropinityLocal<String>(...)
5. Set Appropriate List Height #
// Prevent overflow on smaller screens
listHeight: MediaQuery.of(context).size.height * 0.3
// Or use fixed height for consistent UI
listHeight: 250
6. Preserve List State #
Enable maintainState to keep the scroll position and loaded pages intact when the user toggles the dropdown:
Dropinity<ApiResponse, User>.withApiRequest(
maintainState: true,
// ...
)
7. Handle Empty States #
valuesData: ValuesData(
itemBuilder: (context, index, item) {
if (items.isEmpty) {
return Center(
child: Text('No items found'),
);
}
return ListTile(title: Text(item));
},
)
Troubleshooting #
Common Issues #
1. Dropdown not showing items
- Ensure
valueslist is not empty in local mode - Check that
asyncCallis returning data in API mode - Verify
itemBuilderis returning a valid widget
2. Search not working
- Confirm
onSearchcallback is implemented correctly - Check for null safety in search logic
- Ensure pattern comparison logic matches your data
3. Validation errors not showing
- Set
autoValidateModetoAutovalidateMode.onUserInteraction - Provide
validatorfunction that returns error strings - Optionally customize with
errorWidget
4. Memory leaks
- Always call
dispose()on controllers in the widget'sdisposemethod - Don't forget to dispose both
DropinityControllerandPagifyController
5. API pagination not loading
- Verify
mappercorrectly extracts data from API response - Check
PaginationDatahas correcttotalPagesvalue - Ensure
asyncCallreturns properly formatted response
Debug Mode #
Enable debug logging:
// In your API calls
asyncCall: (context, page) async {
print('Fetching page: $page');
final response = await yourApiCall(page);
print('Received ${response.data.length} items');
return response;
}
Contributing #
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Development Setup #
git clone https://github.com/ahmedemara231/dropinity.git
cd dropinity
flutter pub get
flutter test
Guidelines #
- Follow Effective Dart guidelines
- Add tests for new features
- Update documentation for API changes
- Run
flutter analyzebefore committing
Related Packages #
- Pagify - Pagination helper used for API mode
- dropdown_button2 - Alternative dropdown implementation
- searchable_dropdown - Another searchable dropdown package
Changelog #
See CHANGELOG.md for version history and updates.
License #
This project is licensed under the MIT License - see the LICENSE file for details.
MIT License
Copyright (c) 2025 Ahmed Emara
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
Author #
Ahmed Emara
- LinkedIn: Ahmed Emara
- GitHub: @ahmedemara231
- Email: Contact via GitHub
Support #
If you find this package useful, please consider:
- ⭐ Starring the repository
- 🐛 Reporting bugs via GitHub Issues
- 💡 Suggesting features
- 📖 Improving documentation
- 👍 Liking on pub.dev
Made with ❤️ for the Flutter community
