dropinity 0.0.1 copy "dropinity: ^0.0.1" to clipboard
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 item
  • hint: Widget shown when no item is selected
  • buttonWidth: Width of the button (default: double.infinity)
  • buttonHeight: Height of the button (default: 50)
  • buttonBorderRadius: Border radius of the button
  • buttonBorderColor: Color of the button border
  • color: Background color of the button
  • padding: Internal padding of the button
  • expandedListIcon: Icon shown when dropdown is open
  • collapsedListIcon: 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 (returns true if item matches)
  • controller: Text editing controller for the search field
  • title: Label/hint text for the search field
  • prefixIcon: Icon at the start of the search field
  • suffixIcon: Icon at the end of the search field
  • borderRadius: Border radius of the search field
  • borderColor: Color of the search field border
  • contentPadding: Internal padding of the search field
  • fillColor: Background color of the search field
  • maxLength: Maximum character length
  • style: 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 instance
  • asyncCall: Function to fetch paginated data
  • mapper: Maps API response to PagifyData
  • errorMapper: Maps errors from the response
  • itemBuilder: Builder for each list item
  • loadingBuilder: Widget shown while loading
  • errorBuilder: Widget shown on error
  • emptyListView: Widget shown when list is empty
  • padding: Padding around the list
  • itemExtent: Fixed height for list items
  • noConnectionText: Message for no internet connection
  • onUpdateStatus: Callback for status changes
  • onLoading: Callback when loading starts
  • onSuccess: Callback on successful load
  • onError: 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 #

  1. For Large Datasets: Use API mode (Dropify.withApiRequest) with pagination
  2. Search Optimization: Implement efficient onSearch logic to avoid performance issues
  3. State Management: The widget uses maintainState: true to preserve state when toggling visibility
  4. Memory Usage: Local mode keeps all data in memory, so use API mode for very large datasets
  5. 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

8
likes
130
points
134
downloads

Publisher

unverified uploader

Weekly Downloads

a powerful and customizable paginated dropdown widget for Flutter

Repository (GitHub)
View/report issues

Topics

#dropdown #pagination #lazyloading #network #api

Documentation

API reference

License

MIT (license)

Dependencies

flutter, pagify

More

Packages that depend on dropinity