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
