Multi Reorderable

A powerful and customizable Flutter widget for multi-selection and animated reordering of items.

Portfolio

Work with me

Features

  • Multi-selection: Select multiple items with checkboxes
  • Drag & Drop Reordering: Easily reorder items with smooth animations
  • Animated Stacking: Selected items stack visually when being dragged
  • Customizable UI: Extensive theming options and builder patterns
  • Selection Management: Built-in selection state management with callbacks
  • Auto-scrolling: Automatically scrolls when dragging near edges
  • Header & Footer Support: Add custom widgets above and below the list
  • Pagination Support: Load more items as the user scrolls, with state preservation
  • Pull-to-Refresh: Standard pull-to-refresh functionality for reloading data
  • Programmable Refresh: Refresh the list from outside using a GlobalKey

Examples

Simple Usage

Simple Example

Advanced Usage

Advanced Example

Drag Styles

Stacked Style

The default style stacks items neatly behind the dragged item: Stacked Style

Animated Cards

Provides a modern animated card interface for your dragged items: Animated Cards

Minimalist

A clean, simple approach with minimal visual elements: Minimalist Style

Changing between styles is simple:

ReorderableMultiDragTheme(
  // Normal theme properties
  draggedItemBorderColor: Colors.blue,
  itemBorderRadius: 8.0,
  // Set the drag style
  dragStyle: DragStyle.animatedCardStyle, // or .stackedStyle, .minimalistStyle
)

Installation

Add the package to your pubspec.yaml:

dependencies:
  multi_reorderable: ^0.2.0

Then import it in your Dart code:

import 'package:multi_reorderable/multi_reorderable.dart';

Basic Usage

ReorderableMultiDragList<String>(
  items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'],
  itemBuilder: (context, item, index, isSelected, isDragging) {
    return Container(
      height: 60,
      padding: const EdgeInsets.all(8),
      child: Text(
        item,
        style: TextStyle(
          fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
          color: isDragging ? Colors.grey : Colors.black,
        ),
      ),
    );
  },
  onReorder: (reorderedItems) {
    setState(() {
      items = reorderedItems;
    });
  },
  onSelectionChanged: (selectedItems) {
    print('Selected items: $selectedItems');
  },
  onDone: (selectedItems) {
    print('Done with selection: $selectedItems');
  },
)

Customization

Theming

Customize the appearance using the ReorderableMultiDragTheme:

ReorderableMultiDragList<String>(
  // ... other properties
  theme: ReorderableMultiDragTheme(
    itemColor: Colors.white,
    selectedItemColor: Colors.blue.shade50,
    selectionBarColor: Colors.blue.shade100,
    draggedItemBorderColor: Colors.blue,
    itemBorderRadius: 8.0,
    itemHorizontalMargin: 8.0,
    itemVerticalMargin: 4.0,
    maxStackOffset: 6.0,
    maxStackRotation: 3.0,
  ),
)

Custom Builders

Use builder functions for advanced customization:

ReorderableMultiDragList<String>(
  // ... other properties
  selectionBarBuilder: (context, selectedCount, onDone) {
    return Container(
      padding: const EdgeInsets.all(16),
      color: Colors.blueGrey.shade100,
      child: Row(
        children: [
          Text('$selectedCount items selected'),
          const Spacer(),
          ElevatedButton(
            onPressed: onDone,
            child: const Text('Apply'),
          ),
        ],
      ),
    );
  },
  dragHandleBuilder: (context, isSelected) {
    return Icon(
      Icons.drag_indicator,
      color: isSelected ? Colors.blue : Colors.grey,
    );
  },
)

Additional Options

ReorderableMultiDragList<String>(
  // ... other properties
  showDoneButton: true,
  doneButtonText: 'Apply',
  showSelectionCount: true,
  selectionCountText: '{} selected',
  itemHeight: 70.0,
  showDividers: true,
  dragAnimationDuration: const Duration(milliseconds: 200),
  reorderAnimationDuration: const Duration(milliseconds: 300),
  autoScrollSpeed: 15.0,
  autoScrollThreshold: 100.0,
  headerWidget: Container(
    padding: const EdgeInsets.all(16),
    child: const Text('My Items', style: TextStyle(fontWeight: FontWeight.bold)),
  ),
  footerWidget: Container(
    padding: const EdgeInsets.all(16),
    child: const Text('Swipe to see more'),
  ),
)

Pagination

The widget now offers improved pagination with external control to prevent duplicate page loading:

// Create a global key to access the widget's state
final listKey = GlobalKey<ReorderableMultiDragListState<MyItem>>();

// Track your current page in your state
int currentPage = 1;

// In your build method
ReorderableMultiDragList<MyItem>(
  listKey: listKey,
  items: myItems,
  pageSize: 20, // Number of items per page
  currentPage: currentPage, // Pass your current page
  totalPages: totalPages, // Optional - if you know the total number of pages
  onPageRequest: (page, pageSize) async {
    print('Loading page: $page'); 
    
    // Make your API request with the provided page number
    final response = await yourApi.fetchItems(page: page, pageSize: pageSize);
    
    setState(() {
      // Add new items to your list (not replace)
      myItems.addAll(response.items);
      // IMPORTANT: Update your current page to match the loaded page
      currentPage = page;
    });
  },
  // Customize loading indicator (optional)
  loadingWidgetBuilder: (context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        const CircularProgressIndicator(),
        const SizedBox(height: 8),
        const Text('Loading more items...'),
      ],
    );
  },
  // Customize "no more items" message (optional)
  noMoreItemsBuilder: (context) {
    return Container(
      padding: const EdgeInsets.all(16),
      child: const Text(
        "You've reached the end of the list",
        style: TextStyle(
          fontStyle: FontStyle.italic,
          color: Colors.grey,
        ),
      ),
    );
  },
  // ... other properties
)

Key Pagination Features:

  1. External page control: The currentPage parameter lets you track and control page loading externally
  2. Prevent duplicate loading: Widget uses the provided currentPage to ensure pages aren't loaded twice
  3. Total pages limit: Optional totalPages parameter to stop pagination at a known limit
  4. Smart empty state: Shows appropriate messages for both loading and "no more items" states
  5. Customizable indicators: Custom builders for both loading indicators and end-of-list messages

Pull-to-Refresh

Enable pull-to-refresh functionality:

ReorderableMultiDragList<MyItem>(
  // ... other properties
  enablePullToRefresh: true, // Enable pull-to-refresh
  onRefresh: () async {
    // Clear your existing items
    setState(() {
      myItems.clear();
    });
    
    // Fetch fresh data (first page)
    final response = await yourApi.fetchItems(page: 1);
    
    setState(() {
      myItems.addAll(response.items);
    });
  },
  // Customize refresh indicator (optional)
  refreshIndicatorColor: Colors.blue,
  refreshIndicatorBackgroundColor: Colors.white,
  refreshIndicatorDisplacement: 40.0,
)

If you don't provide an onRefresh callback, the widget will automatically use the onPageRequest callback with page 1.

Programmatic Refresh

Refresh the list from outside using the GlobalKey:

// Refresh the list programmatically
void refreshList() {
  // Reset pagination (optional)
  listKey.currentState?.refreshItems(resetPagination: true);
}

// Use in a button or other event
FloatingActionButton(
  onPressed: refreshList,
  child: Icon(Icons.refresh),
)

Example

Check out the /example folder for a complete implementation.

About the Developer

This package is developed and maintained by Abdelrahman Atef, a Flutter developer specializing in creating custom, high-quality UI components and applications.

Work with me

Feel free to reach out for custom development or modifications to this package.

License

This package is available under the MIT License.

Libraries

multi_reorderable